Untested merge of master.
authorCarl Hetherington <cth@carlh.net>
Sun, 21 Apr 2013 21:41:52 +0000 (22:41 +0100)
committerCarl Hetherington <cth@carlh.net>
Sun, 21 Apr 2013 21:41:52 +0000 (22:41 +0100)
34 files changed:
1  2 
debian/changelog
debian/rules
src/lib/ab_transcoder.cc
src/lib/audio_decoder.h
src/lib/audio_sink.h
src/lib/audio_source.cc
src/lib/audio_source.h
src/lib/decoder.h
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_decoder.h
src/lib/film.cc
src/lib/format.cc
src/lib/format.h
src/lib/imagemagick_decoder.cc
src/lib/imagemagick_decoder.h
src/lib/matcher.cc
src/lib/player.cc
src/lib/player.h
src/lib/processor.h
src/lib/sndfile_decoder.cc
src/lib/sndfile_decoder.h
src/lib/transcoder.cc
src/lib/util.cc
src/lib/util.h
src/lib/video_decoder.cc
src/lib/video_decoder.h
src/lib/video_sink.h
src/lib/video_source.cc
src/lib/video_source.h
src/wx/film_viewer.cc
src/wx/film_viewer.h
test/test.cc
wscript

diff --combined debian/changelog
index 0a53eff154451ca9b659f31aa5a91be933cadb00,eab1f4b4fa3bc60373c0780a57f1c2e322b4cdf8..a0b068d5590d3bd4d97f9f006bbdbb88b6612dc1
- dcpomatic (0.83-1) UNRELEASED; urgency=low
+ dvdomatic (0.84-1) UNRELEASED; urgency=low
+   * New upstream release.
+  -- Carl Hetherington <carl@houllier.lan>  Sun, 21 Apr 2013 17:49:54 +0100
+ dvdomatic (0.84beta5-1) UNRELEASED; urgency=low
+   * New upstream release.
+  -- Carl Hetherington <carl@houllier.lan>  Sun, 21 Apr 2013 00:06:12 +0100
+ dvdomatic (0.84beta4-1) UNRELEASED; urgency=low
+   * New upstream release.
+  -- Carl Hetherington <carl@houllier.lan>  Fri, 19 Apr 2013 17:41:58 +0100
+ dvdomatic (0.84beta3-1) UNRELEASED; urgency=low
+   * New upstream release.
+  -- Carl Hetherington <carl@houllier.lan>  Fri, 19 Apr 2013 11:36:37 +0100
+ dvdomatic (0.84beta2-1) UNRELEASED; urgency=low
+   * New upstream release.
+  -- Carl Hetherington <carl@houllier.lan>  Fri, 19 Apr 2013 11:12:09 +0100
+ dvdomatic (0.84beta1-1) UNRELEASED; urgency=low
+   * New upstream release.
+  -- Carl Hetherington <carl@houllier.lan>  Thu, 18 Apr 2013 23:32:17 +0100
+ dvdomatic (0.83-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 10 Apr 2013 12:48:25 +0100
  
 -dvdomatic (0.82-1) UNRELEASED; urgency=low
 +dcpomatic (0.82-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 09 Apr 2013 23:43:35 +0100
  
 -dvdomatic (0.82beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.82beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 09 Apr 2013 21:48:56 +0100
  
 -dvdomatic (0.81-1) UNRELEASED; urgency=low
 +dcpomatic (0.81-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 09 Apr 2013 19:48:04 +0100
  
 -dvdomatic (0.81beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.81beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 09 Apr 2013 15:37:32 +0100
  
 -dvdomatic (0.80-1) UNRELEASED; urgency=low
 +dcpomatic (0.80-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 07 Apr 2013 23:48:12 +0100
  
 -dvdomatic (0.80beta4-1) UNRELEASED; urgency=low
 +dcpomatic (0.80beta4-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 07 Apr 2013 23:08:49 +0100
  
 -dvdomatic (0.80beta3-1) UNRELEASED; urgency=low
 +dcpomatic (0.80beta3-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 07 Apr 2013 22:44:29 +0100
  
 -dvdomatic (0.80beta2-1) UNRELEASED; urgency=low
 +dcpomatic (0.80beta2-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 07 Apr 2013 22:19:34 +0100
  
 -dvdomatic (0.80beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.80beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 07 Apr 2013 18:21:33 +0100
  
 -dvdomatic (0.79-1) UNRELEASED; urgency=low
 +dcpomatic (0.79-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Mon, 01 Apr 2013 22:37:03 +0100
  
 -dvdomatic (0.78-1) UNRELEASED; urgency=low
 +dcpomatic (0.78-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 31 Mar 2013 02:43:03 +0100
  
 -dvdomatic (0.78beta16-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta16-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 28 Mar 2013 16:28:05 +0000
  
 -dvdomatic (0.78beta15-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta15-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 28 Mar 2013 14:25:56 +0000
  
 -dvdomatic (0.78beta14-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta14-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 28 Mar 2013 10:38:07 +0000
  
 -dvdomatic (0.78beta13-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta13-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 27 Mar 2013 12:26:55 +0000
  
 -dvdomatic (0.78beta12-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta12-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 26 Mar 2013 21:13:54 +0000
  
 -dvdomatic (0.78beta11-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta11-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 26 Mar 2013 17:34:49 +0000
  
 -dvdomatic (0.78beta10-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta10-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 26 Mar 2013 11:35:15 +0000
  
 -dvdomatic (0.78beta9-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta9-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 26 Mar 2013 10:36:05 +0000
  
 -dvdomatic (0.78beta8-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta8-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 26 Mar 2013 00:59:36 +0000
  
 -dvdomatic (0.78beta7-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta7-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 26 Mar 2013 00:19:21 +0000
  
 -dvdomatic (0.78beta6-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta6-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Mon, 25 Mar 2013 00:08:10 +0000
  
 -dvdomatic (0.78beta5-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta5-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 21 Mar 2013 16:32:21 +0000
  
 -dvdomatic (0.78beta4-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta4-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 20 Mar 2013 15:01:10 +0000
  
 -dvdomatic (0.78beta3-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta3-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 20 Mar 2013 10:49:17 +0000
  
 -dvdomatic (0.78beta2-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta2-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 19 Mar 2013 21:35:50 +0000
  
 -dvdomatic (0.78beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.78beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 19 Mar 2013 20:50:54 +0000
  
 -dvdomatic (0.77-1) UNRELEASED; urgency=low
 +dcpomatic (0.77-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 14 Mar 2013 17:12:03 +0000
  
 -dvdomatic (0.77beta2-1) UNRELEASED; urgency=low
 +dcpomatic (0.77beta2-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 14 Mar 2013 15:50:43 +0000
  
 -dvdomatic (0.77beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.77beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 14 Mar 2013 15:14:01 +0000
  
 -dvdomatic (0.76-1) UNRELEASED; urgency=low
 +dcpomatic (0.76-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 05 Mar 2013 13:30:28 +0000
  
 -dvdomatic (0.76beta3-1) UNRELEASED; urgency=low
 +dcpomatic (0.76beta3-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 05 Mar 2013 12:47:20 +0000
  
 -dvdomatic (0.76beta2-1) UNRELEASED; urgency=low
 +dcpomatic (0.76beta2-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Fri, 01 Mar 2013 18:32:16 +0000
  
 -dvdomatic (0.76beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.76beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Fri, 01 Mar 2013 17:36:55 +0000
  
 -dvdomatic (0.75-1) UNRELEASED; urgency=low
 +dcpomatic (0.75-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 27 Feb 2013 11:03:07 +0000
  
 -dvdomatic (0.75beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.75beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 27 Feb 2013 08:20:42 +0000
  
 -dvdomatic (0.74-1) UNRELEASED; urgency=low
 +dcpomatic (0.74-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sat, 23 Feb 2013 22:57:20 +0000
  
 -dvdomatic (0.74beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.74beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sat, 23 Feb 2013 21:44:22 +0000
  
 -dvdomatic (0.73-1) UNRELEASED; urgency=low
 +dcpomatic (0.73-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 21 Feb 2013 00:43:40 +0000
  
 -dvdomatic (0.73beta9-1) UNRELEASED; urgency=low
 +dcpomatic (0.73beta9-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 20 Feb 2013 23:40:24 +0000
  
 -dvdomatic (0.73beta8-1) UNRELEASED; urgency=low
 +dcpomatic (0.73beta8-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Mon, 18 Feb 2013 22:35:51 +0000
  
 -dvdomatic (0.73beta7-1) UNRELEASED; urgency=low
 +dcpomatic (0.73beta7-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Mon, 18 Feb 2013 20:38:51 +0000
  
 -dvdomatic (0.73beta6-1) UNRELEASED; urgency=low
 +dcpomatic (0.73beta6-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 17 Feb 2013 23:05:56 +0000
  
 -dvdomatic (0.73beta3-1) UNRELEASED; urgency=low
 +dcpomatic (0.73beta3-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 17 Feb 2013 23:05:05 +0000
  
 -dvdomatic (0.73beta2-1) UNRELEASED; urgency=low
 +dcpomatic (0.73beta2-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sat, 16 Feb 2013 22:42:32 +0000
  
 -dvdomatic (0.73beta1-1) UNRELEASED; urgency=low
 +dcpomatic (0.73beta1-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sat, 16 Feb 2013 21:19:24 +0000
  
 -dvdomatic (0.72-1) UNRELEASED; urgency=low
 +dcpomatic (0.72-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 24 Jan 2013 15:31:57 +0000
  
 -dvdomatic (0.71-1) UNRELEASED; urgency=low
 +dcpomatic (0.71-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 24 Jan 2013 11:36:04 +0000
  
 -dvdomatic (0.70-1) UNRELEASED; urgency=low
 +dcpomatic (0.70-1) UNRELEASED; urgency=low
  
    * New upstream release.
    * New upstream release.
  
   -- Carl Hetherington <cth@carlh.net>  Sat, 12 Jan 2013 23:07:15 +0000
  
 -dvdomatic (0.70beta3-1) UNRELEASED; urgency=low
 +dcpomatic (0.70beta3-1) UNRELEASED; urgency=low
  
    * New upstream release.
    * New upstream release.
  
   -- Carl Hetherington <cth@carlh.net>  Sun, 06 Jan 2013 23:44:24 +0000
  
 -dvdomatic (0.68-1) UNRELEASED; urgency=low
 +dcpomatic (0.68-1) UNRELEASED; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 23 Dec 2012 01:43:44 +0000
  
 -dvdomatic (0.68beta10-1) UNRELEASED; urgency=low
 +dcpomatic (0.68beta10-1) UNRELEASED; urgency=low
  
    * New upstream release.
    * New upstream release.
  
   -- Carl Hetherington <cth@carlh.net>  Sat, 22 Dec 2012 13:27:27 +0000
  
 -dvdomatic (0.68beta5-1) unstable; urgency=low
 +dcpomatic (0.68beta5-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 20 Dec 2012 07:53:46 +0000
  
 -dvdomatic (0.68beta4-1) unstable; urgency=low
 +dcpomatic (0.68beta4-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 20 Dec 2012 07:48:45 +0000
  
 -dvdomatic (0.68beta3-1) unstable; urgency=low
 +dcpomatic (0.68beta3-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 20 Dec 2012 00:35:45 +0000
  
 -dvdomatic (0.68beta2-1) unstable; urgency=low
 +dcpomatic (0.68beta2-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 19 Dec 2012 11:22:58 +0000
  
 -dvdomatic (0.68beta1-1) unstable; urgency=low
 +dcpomatic (0.68beta1-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Wed, 19 Dec 2012 10:11:13 +0000
  
 -dvdomatic (0.67-1) unstable; urgency=low
 +dcpomatic (0.67-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 18 Dec 2012 23:49:27 +0000
  
 -dvdomatic (0.66-1) unstable; urgency=low
 +dcpomatic (0.66-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 18 Dec 2012 11:29:04 +0000
  
 -dvdomatic (0.65-1) unstable; urgency=low
 +dcpomatic (0.65-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 18 Dec 2012 09:24:56 +0000
  
 -dvdomatic (0.64-1) unstable; urgency=low
 +dcpomatic (0.64-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Thu, 13 Dec 2012 21:52:09 +0000
  
 -dvdomatic (0.63pre-1) unstable; urgency=low
 +dcpomatic (0.63pre-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 11 Dec 2012 23:15:52 +0000
  
 -dvdomatic (0.60-1) unstable; urgency=low
 +dcpomatic (0.60-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Tue, 11 Dec 2012 22:46:04 +0000
  
 -dvdomatic (0.59-1) unstable; urgency=low
 +dcpomatic (0.59-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Mon, 10 Dec 2012 20:58:19 +0000
  
 -dvdomatic (0.59beta5-1) unstable; urgency=low
 +dcpomatic (0.59beta5-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 09 Dec 2012 23:51:55 +0000
  
 -dvdomatic (0.59beta4-1) unstable; urgency=low
 +dcpomatic (0.59beta4-1) unstable; urgency=low
  
    * New upstream release.
  
   -- Carl Hetherington <carl@houllier.lan>  Sun, 09 Dec 2012 21:38:00 +0000
  
 -dvdomatic (0.59beta1-1) unstable; urgency=low
 +dcpomatic (0.59beta1-1) unstable; urgency=low
  
    * Initial release.
  
diff --combined debian/rules
index ba42e5291fabb5689821c7eee5ba168a9a964063,a2bf70bd87a595913382c4de942d07f3016e6767..7f7da5530eddbb8d128f2ba9035454482dbe7844
        dh $@ 
  
  override_dh_auto_configure:
-       ./waf --nocache configure --prefix=/usr --static
+       LINKFLAGS=$(CDIST_LINKFLAGS) CXXFLAGS="$(CXXFLAGS) $(CDIST_CXXFLAGS)" PKG_CONFIG_PATH=$(CDIST_PKG_CONFIG_PATH) \
+                ./waf --nocache configure --prefix=/usr --static
  
  override_dh_auto_build:
        ./waf --nocache build
  
  override_dh_auto_install:
 -      ./waf --nocache install --destdir=debian/dvdomatic
 +      ./waf --nocache install --destdir=debian/dcpomatic
  
diff --combined src/lib/ab_transcoder.cc
index 81aa5a4bae2192d5622689f32f57ee9f7724d2ae,6eef397c245e69db4700ccc4454964b49ffabc33..c6ccfdc67e268492a9cae8eca5de06f53c7b3daf
  #include <boost/shared_ptr.hpp>
  #include "ab_transcoder.h"
  #include "film.h"
 -#include "video_decoder.h"
 -#include "audio_decoder.h"
  #include "encoder.h"
  #include "job.h"
 -#include "options.h"
  #include "image.h"
 -#include "decoder_factory.h"
 +#include "player.h"
  #include "matcher.h"
  #include "delay_line.h"
  #include "gain.h"
@@@ -46,37 -49,38 +46,30 @@@ using boost::dynamic_pointer_cast
   *  @param e Encoder to use.
   */
  
 -ABTranscoder::ABTranscoder (
 -      shared_ptr<Film> a, shared_ptr<Film> b, DecodeOptions o, Job* j, shared_ptr<Encoder> e)
 +ABTranscoder::ABTranscoder (shared_ptr<Film> a, shared_ptr<Film> b, shared_ptr<Job> j)
        : _film_a (a)
        , _film_b (b)
 +      , _player_a (_film_a->player ())
 +      , _player_b (_film_b->player ())
        , _job (j)
 -      , _encoder (e)
 +      , _encoder (new Encoder (_film_a))
        , _combiner (new Combiner (a->log()))
  {
-       if (_film_a->has_audio ()) {
-               _matcher.reset (new Matcher (_film_a->log(), _film_a->audio_frame_rate(), _film_a->video_frame_rate()));
-               _delay_line.reset (new DelayLine (_film_a->log(), _film_a->audio_channels(), _film_a->audio_delay() * _film_a->audio_frame_rate() / 1000));
-               _gain.reset (new Gain (_film_a->log(), _film_a->audio_gain()));
-       }
 -      _da = decoder_factory (_film_a, o);
 -      _db = decoder_factory (_film_b, o);
 -
 -      shared_ptr<AudioStream> st = _film_a->audio_stream();
 -      _matcher.reset (new Matcher (_film_a->log(), st->sample_rate(), _film_a->source_frame_rate()));
 -      _delay_line.reset (new DelayLine (_film_a->log(), _film_a->audio_delay() / 1000.0f));
++      _matcher.reset (new Matcher (_film_a->log(), _film_a->audio_frame_rate(), _film_a->video_frame_rate()));
++      _delay_line.reset (new DelayLine (_film_a->log(), _film_a->audio_delay() * _film_a->audio_frame_rate() / 1000));
+       _gain.reset (new Gain (_film_a->log(), _film_a->audio_gain()));
  
-       _player_a->Video.connect (bind (&Combiner::process_video, _combiner, _1, _2, _3));
-       _player_b->Video.connect (bind (&Combiner::process_video_b, _combiner, _1, _2, _3));
 -      /* Set up the decoder to use the film's set streams */
 -      _da.video->set_subtitle_stream (_film_a->subtitle_stream ());
 -      _db.video->set_subtitle_stream (_film_a->subtitle_stream ());
 -      _da.audio->set_audio_stream (_film_a->audio_stream ());
 -
 -      _da.video->Video.connect (bind (&Combiner::process_video, _combiner, _1, _2, _3, _4));
 -      _db.video->Video.connect (bind (&Combiner::process_video_b, _combiner, _1, _2, _3, _4));
++      _player_a->Video.connect (bind (&Combiner::process_video, _combiner, _1, _2, _3, _4));
++      _player_b->Video.connect (bind (&Combiner::process_video_b, _combiner, _1, _2, _3, _4));
  
-       if (_matcher) {
-               _combiner->connect_video (_matcher);
-               _matcher->connect_video (_encoder);
-       } else {
-               _combiner->connect_video (_encoder);
-       }
+       _combiner->connect_video (_delay_line);
+       _delay_line->connect_video (_matcher);
+       _matcher->connect_video (_encoder);
        
-       if (_matcher && _delay_line) {
-               _player_a->connect_audio (_delay_line);
-               _delay_line->connect_audio (_matcher);
-               _matcher->connect_audio (_gain);
-               _gain->connect_audio (_encoder);
-       }
 -      _da.audio->connect_audio (_delay_line);
++      _player_a->connect_audio (_delay_line);
+       _delay_line->connect_audio (_matcher);
+       _matcher->connect_audio (_gain);
+       _gain->connect_audio (_encoder);
  }
  
  void
@@@ -84,17 -88,23 +77,17 @@@ ABTranscoder::go (
  {
        _encoder->process_begin ();
  
 -      bool done[3] = { false, false, false };
 +      bool done[2] = { false, false };
        
        while (1) {
 -              done[0] = _da.video->pass ();
 -              done[1] = _db.video->pass ();
 -              
 -              if (!done[2] && _da.audio && dynamic_pointer_cast<Decoder> (_da.audio) != dynamic_pointer_cast<Decoder> (_da.video)) {
 -                      done[2] = _da.audio->pass ();
 -              } else {
 -                      done[2] = true;
 -              }
 +              done[0] = _player_a->pass ();
 +              done[1] = _player_b->pass ();
  
                if (_job) {
 -                      _da.video->set_progress (_job);
 +                      _player_a->set_progress (_job);
                }
  
 -              if (done[0] && done[1] && done[2]) {
 +              if (done[0] && done[1]) {
                        break;
                }
        }
diff --combined src/lib/audio_decoder.h
index 418fc6da2b117deb53790bbf890a10ba922ed164,cfe94b5283dc48c06a83e516b526c789dae46861..c393e95f12b776f3b93172a4539159c6b78fa8a2
   *  @brief Parent class for audio decoders.
   */
  
 -#ifndef DVDOMATIC_AUDIO_DECODER_H
 -#define DVDOMATIC_AUDIO_DECODER_H
 +#ifndef DCPOMATIC_AUDIO_DECODER_H
 +#define DCPOMATIC_AUDIO_DECODER_H
  
  #include "audio_source.h"
 -#include "stream.h"
  #include "decoder.h"
  
 +class AudioContent;
 +
  /** @class AudioDecoder.
   *  @brief Parent class for audio decoders.
   */
- class AudioDecoder : public AudioSource, public virtual Decoder
+ class AudioDecoder : public TimedAudioSource, public virtual Decoder
  {
  public:
 -      AudioDecoder (boost::shared_ptr<Film>, DecodeOptions);
 -
 -      virtual void set_audio_stream (boost::shared_ptr<AudioStream>);
 -
 -      /** @return Audio stream that we are using */
 -      boost::shared_ptr<AudioStream> audio_stream () const {
 -              return _audio_stream;
 -      }
 -
 -      /** @return All available audio streams */
 -      std::vector<boost::shared_ptr<AudioStream> > audio_streams () const {
 -              return _audio_streams;
 -      }
 -
 -protected:
 -      /** Audio stream that we are using */
 -      boost::shared_ptr<AudioStream> _audio_stream;
 -      /** All available audio streams */
 -      std::vector<boost::shared_ptr<AudioStream> > _audio_streams;
 +      AudioDecoder (boost::shared_ptr<const Film>);
  };
  
  #endif
diff --combined src/lib/audio_sink.h
index 08549165710530435f37455ca1562329be1272bb,f34b24f88369feef2b23729390c9ec13b7818e9a..ba4b772c8a66b02057d070e8d014473479d76604
@@@ -17,8 -17,8 +17,8 @@@
  
  */
  
 -#ifndef DVDOMATIC_AUDIO_SINK_H
 -#define DVDOMATIC_AUDIO_SINK_H
 +#ifndef DCPOMATIC_AUDIO_SINK_H
 +#define DCPOMATIC_AUDIO_SINK_H
  
  class AudioSink
  {
@@@ -27,4 -27,11 +27,11 @@@ public
        virtual void process_audio (boost::shared_ptr<AudioBuffers>) = 0;
  };
  
+ class TimedAudioSink
+ {
+ public:
+         /** Call with some audio data */
+         virtual void process_audio (boost::shared_ptr<AudioBuffers>, double t) = 0;
+ };
  #endif
diff --combined src/lib/audio_source.cc
index 99b59759d07eac7250126429515f7c00863f9677,bca3562cf974f36ea45077b1a08ed01142ea79a2..3dd3027ab4650bd23fd4ef4fc8fd958438e5a7f4
  #include "audio_sink.h"
  
  using boost::shared_ptr;
 +using boost::weak_ptr;
  using boost::bind;
  
 +static void
 +process_audio_proxy (weak_ptr<AudioSink> sink, shared_ptr<AudioBuffers> audio)
 +{
 +      shared_ptr<AudioSink> p = sink.lock ();
 +      if (p) {
 +              p->process_audio (audio);
 +      }
 +}
 +
  void
  AudioSource::connect_audio (shared_ptr<AudioSink> s)
  {
 -      Audio.connect (bind (&AudioSink::process_audio, s, _1));
 +      Audio.connect (bind (process_audio_proxy, weak_ptr<AudioSink> (s), _1));
  }
+ void
+ TimedAudioSource::connect_audio (shared_ptr<TimedAudioSink> s)
+ {
+       Audio.connect (bind (&TimedAudioSink::process_audio, s, _1, _2));
+ }
diff --combined src/lib/audio_source.h
index ee5c606dc4c90542a9b7e66501bdfd8bfb6300eb,3dc998ccacd7156cf2f766b7f40dff009791fcb2..dd248071651fbbc0e7c1b497e9cfcdf39c899970
   *  @brief Parent class for classes which emit audio data.
   */
  
 -#ifndef DVDOMATIC_AUDIO_SOURCE_H
 -#define DVDOMATIC_AUDIO_SOURCE_H
 +#ifndef DCPOMATIC_AUDIO_SOURCE_H
 +#define DCPOMATIC_AUDIO_SOURCE_H
  
  #include <boost/signals2.hpp>
  
  class AudioBuffers;
  class AudioSink;
+ class TimedAudioSink;
  
  /** A class that emits audio data */
  class AudioSource
@@@ -39,4 -40,15 +40,15 @@@ public
        void connect_audio (boost::shared_ptr<AudioSink>);
  };
  
+ /** A class that emits audio data with timestamps */
+ class TimedAudioSource
+ {
+ public:
+       /** Emitted when some audio data is ready */
+       boost::signals2::signal<void (boost::shared_ptr<AudioBuffers>, double)> Audio;
+       void connect_audio (boost::shared_ptr<TimedAudioSink>);
+ };
  #endif
diff --combined src/lib/decoder.h
index 72b866ffe9e7e6cf2dcad643cbe50b7376569dae,2bc462c33c635013c4111447f43b2610204d562f..20e32bfbf64ce83d58b38672a0a6be5500085eb7
@@@ -21,8 -21,8 +21,8 @@@
   *  @brief Parent class for decoders of content.
   */
  
 -#ifndef DVDOMATIC_DECODER_H
 -#define DVDOMATIC_DECODER_H
 +#ifndef DCPOMATIC_DECODER_H
 +#define DCPOMATIC_DECODER_H
  
  #include <vector>
  #include <string>
  #include <boost/shared_ptr.hpp>
  #include <boost/signals2.hpp>
  #include "util.h"
 -#include "stream.h"
  #include "video_source.h"
  #include "audio_source.h"
  #include "film.h"
 -#include "options.h"
  
  class Image;
  class Log;
@@@ -51,18 -53,26 +51,22 @@@ class FilterGraph
  class Decoder
  {
  public:
 -      Decoder (boost::shared_ptr<Film>, DecodeOptions);
 +      Decoder (boost::shared_ptr<const Film>);
        virtual ~Decoder () {}
  
        virtual bool pass () = 0;
        virtual bool seek (double);
 -      virtual bool seek_to_last ();
+       virtual void seek_back () {}
+       virtual void seek_forward () {}
+       boost::signals2::signal<void()> OutputChanged;
  
  protected:
 -      /** our Film */
 -      boost::shared_ptr<Film> _film;
 -      /** our decode options */
 -      DecodeOptions _opt;
 +      boost::shared_ptr<const Film> _film;
  
  private:
        virtual void film_changed (Film::Property) {}
 -      
 +
        boost::signals2::scoped_connection _film_connection;
  };
  
index 577dbd14d084ba6fe8a19db0c8870935a7d7d9dd,0000000000000000000000000000000000000000..719c4cb53ecf03488d8266954b6309205657bb5e
mode 100644,000000..100644
--- /dev/null
@@@ -1,282 -1,0 +1,282 @@@
-       shared_ptr<FFmpegDecoder> decoder (new FFmpegDecoder (film, shared_from_this (), true, false, false, true));
 +/*
 +    Copyright (C) 2013 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
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include <libcxml/cxml.h>
 +#include "ffmpeg_content.h"
 +#include "ffmpeg_decoder.h"
 +#include "compose.hpp"
 +#include "job.h"
 +#include "util.h"
 +#include "log.h"
 +
 +#include "i18n.h"
 +
 +using std::string;
 +using std::stringstream;
 +using std::vector;
 +using std::list;
 +using std::cout;
 +using boost::shared_ptr;
 +using boost::lexical_cast;
 +
 +int const FFmpegContentProperty::SUBTITLE_STREAMS = 100;
 +int const FFmpegContentProperty::SUBTITLE_STREAM = 101;
 +int const FFmpegContentProperty::AUDIO_STREAMS = 102;
 +int const FFmpegContentProperty::AUDIO_STREAM = 103;
 +
 +FFmpegContent::FFmpegContent (boost::filesystem::path f)
 +      : Content (f)
 +      , VideoContent (f)
 +      , AudioContent (f)
 +{
 +
 +}
 +
 +FFmpegContent::FFmpegContent (shared_ptr<const cxml::Node> node)
 +      : Content (node)
 +      , VideoContent (node)
 +      , AudioContent (node)
 +{
 +      list<shared_ptr<cxml::Node> > c = node->node_children ("SubtitleStream");
 +      for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) {
 +              _subtitle_streams.push_back (FFmpegSubtitleStream (*i));
 +              if ((*i)->optional_number_child<int> ("Selected")) {
 +                      _subtitle_stream = _subtitle_streams.back ();
 +              }
 +      }
 +
 +      c = node->node_children ("AudioStream");
 +      for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) {
 +              _audio_streams.push_back (FFmpegAudioStream (*i));
 +              if ((*i)->optional_number_child<int> ("Selected")) {
 +                      _audio_stream = _audio_streams.back ();
 +              }
 +      }
 +}
 +
 +FFmpegContent::FFmpegContent (FFmpegContent const & o)
 +      : Content (o)
 +      , VideoContent (o)
 +      , AudioContent (o)
 +      , _subtitle_streams (o._subtitle_streams)
 +      , _subtitle_stream (o._subtitle_stream)
 +      , _audio_streams (o._audio_streams)
 +      , _audio_stream (o._audio_stream)
 +{
 +
 +}
 +
 +void
 +FFmpegContent::as_xml (xmlpp::Node* node) const
 +{
 +      node->add_child("Type")->add_child_text ("FFmpeg");
 +      Content::as_xml (node);
 +      VideoContent::as_xml (node);
 +
 +      boost::mutex::scoped_lock lm (_mutex);
 +
 +      for (vector<FFmpegSubtitleStream>::const_iterator i = _subtitle_streams.begin(); i != _subtitle_streams.end(); ++i) {
 +              xmlpp::Node* t = node->add_child("SubtitleStream");
 +              if (_subtitle_stream && *i == _subtitle_stream.get()) {
 +                      t->add_child("Selected")->add_child_text("1");
 +              }
 +              i->as_xml (t);
 +      }
 +
 +      for (vector<FFmpegAudioStream>::const_iterator i = _audio_streams.begin(); i != _audio_streams.end(); ++i) {
 +              xmlpp::Node* t = node->add_child("AudioStream");
 +              if (_audio_stream && *i == _audio_stream.get()) {
 +                      t->add_child("Selected")->add_child_text("1");
 +              }
 +              i->as_xml (t);
 +      }
 +}
 +
 +void
 +FFmpegContent::examine (shared_ptr<Film> film, shared_ptr<Job> job, bool quick)
 +{
 +      job->set_progress_unknown ();
 +
 +      Content::examine (film, job, quick);
 +
++      shared_ptr<FFmpegDecoder> decoder (new FFmpegDecoder (film, shared_from_this (), true, false, false));
 +
 +      ContentVideoFrame video_length = 0;
 +      if (quick) {
 +              video_length = decoder->video_length ();
 +                film->log()->log (String::compose ("Video length obtained from header as %1 frames", decoder->video_length ()));
 +        } else {
 +                while (!decoder->pass ()) {
 +                        /* keep going */
 +                }
 +
 +                video_length = decoder->video_frame ();
 +                film->log()->log (String::compose ("Video length examined as %1 frames", decoder->video_frame ()));
 +        }
 +
 +        {
 +                boost::mutex::scoped_lock lm (_mutex);
 +
 +                _video_length = video_length;
 +
 +                _subtitle_streams = decoder->subtitle_streams ();
 +                if (!_subtitle_streams.empty ()) {
 +                        _subtitle_stream = _subtitle_streams.front ();
 +                }
 +                
 +                _audio_streams = decoder->audio_streams ();
 +                if (!_audio_streams.empty ()) {
 +                        _audio_stream = _audio_streams.front ();
 +                }
 +        }
 +
 +        take_from_video_decoder (decoder);
 +
 +        signal_changed (VideoContentProperty::VIDEO_LENGTH);
 +        signal_changed (FFmpegContentProperty::SUBTITLE_STREAMS);
 +        signal_changed (FFmpegContentProperty::SUBTITLE_STREAM);
 +        signal_changed (FFmpegContentProperty::AUDIO_STREAMS);
 +        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
 +        signal_changed (AudioContentProperty::AUDIO_CHANNELS);
 +}
 +
 +string
 +FFmpegContent::summary () const
 +{
 +      return String::compose (_("Movie: %1"), file().filename().string());
 +}
 +
 +string
 +FFmpegContent::information () const
 +{
 +      if (video_length() == 0 || video_frame_rate() == 0) {
 +              return "";
 +      }
 +      
 +      stringstream s;
 +      
 +      s << String::compose (_("%1 frames; %2 frames per second"), video_length(), video_frame_rate()) << "\n";
 +      s << VideoContent::information ();
 +
 +      return s.str ();
 +}
 +
 +void
 +FFmpegContent::set_subtitle_stream (FFmpegSubtitleStream s)
 +{
 +        {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                _subtitle_stream = s;
 +        }
 +
 +        signal_changed (FFmpegContentProperty::SUBTITLE_STREAM);
 +}
 +
 +void
 +FFmpegContent::set_audio_stream (FFmpegAudioStream s)
 +{
 +        {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                _audio_stream = s;
 +        }
 +
 +        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
 +}
 +
 +ContentAudioFrame
 +FFmpegContent::audio_length () const
 +{
 +        if (!_audio_stream) {
 +                return 0;
 +        }
 +        
 +        return video_frames_to_audio_frames (_video_length, audio_frame_rate(), video_frame_rate());
 +}
 +
 +int
 +FFmpegContent::audio_channels () const
 +{
 +        if (!_audio_stream) {
 +                return 0;
 +        }
 +
 +        return _audio_stream->channels;
 +}
 +
 +int
 +FFmpegContent::audio_frame_rate () const
 +{
 +        if (!_audio_stream) {
 +                return 0;
 +        }
 +
 +        return _audio_stream->frame_rate;
 +}
 +
 +bool
 +operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b)
 +{
 +        return a.id == b.id;
 +}
 +
 +bool
 +operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b)
 +{
 +        return a.id == b.id;
 +}
 +
 +FFmpegAudioStream::FFmpegAudioStream (shared_ptr<const cxml::Node> node)
 +{
 +      name = node->string_child ("Name");
 +      id = node->number_child<int> ("Id");
 +      frame_rate = node->number_child<int> ("FrameRate");
 +      channels = node->number_child<int64_t> ("Channels");
 +}
 +
 +void
 +FFmpegAudioStream::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));
 +      root->add_child("FrameRate")->add_child_text (lexical_cast<string> (frame_rate));
 +      root->add_child("Channels")->add_child_text (lexical_cast<string> (channels));
 +}
 +
 +/** 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)
 +{
 +      name = node->string_child ("Name");
 +      id = node->number_child<int> ("Id");
 +}
 +
 +void
 +FFmpegSubtitleStream::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));
 +}
 +
 +shared_ptr<Content>
 +FFmpegContent::clone () const
 +{
 +      return shared_ptr<Content> (new FFmpegContent (*this));
 +}
index eac1d91aea3c93db48178ed6fd7775e9b830c7af,8e09810cb2cc75c3a6109139e0a0db8427f4eb8e..82c7cafd1b8ec629b1473402ed089b06efad0f16
@@@ -41,6 -41,7 +41,6 @@@ extern "C" 
  #include "transcoder.h"
  #include "job.h"
  #include "filter.h"
 -#include "options.h"
  #include "exceptions.h"
  #include "image.h"
  #include "util.h"
@@@ -61,13 -62,10 +61,13 @@@ using boost::optional
  using boost::dynamic_pointer_cast;
  using libdcp::Size;
  
 -FFmpegDecoder::FFmpegDecoder (shared_ptr<Film> f, DecodeOptions o)
 -      : Decoder (f, o)
 -      , VideoDecoder (f, o)
 -      , AudioDecoder (f, o)
 +boost::mutex FFmpegDecoder::_mutex;
 +
- FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegContent> c, bool video, bool audio, bool subtitles, bool video_sync)
++FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegContent> c, bool video, bool audio, bool subtitles)
 +      : Decoder (f)
 +      , VideoDecoder (f)
 +      , AudioDecoder (f)
 +      , _ffmpeg_content (c)
        , _format_context (0)
        , _video_stream (-1)
        , _frame (0)
        , _audio_codec (0)
        , _subtitle_codec_context (0)
        , _subtitle_codec (0)
-       , _video_sync (video_sync)
 +      , _decode_video (video)
 +      , _decode_audio (audio)
 +      , _decode_subtitles (subtitles)
  {
        setup_general ();
        setup_video ();
        setup_audio ();
        setup_subtitle ();
-       if (!video_sync) {
-               _first_video = 0;
-       }
  }
  
  FFmpegDecoder::~FFmpegDecoder ()
  {
 +      boost::mutex::scoped_lock lm (_mutex);
 +      
        if (_audio_codec_context) {
                avcodec_close (_audio_codec_context);
        }
 -      
 +
        if (_video_codec_context) {
                avcodec_close (_video_codec_context);
        }
@@@ -118,15 -106,15 +113,15 @@@ FFmpegDecoder::setup_general (
  {
        av_register_all ();
  
 -      if (avformat_open_input (&_format_context, _film->content_path().c_str(), 0, 0) < 0) {
 -              throw OpenFileError (_film->content_path ());
 +      if (avformat_open_input (&_format_context, _ffmpeg_content->file().string().c_str(), 0, 0) < 0) {
 +              throw OpenFileError (_ffmpeg_content->file().string ());
        }
  
        if (avformat_find_stream_info (_format_context, 0) < 0) {
                throw DecodeError (_("could not find stream information"));
        }
  
 -      /* Find video, audio and subtitle streams and choose the first of each */
 +      /* Find video, audio and subtitle streams */
  
        for (uint32_t i = 0; i < _format_context->nb_streams; ++i) {
                AVStream* s = _format_context->streams[i];
                        }
                        
                        _audio_streams.push_back (
 -                              shared_ptr<AudioStream> (
 -                                      new FFmpegAudioStream (stream_name (s), i, s->codec->sample_rate, s->codec->channel_layout)
 -                                      )
 +                              FFmpegAudioStream (stream_name (s), i, s->codec->sample_rate, s->codec->channels)
                                );
                        
                } else if (s->codec->codec_type == AVMEDIA_TYPE_SUBTITLE) {
 -                      _subtitle_streams.push_back (
 -                              shared_ptr<SubtitleStream> (
 -                                      new SubtitleStream (stream_name (s), i)
 -                                      )
 -                              );
 +                      _subtitle_streams.push_back (FFmpegSubtitleStream (stream_name (s), i));
                }
        }
  
  void
  FFmpegDecoder::setup_video ()
  {
 +      boost::mutex::scoped_lock lm (_mutex);
 +      
        _video_codec_context = _format_context->streams[_video_stream]->codec;
        _video_codec = avcodec_find_decoder (_video_codec_context->codec_id);
  
  void
  FFmpegDecoder::setup_audio ()
  {
 -      if (!_audio_stream) {
 +      boost::mutex::scoped_lock lm (_mutex);
 +      
 +      if (!_ffmpeg_content->audio_stream ()) {
                return;
        }
  
 -      shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream);
 -      assert (ffa);
 -      
 -      _audio_codec_context = _format_context->streams[ffa->id()]->codec;
 +      _audio_codec_context = _format_context->streams[_ffmpeg_content->audio_stream()->id]->codec;
        _audio_codec = avcodec_find_decoder (_audio_codec_context->codec_id);
  
        if (_audio_codec == 0) {
  void
  FFmpegDecoder::setup_subtitle ()
  {
 -      if (!_subtitle_stream || _subtitle_stream->id() >= int (_format_context->nb_streams)) {
 +      boost::mutex::scoped_lock lm (_mutex);
 +      
 +      if (!_ffmpeg_content->subtitle_stream() || _ffmpeg_content->subtitle_stream()->id >= int (_format_context->nb_streams)) {
                return;
        }
  
 -      _subtitle_codec_context = _format_context->streams[_subtitle_stream->id()]->codec;
 +      _subtitle_codec_context = _format_context->streams[_ffmpeg_content->subtitle_stream()->id]->codec;
        _subtitle_codec = avcodec_find_decoder (_subtitle_codec_context->codec_id);
  
        if (_subtitle_codec == 0) {
@@@ -233,32 -224,34 +228,32 @@@ FFmpegDecoder::pass (
                        av_strerror (r, buf, sizeof(buf));
                        _film->log()->log (String::compose (N_("error on av_read_frame (%1) (%2)"), buf, r));
                }
-               
                /* Get any remaining frames */
                
                _packet.data = 0;
                _packet.size = 0;
+               
                /* XXX: should we reset _packet.data and size after each *_decode_* call? */
+               
                int frame_finished;
 -              
 -              if (_opt.decode_video) {
 +
 +              if (_decode_video) {
                        while (avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
-                               filter_and_emit_video (_frame);
+                               filter_and_emit_video ();
                        }
                }
 -              
 -              if (_audio_stream && _opt.decode_audio) {
 +
 +              if (_ffmpeg_content->audio_stream() && _decode_audio) {
                        decode_audio_packet ();
                }
+                       
                return true;
        }
  
        avcodec_get_frame_defaults (_frame);
  
 -      shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream);
 -
 -      if (_packet.stream_index == _video_stream && _opt.decode_video) {
 +      if (_packet.stream_index == _video_stream && _decode_video) {
  
                int frame_finished;
                int const r = avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet);
                                _film->log()->log (String::compose (N_("Used only %1 bytes of %2 in packet"), r, _packet.size));
                        }
  
-                       if (_video_sync) {
-                               out_with_sync ();
-                       } else {
-                               filter_and_emit_video (_frame);
-                       }
+                       filter_and_emit_video ();
                }
  
 -      } else if (ffa && _packet.stream_index == ffa->id() && _opt.decode_audio) {
 +      } else if (_ffmpeg_content->audio_stream() && _packet.stream_index == _ffmpeg_content->audio_stream()->id && _decode_audio) {
                decode_audio_packet ();
-       } else if (_ffmpeg_content->subtitle_stream() && _packet.stream_index == _ffmpeg_content->subtitle_stream()->id && _decode_subtitles && _first_video) {
 -      } else if (_subtitle_stream && _packet.stream_index == _subtitle_stream->id() && _opt.decode_subtitles) {
++      } else if (_ffmpeg_content->subtitle_stream() && _packet.stream_index == _ffmpeg_content->subtitle_stream()->id && _decode_subtitles) {
  
                int got_subtitle;
                AVSubtitle sub;
  shared_ptr<AudioBuffers>
  FFmpegDecoder::deinterleave_audio (uint8_t** data, int size)
  {
 -      assert (_film->audio_channels());
 +      assert (_ffmpeg_content->audio_channels());
        assert (bytes_per_audio_sample());
  
 -      shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream);
 -      assert (ffa);
 -      
        /* Deinterleave and convert to float */
  
 -      assert ((size % (bytes_per_audio_sample() * ffa->channels())) == 0);
 +      assert ((size % (bytes_per_audio_sample() * _ffmpeg_content->audio_channels())) == 0);
  
        int const total_samples = size / bytes_per_audio_sample();
 -      int const frames = total_samples / _film->audio_channels();
 -      shared_ptr<AudioBuffers> audio (new AudioBuffers (ffa->channels(), frames));
 +      int const frames = total_samples / _ffmpeg_content->audio_channels();
 +      shared_ptr<AudioBuffers> audio (new AudioBuffers (_ffmpeg_content->audio_channels(), frames));
  
        switch (audio_sample_format()) {
        case AV_SAMPLE_FMT_S16:
                        audio->data(channel)[sample] = float(*p++) / (1 << 15);
  
                        ++channel;
 -                      if (channel == _film->audio_channels()) {
 +                      if (channel == _ffmpeg_content->audio_channels()) {
                                channel = 0;
                                ++sample;
                        }
        case AV_SAMPLE_FMT_S16P:
        {
                int16_t** p = reinterpret_cast<int16_t **> (data);
 -              for (int i = 0; i < _film->audio_channels(); ++i) {
 +              for (int i = 0; i < _ffmpeg_content->audio_channels(); ++i) {
                        for (int j = 0; j < frames; ++j) {
                                audio->data(i)[j] = static_cast<float>(p[i][j]) / (1 << 15);
                        }
                        audio->data(channel)[sample] = static_cast<float>(*p++) / (1 << 31);
  
                        ++channel;
 -                      if (channel == _film->audio_channels()) {
 +                      if (channel == _ffmpeg_content->audio_channels()) {
                                channel = 0;
                                ++sample;
                        }
                        audio->data(channel)[sample] = *p++;
  
                        ++channel;
 -                      if (channel == _film->audio_channels()) {
 +                      if (channel == _ffmpeg_content->audio_channels()) {
                                channel = 0;
                                ++sample;
                        }
        case AV_SAMPLE_FMT_FLTP:
        {
                float** p = reinterpret_cast<float**> (data);
 -              for (int i = 0; i < _film->audio_channels(); ++i) {
 +              for (int i = 0; i < _ffmpeg_content->audio_channels(); ++i) {
                        memcpy (audio->data(i), p[i], frames * sizeof(float));
                }
        }
  }
  
  float
 -FFmpegDecoder::frames_per_second () const
 +FFmpegDecoder::video_frame_rate () const
  {
        AVStream* s = _format_context->streams[_video_stream];
  
@@@ -461,18 -453,20 +452,20 @@@ strin
  FFmpegDecoder::stream_name (AVStream* s) const
  {
        stringstream n;
-       
-       AVDictionaryEntry const * lang = av_dict_get (s->metadata, N_("language"), 0, 0);
-       if (lang) {
-               n << lang->value;
-       }
-       
-       AVDictionaryEntry const * title = av_dict_get (s->metadata, N_("title"), 0, 0);
-       if (title) {
-               if (!n.str().empty()) {
-                       n << N_(" ");
+       if (s->metadata) {
+               AVDictionaryEntry const * lang = av_dict_get (s->metadata, N_("language"), 0, 0);
+               if (lang) {
+                       n << lang->value;
+               }
+               
+               AVDictionaryEntry const * title = av_dict_get (s->metadata, N_("title"), 0, 0);
+               if (title) {
+                       if (!n.str().empty()) {
+                               n << N_(" ");
+                       }
+                       n << title->value;
                }
-               n << title->value;
        }
  
        if (n.str().empty()) {
@@@ -488,100 -482,172 +481,95 @@@ FFmpegDecoder::bytes_per_audio_sample (
        return av_get_bytes_per_sample (audio_sample_format ());
  }
  
 -void
 -FFmpegDecoder::set_audio_stream (shared_ptr<AudioStream> s)
 -{
 -      AudioDecoder::set_audio_stream (s);
 -      setup_audio ();
 -}
 -
 -void
 -FFmpegDecoder::set_subtitle_stream (shared_ptr<SubtitleStream> s)
 -{
 -      VideoDecoder::set_subtitle_stream (s);
 -      setup_subtitle ();
 -      OutputChanged ();
 -}
 -
  void
- FFmpegDecoder::filter_and_emit_video (AVFrame* frame)
+ FFmpegDecoder::filter_and_emit_video ()
  {
        boost::mutex::scoped_lock lm (_filter_graphs_mutex);
        
        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 (libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)) {
                ++i;
        }
  
        if (i == _filter_graphs.end ()) {
-               graph.reset (new FilterGraph (_film, this, libdcp::Size (frame->width, frame->height), (AVPixelFormat) frame->format));
+               graph.reset (new FilterGraph (_film, this, libdcp::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));
+               _film->log()->log (String::compose (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format));
        } else {
                graph = *i;
        }
  
-       list<shared_ptr<Image> > images = graph->process (frame);
+       list<shared_ptr<Image> > images = graph->process (_frame);
  
        for (list<shared_ptr<Image> >::iterator i = images.begin(); i != images.end(); ++i) {
-               emit_video (*i, frame_time ());
+               int64_t const bet = av_frame_get_best_effort_timestamp (_frame);
+               if (bet != AV_NOPTS_VALUE) {
+                       emit_video (*i, false, bet * av_q2d (_format_context->streams[_video_stream]->time_base));
+               } else {
+                       _film->log()->log ("Dropping frame without PTS");
+               }
        }
  }
  
  bool
  FFmpegDecoder::seek (double p)
  {
-       /* This use of AVSEEK_FLAG_BACKWARD is a bit of a hack; without it, if we ask for a seek to the same place as last time
-          (used when we change decoder parameters and want to re-fetch the frame) we end up going forwards rather than
-          staying in the same place.
-       */
-       bool const backwards = (p == last_content_time());
-       
+       return do_seek (p, false, false);
+ }
 -bool
 -FFmpegDecoder::seek_to_last ()
 -{
 -      /* This AVSEEK_FLAG_BACKWARD in do_seek is a bit of a hack; without it, if we ask for a seek to the same place as last time
 -         (used when we change decoder parameters and want to re-fetch the frame) we end up going forwards rather than
 -         staying in the same place.
 -      */
 -      return do_seek (last_source_time(), true, false);
 -}
 -
+ void
+ FFmpegDecoder::seek_back ()
+ {
 -      do_seek (last_source_time() - 2.5 / frames_per_second (), true, true);
++      do_seek (last_content_time() - 2.5 / video_frame_rate(), true, true);
+ }
+ void
+ FFmpegDecoder::seek_forward ()
+ {
 -      do_seek (last_source_time() - 0.5 / frames_per_second(), true, true);
++      do_seek (last_content_time() - 0.5 / video_frame_rate(), true, true);
+ }
+ bool
+ FFmpegDecoder::do_seek (double p, bool backwards, bool accurate)
+ {
        int64_t const vt = p / av_q2d (_format_context->streams[_video_stream]->time_base);
  
        int const r = av_seek_frame (_format_context, _video_stream, vt, backwards ? AVSEEK_FLAG_BACKWARD : 0);
-       
        avcodec_flush_buffers (_video_codec_context);
        if (_subtitle_codec_context) {
                avcodec_flush_buffers (_subtitle_codec_context);
        }
-       
-       return r < 0;
- }
  
- void
- FFmpegDecoder::out_with_sync ()
- {
-       /* Where we are in the output, in seconds */
-       double const out_pts_seconds = video_frame() / video_frame_rate();
-       
-       /* Where we are in the source, in seconds */
-       double const source_pts_seconds = av_q2d (_format_context->streams[_packet.stream_index]->time_base)
-               * av_frame_get_best_effort_timestamp(_frame);
-       
-       _film->log()->log (
-               String::compose (N_("Source video frame ready; source at %1, output at %2"), source_pts_seconds, out_pts_seconds),
-               Log::VERBOSE
-               );
-       
-       if (!_first_video) {
-               _first_video = source_pts_seconds;
-       }
-       
-       /* Difference between where we are and where we should be */
-       double const delta = source_pts_seconds - _first_video.get() - out_pts_seconds;
-       double const one_frame = 1 / video_frame_rate();
-       
-       /* Insert frames if required to get out_pts_seconds up to pts_seconds */
-       if (delta > one_frame) {
-               int const extra = rint (delta / one_frame);
-               for (int i = 0; i < extra; ++i) {
-                       repeat_last_video (frame_time ());
-                       _film->log()->log (
-                               String::compose (
-                                       N_("Extra video frame inserted at %1s; source frame %2, source PTS %3 (at %4 fps)"),
-                                       out_pts_seconds, video_frame(), source_pts_seconds, video_frame_rate()
-                                       )
-                               );
+       if (accurate) {
+               while (1) {
+                       int r = av_read_frame (_format_context, &_packet);
+                       if (r < 0) {
+                               return true;
+                       }
+                       
+                       avcodec_get_frame_defaults (_frame);
+                       
+                       if (_packet.stream_index == _video_stream) {
+                               int finished = 0;
+                               int const r = avcodec_decode_video2 (_video_codec_context, _frame, &finished, &_packet);
+                               if (r >= 0 && finished) {
+                                       int64_t const bet = av_frame_get_best_effort_timestamp (_frame);
+                                       if (bet > vt) {
+                                               break;
+                                       }
+                               }
+                       }
+                       
+                       av_free_packet (&_packet);
                }
        }
-       
-       if (delta > -one_frame) {
-               /* Process this frame */
-               filter_and_emit_video (_frame);
-       } else {
-               /* Otherwise we are omitting a frame to keep things right */
-               _film->log()->log (String::compose (N_("Frame removed at %1s"), out_pts_seconds));
-       }
+               
+       return r < 0;
  }
  
 -shared_ptr<FFmpegAudioStream>
 -FFmpegAudioStream::create (string t, optional<int> v)
 -{
 -      if (!v) {
 -              /* version < 1; no type in the string, and there's only FFmpeg streams anyway */
 -              return shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (t, v));
 -      }
 -
 -      stringstream s (t);
 -      string type;
 -      s >> type;
 -      if (type != N_("ffmpeg")) {
 -              return shared_ptr<FFmpegAudioStream> ();
 -      }
 -
 -      return shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (t, v));
 -}
 -
 -FFmpegAudioStream::FFmpegAudioStream (string t, optional<int> version)
 -{
 -      stringstream n (t);
 -      
 -      int name_index = 4;
 -      if (!version) {
 -              name_index = 2;
 -              int channels;
 -              n >> _id >> channels;
 -              _channel_layout = av_get_default_channel_layout (channels);
 -              _sample_rate = 0;
 -      } else {
 -              string type;
 -              /* Current (marked version 1) */
 -              n >> type >> _id >> _sample_rate >> _channel_layout;
 -              assert (type == N_("ffmpeg"));
 -      }
 -
 -      for (int i = 0; i < name_index; ++i) {
 -              size_t const s = t.find (' ');
 -              if (s != string::npos) {
 -                      t = t.substr (s + 1);
 -              }
 -      }
 -
 -      _name = t;
 -}
 -
 -string
 -FFmpegAudioStream::to_string () const
 -{
 -      return String::compose (N_("ffmpeg %1 %2 %3 %4"), _id, _sample_rate, _channel_layout, _name);
 -}
 -
  void
  FFmpegDecoder::film_changed (Film::Property p)
  {
                boost::mutex::scoped_lock lm (_filter_graphs_mutex);
                _filter_graphs.clear ();
        }
 -      OutputChanged ();
        break;
  
        default:
  }
  
  /** @return Length (in video frames) according to our content's header */
 -SourceFrame
 -FFmpegDecoder::length () const
 +ContentVideoFrame
 +FFmpegDecoder::video_length () const
  {
 -      return (double(_format_context->duration) / AV_TIME_BASE) * frames_per_second();
 +      return (double(_format_context->duration) / AV_TIME_BASE) * video_frame_rate();
  }
  
- double
- FFmpegDecoder::frame_time () const
- {
-       return av_frame_get_best_effort_timestamp(_frame) * av_q2d (_format_context->streams[_video_stream]->time_base);
- }
  void
  FFmpegDecoder::decode_audio_packet ()
  {
 -      shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream);
 -      assert (ffa);
 -
        /* Audio packets can contain multiple frames, so we may have to call avcodec_decode_audio4
           several times.
        */
  
                int frame_finished;
                int const decode_result = avcodec_decode_audio4 (_audio_codec_context, _frame, &frame_finished, &copy_packet);
-               if (decode_result >= 0 && frame_finished) {
-                       
-                       /* Where we are in the source, in seconds */
-                       double const source_pts_seconds = av_q2d (_format_context->streams[copy_packet.stream_index]->time_base)
-                               * av_frame_get_best_effort_timestamp(_frame);
-                       
-                       /* We only decode audio if we've had our first video packet through, and if it
-                          was before this packet.  Until then audio is thrown away.
-                       */
+               if (decode_result >= 0) {
+                       if (frame_finished) {
                        
-                       if ((_first_video && _first_video.get() <= source_pts_seconds) || !_decode_video) {
-                               
-                               if (!_first_audio && _decode_video) {
-                                       _first_audio = source_pts_seconds;
-                                       
-                                       /* This is our first audio frame, and if we've arrived here we must have had our
-                                          first video frame.  Push some silence to make up any gap between our first
-                                          video frame and our first audio.
-                                       */
-                                       
-                                       /* frames of silence that we must push */
-                                       int const s = rint ((_first_audio.get() - _first_video.get()) * _ffmpeg_content->audio_frame_rate ());
-                                       
-                                       _film->log()->log (
-                                               String::compose (
-                                                       N_("First video at %1, first audio at %2, pushing %3 audio frames of silence for %4 channels (%5 bytes per sample)"),
-                                                       _first_video.get(), _first_audio.get(), s, _ffmpeg_content->audio_channels(), bytes_per_audio_sample()
-                                                       )
-                                               );
-                                       
-                                       if (s) {
-                                               shared_ptr<AudioBuffers> audio (new AudioBuffers (_ffmpeg_content->audio_channels(), s));
-                                               audio->make_silent ();
-                                               Audio (audio);
-                                       }
-                               }
+                               /* Where we are in the source, in seconds */
+                               double const source_pts_seconds = av_q2d (_format_context->streams[copy_packet.stream_index]->time_base)
+                                       * av_frame_get_best_effort_timestamp(_frame);
                                
                                int const data_size = av_samples_get_buffer_size (
                                        0, _audio_codec_context->channels, _frame->nb_samples, audio_sample_format (), 1
                                        );
                                
 -                              assert (_audio_codec_context->channels == _film->audio_channels());
 +                              assert (_audio_codec_context->channels == _ffmpeg_content->audio_channels());
-                               Audio (deinterleave_audio (_frame->data, data_size));
+                               Audio (deinterleave_audio (_frame->data, data_size), source_pts_seconds);
                        }
-               }
-               if (decode_result >= 0) {
+                       
                        copy_packet.data += decode_result;
                        copy_packet.size -= decode_result;
                }
diff --combined src/lib/ffmpeg_decoder.h
index f6a53874ae723871d4d6dba390d57dccab047597,0c89b973dfbb9b47c237cd2e9ade391327758fd9..174cc39954b9fccc92153ea249cfc421e4458e80
@@@ -36,7 -36,6 +36,7 @@@ extern "C" 
  #include "video_decoder.h"
  #include "audio_decoder.h"
  #include "film.h"
 +#include "ffmpeg_content.h"
  
  struct AVFilterGraph;
  struct AVCodecContext;
@@@ -51,47 -50,70 +51,48 @@@ class Options
  class Image;
  class Log;
  
 -class FFmpegAudioStream : public AudioStream
 -{
 -public:
 -      FFmpegAudioStream (std::string n, int i, int s, int64_t c)
 -              : AudioStream (s, c)
 -              , _name (n)
 -              , _id (i)
 -      {}
 -
 -      std::string to_string () const;
 -
 -      std::string name () const {
 -              return _name;
 -      }
 -
 -      int id () const {
 -              return _id;
 -      }
 -
 -      static boost::shared_ptr<FFmpegAudioStream> create (std::string t, boost::optional<int> v);
 -
 -private:
 -      friend class stream_test;
 -      
 -      FFmpegAudioStream (std::string t, boost::optional<int> v);
 -      
 -      std::string _name;
 -      int _id;
 -};
 -
  /** @class FFmpegDecoder
   *  @brief A decoder using FFmpeg to decode content.
   */
  class FFmpegDecoder : public VideoDecoder, public AudioDecoder
  {
  public:
-       FFmpegDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const FFmpegContent>, bool video, bool audio, bool subtitles, bool video_sync);
 -      FFmpegDecoder (boost::shared_ptr<Film>, DecodeOptions);
++      FFmpegDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const FFmpegContent>, bool video, bool audio, bool subtitles);
        ~FFmpegDecoder ();
  
 -      float frames_per_second () const;
 +      float video_frame_rate () const;
        libdcp::Size native_size () const;
 -      SourceFrame length () const;
 +      ContentVideoFrame video_length () const;
        int time_base_numerator () const;
        int time_base_denominator () const;
        int sample_aspect_ratio_numerator () const;
        int sample_aspect_ratio_denominator () const;
  
 -      void set_audio_stream (boost::shared_ptr<AudioStream>);
 -      void set_subtitle_stream (boost::shared_ptr<SubtitleStream>);
 +      std::vector<FFmpegSubtitleStream> subtitle_streams () const {
 +              return _subtitle_streams;
 +      }
 +      
 +      std::vector<FFmpegAudioStream> audio_streams () const {
 +              return _audio_streams;
 +      }
  
        bool seek (double);
 -      bool seek_to_last ();
+       void seek_forward ();
+       void seek_back ();
 +      bool pass ();
  
  private:
  
 -      bool pass ();
 -      bool do_seek (double p, bool, bool);
 +      /* No copy construction */
 +      FFmpegDecoder (FFmpegDecoder const &);
 +      FFmpegDecoder& operator= (FFmpegDecoder const &);
 +
        PixelFormat pixel_format () const;
        AVSampleFormat audio_sample_format () const;
        int bytes_per_audio_sample () const;
++      bool do_seek (double, bool, bool);
  
-       void out_with_sync ();
-       void filter_and_emit_video (AVFrame *);
-       double frame_time () const;
+       void filter_and_emit_video ();
  
        void setup_general ();
        void setup_video ();
  
        std::string stream_name (AVStream* s) const;
  
 +      boost::shared_ptr<const FFmpegContent> _ffmpeg_content;
 +
        AVFormatContext* _format_context;
        int _video_stream;
        
  
        AVPacket _packet;
  
-       boost::optional<double> _first_video;
-       boost::optional<double> _first_audio;
        std::list<boost::shared_ptr<FilterGraph> > _filter_graphs;
        boost::mutex _filter_graphs_mutex;
-       bool _video_sync;
 +
 +        std::vector<FFmpegSubtitleStream> _subtitle_streams;
 +        std::vector<FFmpegAudioStream> _audio_streams;
 +
 +      bool _decode_video;
 +      bool _decode_audio;
 +      bool _decode_subtitles;
 +
 +      /* It would appear (though not completely verified) that one must have
 +         a mutex around calls to avcodec_open* and avcodec_close... and here
 +         it is.
 +      */
 +      static boost::mutex _mutex;
  };
diff --combined src/lib/film.cc
index c8aee7a0aa6bdf6bc6b925209a6ae92043f72098,b0785df34aeba5e1eb7b203dc124b6f031637068..361031fc87d9e6b8309506bb78b8d95216336bd9
  #include <boost/algorithm/string.hpp>
  #include <boost/lexical_cast.hpp>
  #include <boost/date_time.hpp>
 +#include <libxml++/libxml++.h>
 +#include <libcxml/cxml.h>
  #include "film.h"
  #include "format.h"
  #include "job.h"
  #include "filter.h"
 -#include "transcoder.h"
  #include "util.h"
  #include "job_manager.h"
  #include "ab_transcode_job.h"
  #include "transcode_job.h"
  #include "scp_dcp_job.h"
  #include "log.h"
 -#include "options.h"
  #include "exceptions.h"
  #include "examine_content_job.h"
  #include "scaler.h"
 -#include "decoder_factory.h"
  #include "config.h"
  #include "version.h"
  #include "ui_signaller.h"
 -#include "video_decoder.h"
 -#include "audio_decoder.h"
 -#include "sndfile_decoder.h"
  #include "analyse_audio_job.h"
 +#include "playlist.h"
 +#include "player.h"
 +#include "ffmpeg_content.h"
 +#include "imagemagick_content.h"
 +#include "sndfile_content.h"
 +#include "dcp_content_type.h"
  
  #include "i18n.h"
  
@@@ -69,8 -67,6 +69,8 @@@ using std::setfill
  using std::min;
  using std::make_pair;
  using std::endl;
 +using std::cout;
 +using std::list;
  using boost::shared_ptr;
  using boost::lexical_cast;
  using boost::to_upper_copy;
@@@ -90,18 -86,19 +90,18 @@@ int const Film::state_version = 4
   */
  
  Film::Film (string d, bool must_exist)
 -      : _use_dci_name (true)
 -      , _trust_content_header (true)
 +      : _playlist (new Playlist)
 +      , _use_dci_name (true)
 +      , _trust_content_headers (true)
        , _dcp_content_type (0)
 -      , _format (0)
 +      , _format (Format::from_id ("185"))
        , _scaler (Scaler::from_id ("bicubic"))
        , _trim_start (0)
        , _trim_end (0)
        , _trim_type (CPL)
 -      , _dcp_ab (false)
 -      , _use_content_audio (true)
 +      , _ab (false)
        , _audio_gain (0)
        , _audio_delay (0)
 -      , _still_duration (10)
        , _with_subtitles (false)
        , _subtitle_offset (0)
        , _subtitle_scale (1)
        , _j2k_bandwidth (200000000)
        , _dci_metadata (Config::instance()->default_dci_metadata ())
        , _dcp_frame_rate (0)
 -      , _source_frame_rate (0)
        , _dirty (false)
  {
        set_dci_date_today ();
 +
 +      _playlist->ContentChanged.connect (bind (&Film::content_changed, this, _1, _2));
        
        /* Make state.directory a complete path without ..s (where possible)
           (Code swiped from Adam Bowen on stackoverflow)
                }
        }
  
 -      _sndfile_stream = SndfileStream::create ();
 -      
        if (must_exist) {
                read_metadata ();
+       } else {
+               write_metadata ();
        }
  
        _log.reset (new FileLog (file ("log")));
@@@ -154,11 -154,11 +156,11 @@@ Film::Film (Film const & o
        : boost::enable_shared_from_this<Film> (o)
        /* note: the copied film shares the original's log */
        , _log               (o._log)
 +      , _playlist          (new Playlist)
        , _directory         (o._directory)
        , _name              (o._name)
        , _use_dci_name      (o._use_dci_name)
 -      , _content           (o._content)
 -      , _trust_content_header (o._trust_content_header)
 +      , _trust_content_headers (o._trust_content_headers)
        , _dcp_content_type  (o._dcp_content_type)
        , _format            (o._format)
        , _crop              (o._crop)
        , _trim_start        (o._trim_start)
        , _trim_end          (o._trim_end)
        , _trim_type         (o._trim_type)
 -      , _dcp_ab            (o._dcp_ab)
 -      , _content_audio_stream (o._content_audio_stream)
 -      , _external_audio    (o._external_audio)
 -      , _use_content_audio (o._use_content_audio)
 +      , _ab                (o._ab)
        , _audio_gain        (o._audio_gain)
        , _audio_delay       (o._audio_delay)
 -      , _still_duration    (o._still_duration)
 -      , _subtitle_stream   (o._subtitle_stream)
        , _with_subtitles    (o._with_subtitles)
        , _subtitle_offset   (o._subtitle_offset)
        , _subtitle_scale    (o._subtitle_scale)
        , _colour_lut        (o._colour_lut)
        , _j2k_bandwidth     (o._j2k_bandwidth)
        , _dci_metadata      (o._dci_metadata)
 -      , _dci_date          (o._dci_date)
        , _dcp_frame_rate    (o._dcp_frame_rate)
 -      , _size              (o._size)
 -      , _length            (o._length)
 -      , _content_digest    (o._content_digest)
 -      , _content_audio_streams (o._content_audio_streams)
 -      , _sndfile_stream    (o._sndfile_stream)
 -      , _subtitle_streams  (o._subtitle_streams)
 -      , _source_frame_rate (o._source_frame_rate)
 +      , _dci_date          (o._dci_date)
        , _dirty             (o._dirty)
  {
 +      for (ContentList::const_iterator i = o._content.begin(); i != o._content.end(); ++i) {
 +              _content.push_back ((*i)->clone ());
 +      }
        
 -}
 -
 -Film::~Film ()
 -{
 -
 +      _playlist->ContentChanged.connect (bind (&Film::content_changed, this, _1, _2));
 +      
 +      _playlist->setup (_content);
  }
  
  string
  Film::video_state_identifier () const
  {
        assert (format ());
+       LocaleGuard lg;
  
        pair<string, string> f = Filter::ffmpeg_strings (filters());
  
        stringstream s;
        s << format()->id()
 -        << "_" << content_digest()
 +        << "_" << _playlist->video_digest()
          << "_" << crop().left << "_" << crop().right << "_" << crop().top << "_" << crop().bottom
          << "_" << _dcp_frame_rate
          << "_" << f.first << "_" << f.second
          << "_" << j2k_bandwidth()
          << "_" << boost::lexical_cast<int> (colour_lut());
  
 -      if (dcp_ab()) {
 +      if (ab()) {
                pair<string, string> fa = Filter::ffmpeg_strings (Config::instance()->reference_filters());
                s << "ab_" << Config::instance()->reference_scaler()->id() << "_" << fa.first << "_" << fa.second;
        }
@@@ -270,7 -282,7 +273,7 @@@ Film::audio_analysis_path () cons
  {
        boost::filesystem::path p;
        p /= "analysis";
 -      p /= content_digest();
 +      p /= _playlist->audio_digest();
        return file (p.string ());
  }
  
@@@ -284,7 -296,7 +287,7 @@@ Film::make_dcp (
                throw BadSettingError (_("name"), _("cannot contain slashes"));
        }
        
 -      log()->log (String::compose ("DVD-o-matic %1 git %2 using %3", dvdomatic_version, dvdomatic_git_commit, dependency_version_summary()));
 +      log()->log (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary()));
  
        {
                char buffer[128];
                log()->log (String::compose ("Starting to make DCP on %1", buffer));
        }
        
 -      log()->log (String::compose ("Content is %1; type %2", content_path(), (content_type() == STILL ? _("still") : _("video"))));
 -      if (length()) {
 -              log()->log (String::compose ("Content length %1", length().get()));
 -      }
 -      log()->log (String::compose ("Content digest %1", content_digest()));
 -      log()->log (String::compose ("Content at %1 fps, DCP at %2 fps", source_frame_rate(), dcp_frame_rate()));
 +//    log()->log (String::compose ("Content is %1; type %2", content_path(), (content_type() == STILL ? _("still") : _("video"))));
 +//    if (length()) {
 +//            log()->log (String::compose ("Content length %1", length().get()));
 +//    }
 +//    log()->log (String::compose ("Content digest %1", content_digest()));
 +//    log()->log (String::compose ("Content at %1 fps, DCP at %2 fps", source_frame_rate(), dcp_frame_rate()));
        log()->log (String::compose ("%1 threads", Config::instance()->num_local_encoding_threads()));
        log()->log (String::compose ("J2K bandwidth %1", j2k_bandwidth()));
 -#ifdef DVDOMATIC_DEBUG
 -      log()->log ("DVD-o-matic built in debug mode.");
 +#ifdef DCPOMATIC_DEBUG
 +      log()->log ("DCP-o-matic built in debug mode.");
  #else
 -      log()->log ("DVD-o-matic built in optimised mode.");
 +      log()->log ("DCP-o-matic built in optimised mode.");
  #endif
  #ifdef LIBDCP_DEBUG
        log()->log ("libdcp built in debug mode.");
                throw MissingSettingError (_("name"));
        }
  
 -      DecodeOptions od;
 -      od.decode_subtitles = with_subtitles ();
 -
        shared_ptr<Job> r;
  
 -      if (dcp_ab()) {
 -              r = JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (shared_from_this(), od)));
 +      if (ab()) {
 +              r = JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (shared_from_this())));
        } else {
 -              r = JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this(), od)));
 +              r = JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this())));
        }
  }
  
 -/** Start a job to analyse the audio of our content file */
 +/** Start a job to analyse the audio in our Playlist */
  void
  Film::analyse_audio ()
  {
        JobManager::instance()->add (_analyse_audio_job);
  }
  
 -/** Start a job to examine our content file */
 +/** Start a job to examine a piece of content */
  void
 -Film::examine_content ()
 +Film::examine_content (shared_ptr<Content> c)
  {
 -      if (_examine_content_job) {
 -              return;
 -      }
 -
 -      _examine_content_job.reset (new ExamineContentJob (shared_from_this()));
 -      _examine_content_job->Finished.connect (bind (&Film::examine_content_finished, this));
 -      JobManager::instance()->add (_examine_content_job);
 +      shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c, trust_content_headers ()));
 +      JobManager::instance()->add (j);
  }
  
  void
@@@ -371,6 -391,12 +374,6 @@@ Film::analyse_audio_finished (
        _analyse_audio_job.reset ();
  }
  
 -void
 -Film::examine_content_finished ()
 -{
 -      _examine_content_job.reset ();
 -}
 -
  /** Start a job to send our DCP to the configured TMS */
  void
  Film::send_dcp_to_tms ()
@@@ -402,66 -428,86 +405,67 @@@ Film::encoded_frames () cons
  void
  Film::write_metadata () const
  {
 +      ContentList the_content = content ();
 +      
        boost::mutex::scoped_lock lm (_state_mutex);
+       LocaleGuard lg;
  
        boost::filesystem::create_directories (directory());
  
 -      string const m = file ("metadata");
 -      ofstream f (m.c_str ());
 -      if (!f.good ()) {
 -              throw CreateFileError (m);
 -      }
 +      xmlpp::Document doc;
 +      xmlpp::Element* root = doc.create_root_node ("Metadata");
  
 -      f << "version " << state_version << endl;
 +      root->add_child("Version")->add_child_text (boost::lexical_cast<string> (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("TrustContentHeaders")->add_child_text (_trust_content_headers ? "1" : "0");
  
 -      /* User stuff */
 -      f << "name " << _name << endl;
 -      f << "use_dci_name " << _use_dci_name << endl;
 -      f << "content " << _content << endl;
 -      f << "trust_content_header " << (_trust_content_header ? "1" : "0") << endl;
        if (_dcp_content_type) {
 -              f << "dcp_content_type " << _dcp_content_type->dci_name () << endl;
 +              root->add_child("DCPContentType")->add_child_text (_dcp_content_type->dci_name ());
        }
 +
        if (_format) {
 -              f << "format " << _format->as_metadata () << endl;
 -      }
 -      f << "left_crop " << _crop.left << endl;
 -      f << "right_crop " << _crop.right << endl;
 -      f << "top_crop " << _crop.top << endl;
 -      f << "bottom_crop " << _crop.bottom << endl;
 -      for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) {
 -              f << "filter " << (*i)->id () << endl;
 +              root->add_child("Format")->add_child_text (_format->id ());
        }
 -      f << "scaler " << _scaler->id () << endl;
 -      f << "trim_start " << _trim_start << endl;
 -      f << "trim_end " << _trim_end << endl;
 +
        switch (_trim_type) {
        case CPL:
 -              f << "trim_type cpl\n";
 +              root->add_child("TrimType")->add_child_text ("CPL");
                break;
        case ENCODE:
 -              f << "trim_type encode\n";
 -              break;
 -      }
 -      f << "dcp_ab " << (_dcp_ab ? "1" : "0") << endl;
 -      if (_content_audio_stream) {
 -              f << "selected_content_audio_stream " << _content_audio_stream->to_string() << endl;
 +              root->add_child("TrimType")->add_child_text ("Encode");
        }
 -      for (vector<string>::const_iterator i = _external_audio.begin(); i != _external_audio.end(); ++i) {
 -              f << "external_audio " << *i << endl;
 -      }
 -      f << "use_content_audio " << (_use_content_audio ? "1" : "0") << endl;
 -      f << "audio_gain " << _audio_gain << endl;
 -      f << "audio_delay " << _audio_delay << endl;
 -      f << "still_duration " << _still_duration << endl;
 -      if (_subtitle_stream) {
 -              f << "selected_subtitle_stream " << _subtitle_stream->to_string() << endl;
 -      }
 -      f << "with_subtitles " << _with_subtitles << endl;
 -      f << "subtitle_offset " << _subtitle_offset << endl;
 -      f << "subtitle_scale " << _subtitle_scale << endl;
 -      f << "colour_lut " << _colour_lut << endl;
 -      f << "j2k_bandwidth " << _j2k_bandwidth << endl;
 -      _dci_metadata.write (f);
 -      f << "dci_date " << boost::gregorian::to_iso_string (_dci_date) << endl;
 -      f << "dcp_frame_rate " << _dcp_frame_rate << endl;
 -      f << "width " << _size.width << endl;
 -      f << "height " << _size.height << endl;
 -      f << "length " << _length.get_value_or(0) << endl;
 -      f << "content_digest " << _content_digest << endl;
 -
 -      for (vector<shared_ptr<AudioStream> >::const_iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) {
 -              f << "content_audio_stream " << (*i)->to_string () << endl;
 -      }
 -
 -      f << "external_audio_stream " << _sndfile_stream->to_string() << endl;
 +                      
 +      root->add_child("LeftCrop")->add_child_text (boost::lexical_cast<string> (_crop.left));
 +      root->add_child("RightCrop")->add_child_text (boost::lexical_cast<string> (_crop.right));
 +      root->add_child("TopCrop")->add_child_text (boost::lexical_cast<string> (_crop.top));
 +      root->add_child("BottomCrop")->add_child_text (boost::lexical_cast<string> (_crop.bottom));
  
 -      for (vector<shared_ptr<SubtitleStream> >::const_iterator i = _subtitle_streams.begin(); i != _subtitle_streams.end(); ++i) {
 -              f << "subtitle_stream " << (*i)->to_string () << endl;
 +      for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) {
 +              root->add_child("Filter")->add_child_text ((*i)->id ());
        }
 -
 -      f << "source_frame_rate " << _source_frame_rate << endl;
 +      
 +      root->add_child("Scaler")->add_child_text (_scaler->id ());
 +      root->add_child("TrimStart")->add_child_text (boost::lexical_cast<string> (_trim_start));
 +      root->add_child("TrimEnd")->add_child_text (boost::lexical_cast<string> (_trim_end));
 +      root->add_child("AB")->add_child_text (_ab ? "1" : "0");
 +      root->add_child("AudioGain")->add_child_text (boost::lexical_cast<string> (_audio_gain));
 +      root->add_child("AudioDelay")->add_child_text (boost::lexical_cast<string> (_audio_delay));
 +      root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0");
 +      root->add_child("SubtitleOffset")->add_child_text (boost::lexical_cast<string> (_subtitle_offset));
 +      root->add_child("SubtitleScale")->add_child_text (boost::lexical_cast<string> (_subtitle_scale));
 +      root->add_child("ColourLUT")->add_child_text (boost::lexical_cast<string> (_colour_lut));
 +      root->add_child("J2KBandwidth")->add_child_text (boost::lexical_cast<string> (_j2k_bandwidth));
 +      _dci_metadata.as_xml (root->add_child ("DCIMetadata"));
 +      root->add_child("DCPFrameRate")->add_child_text (boost::lexical_cast<string> (_dcp_frame_rate));
 +      root->add_child("DCIDate")->add_child_text (boost::gregorian::to_iso_string (_dci_date));
 +      _audio_mapping.as_xml (root->add_child("AudioMapping"));
 +
 +      for (ContentList::iterator i = the_content.begin(); i != the_content.end(); ++i) {
 +              (*i)->as_xml (root->add_child ("Content"));
 +      }
 +
 +      doc.write_to_file_formatted (file ("metadata.xml"));
        
        _dirty = false;
  }
@@@ -471,90 -517,168 +475,91 @@@ voi
  Film::read_metadata ()
  {
        boost::mutex::scoped_lock lm (_state_mutex);
+       LocaleGuard lg;
  
 -      _external_audio.clear ();
 -      _content_audio_streams.clear ();
 -      _subtitle_streams.clear ();
 -
 -      boost::optional<int> version;
 -
 -      /* Backward compatibility things */
 -      boost::optional<int> audio_sample_rate;
 -      boost::optional<int> audio_stream_index;
 -      boost::optional<int> subtitle_stream_index;
 -
 -      ifstream f (file ("metadata").c_str());
 -      if (!f.good()) {
 -              throw OpenFileError (file ("metadata"));
 +      if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) {
 +              throw StringError (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version.  You will need to create a new Film, re-add your content and set it up again.  Sorry!"));
        }
 -      
 -      multimap<string, string> kv = read_key_value (f);
  
 -      /* We need version before anything else */
 -      multimap<string, string>::iterator v = kv.find ("version");
 -      if (v != kv.end ()) {
 -              version = atoi (v->second.c_str());
 -      }
 +      cxml::File f (file ("metadata.xml"), "Metadata");
        
 -      for (multimap<string, string>::const_iterator i = kv.begin(); i != kv.end(); ++i) {
 -              string const k = i->first;
 -              string const v = i->second;
 +      _name = f.string_child ("Name");
 +      _use_dci_name = f.bool_child ("UseDCIName");
 +      _trust_content_headers = f.bool_child ("TrustContentHeaders");
  
 -              if (k == "audio_sample_rate") {
 -                      audio_sample_rate = atoi (v.c_str());
 +      {
 +              optional<string> c = f.optional_string_child ("DCPContentType");
 +              if (c) {
 +                      _dcp_content_type = DCPContentType::from_dci_name (c.get ());
                }
 +      }
  
 -              /* User-specified stuff */
 -              if (k == "name") {
 -                      _name = v;
 -              } else if (k == "use_dci_name") {
 -                      _use_dci_name = (v == "1");
 -              } else if (k == "content") {
 -                      _content = v;
 -              } else if (k == "trust_content_header") {
 -                      _trust_content_header = (v == "1");
 -              } else if (k == "dcp_content_type") {
 -                      if (version < 3) {
 -                              _dcp_content_type = DCPContentType::from_pretty_name (v);
 -                      } else {
 -                              _dcp_content_type = DCPContentType::from_dci_name (v);
 -                      }
 -              } else if (k == "format") {
 -                      _format = Format::from_metadata (v);
 -              } else if (k == "left_crop") {
 -                      _crop.left = atoi (v.c_str ());
 -              } else if (k == "right_crop") {
 -                      _crop.right = atoi (v.c_str ());
 -              } else if (k == "top_crop") {
 -                      _crop.top = atoi (v.c_str ());
 -              } else if (k == "bottom_crop") {
 -                      _crop.bottom = atoi (v.c_str ());
 -              } else if (k == "filter") {
 -                      _filters.push_back (Filter::from_id (v));
 -              } else if (k == "scaler") {
 -                      _scaler = Scaler::from_id (v);
 -              } else if ( ((!version || version < 2) && k == "dcp_trim_start") || k == "trim_start") {
 -                      _trim_start = atoi (v.c_str ());
 -              } else if ( ((!version || version < 2) && k == "dcp_trim_end") || k == "trim_end") {
 -                      _trim_end = atoi (v.c_str ());
 -              } else if (k == "trim_type") {
 -                      if (v == "cpl") {
 -                              _trim_type = CPL;
 -                      } else if (v == "encode") {
 -                              _trim_type = ENCODE;
 -                      }
 -              } else if (k == "dcp_ab") {
 -                      _dcp_ab = (v == "1");
 -              } else if (k == "selected_content_audio_stream" || (!version && k == "selected_audio_stream")) {
 -                      if (!version) {
 -                              audio_stream_index = atoi (v.c_str ());
 -                      } else {
 -                              _content_audio_stream = audio_stream_factory (v, version);
 -                      }
 -              } else if (k == "external_audio") {
 -                      _external_audio.push_back (v);
 -              } else if (k == "use_content_audio") {
 -                      _use_content_audio = (v == "1");
 -              } else if (k == "audio_gain") {
 -                      _audio_gain = atof (v.c_str ());
 -              } else if (k == "audio_delay") {
 -                      _audio_delay = atoi (v.c_str ());
 -              } else if (k == "still_duration") {
 -                      _still_duration = atoi (v.c_str ());
 -              } else if (k == "selected_subtitle_stream") {
 -                      if (!version) {
 -                              subtitle_stream_index = atoi (v.c_str ());
 -                      } else {
 -                              _subtitle_stream = subtitle_stream_factory (v, version);
 -                      }
 -              } else if (k == "with_subtitles") {
 -                      _with_subtitles = (v == "1");
 -              } else if (k == "subtitle_offset") {
 -                      _subtitle_offset = atoi (v.c_str ());
 -              } else if (k == "subtitle_scale") {
 -                      _subtitle_scale = atof (v.c_str ());
 -              } else if (k == "colour_lut") {
 -                      _colour_lut = atoi (v.c_str ());
 -              } else if (k == "j2k_bandwidth") {
 -                      _j2k_bandwidth = atoi (v.c_str ());
 -              } else if (k == "dci_date") {
 -                      _dci_date = boost::gregorian::from_undelimited_string (v);
 -              } else if (k == "dcp_frame_rate") {
 -                      _dcp_frame_rate = atoi (v.c_str ());
 +      {
 +              optional<string> c = f.optional_string_child ("Format");
 +              if (c) {
 +                      _format = Format::from_id (c.get ());
                }
 +      }
  
 -              _dci_metadata.read (k, v);
 -              
 -              /* Cached stuff */
 -              if (k == "width") {
 -                      _size.width = atoi (v.c_str ());
 -              } else if (k == "height") {
 -                      _size.height = atoi (v.c_str ());
 -              } else if (k == "length") {
 -                      int const vv = atoi (v.c_str ());
 -                      if (vv) {
 -                              _length = vv;
 -                      }
 -              } else if (k == "content_digest") {
 -                      _content_digest = v;
 -              } else if (k == "content_audio_stream" || (!version && k == "audio_stream")) {
 -                      _content_audio_streams.push_back (audio_stream_factory (v, version));
 -              } else if (k == "external_audio_stream") {
 -                      _sndfile_stream = audio_stream_factory (v, version);
 -              } else if (k == "subtitle_stream") {
 -                      _subtitle_streams.push_back (subtitle_stream_factory (v, version));
 -              } else if (k == "source_frame_rate") {
 -                      _source_frame_rate = atof (v.c_str ());
 -              } else if (version < 4 && k == "frames_per_second") {
 -                      _source_frame_rate = atof (v.c_str ());
 -                      /* Fill in what would have been used for DCP frame rate by the older version */
 -                      _dcp_frame_rate = best_dcp_frame_rate (_source_frame_rate);
 +      {
 +              optional<string> c = f.optional_string_child ("TrimType");
 +              if (!c || c.get() == "CPL") {
 +                      _trim_type = CPL;
 +              } else if (c && c.get() == "Encode") {
 +                      _trim_type = ENCODE;
                }
        }
  
 -      if (!version) {
 -              if (audio_sample_rate) {
 -                      /* version < 1 didn't specify sample rate in the audio streams, so fill it in here */
 -                      for (vector<shared_ptr<AudioStream> >::iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) {
 -                              (*i)->set_sample_rate (audio_sample_rate.get());
 -                      }
 -              }
 +      _crop.left = f.number_child<int> ("LeftCrop");
 +      _crop.right = f.number_child<int> ("RightCrop");
 +      _crop.top = f.number_child<int> ("TopCrop");
 +      _crop.bottom = f.number_child<int> ("BottomCrop");
  
 -              /* also the selected stream was specified as an index */
 -              if (audio_stream_index && audio_stream_index.get() >= 0 && audio_stream_index.get() < (int) _content_audio_streams.size()) {
 -                      _content_audio_stream = _content_audio_streams[audio_stream_index.get()];
 +      {
 +              list<shared_ptr<cxml::Node> > c = f.node_children ("Filter");
 +              for (list<shared_ptr<cxml::Node> >::iterator i = c.begin(); i != c.end(); ++i) {
 +                      _filters.push_back (Filter::from_id ((*i)->content ()));
                }
 +      }
  
 -              /* similarly the subtitle */
 -              if (subtitle_stream_index && subtitle_stream_index.get() >= 0 && subtitle_stream_index.get() < (int) _subtitle_streams.size()) {
 -                      _subtitle_stream = _subtitle_streams[subtitle_stream_index.get()];
 +      _scaler = Scaler::from_id (f.string_child ("Scaler"));
 +      _trim_start = f.number_child<int> ("TrimStart");
 +      _trim_end = f.number_child<int> ("TrimEnd");
 +      _ab = f.bool_child ("AB");
 +      _audio_gain = f.number_child<float> ("AudioGain");
 +      _audio_delay = f.number_child<int> ("AudioDelay");
 +      _with_subtitles = f.bool_child ("WithSubtitles");
 +      _subtitle_offset = f.number_child<float> ("SubtitleOffset");
 +      _subtitle_scale = f.number_child<float> ("SubtitleScale");
 +      _colour_lut = f.number_child<int> ("ColourLUT");
 +      _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
 +      _dci_metadata = DCIMetadata (f.node_child ("DCIMetadata"));
 +      _dcp_frame_rate = f.number_child<int> ("DCPFrameRate");
 +      _dci_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate"));
 +
 +      list<shared_ptr<cxml::Node> > c = f.node_children ("Content");
 +      for (list<shared_ptr<cxml::Node> >::iterator i = c.begin(); i != c.end(); ++i) {
 +
 +              string const type = (*i)->string_child ("Type");
 +              boost::shared_ptr<Content> c;
 +              
 +              if (type == "FFmpeg") {
 +                      c.reset (new FFmpegContent (*i));
 +              } else if (type == "ImageMagick") {
 +                      c.reset (new ImageMagickContent (*i));
 +              } else if (type == "Sndfile") {
 +                      c.reset (new SndfileContent (*i));
                }
 +
 +              _content.push_back (c);
        }
 -              
 +
 +      /* This must come after we've loaded the content, as we're looking things up in _content */
 +      _audio_mapping.set_from_xml (_content, f.node_child ("AudioMapping"));
 +
        _dirty = false;
 +
 +      _playlist->setup (_content);
  }
  
  libdcp::Size
@@@ -601,18 -725,47 +606,18 @@@ Film::file (string f) cons
        return p.string ();
  }
  
 -/** @return full path of the content (actual video) file
 - *  of the Film.
 - */
 -string
 -Film::content_path () const
 -{
 -      boost::mutex::scoped_lock lm (_state_mutex);
 -      if (boost::filesystem::path(_content).has_root_directory ()) {
 -              return _content;
 -      }
 -
 -      return file (_content);
 -}
 -
 -ContentType
 -Film::content_type () const
 -{
 -      if (boost::filesystem::is_directory (_content)) {
 -              /* Directory of images, we assume */
 -              return VIDEO;
 -      }
 -
 -      if (still_image_file (_content)) {
 -              return STILL;
 -      }
 -
 -      return VIDEO;
 -}
 -
  /** @return The sampling rate that we will resample the audio to */
  int
  Film::target_audio_sample_rate () const
  {
 -      if (!audio_stream()) {
 +      if (!has_audio ()) {
                return 0;
        }
        
        /* Resample to a DCI-approved sample rate */
 -      double t = dcp_audio_sample_rate (audio_stream()->sample_rate());
 +      double t = dcp_audio_sample_rate (audio_frame_rate());
  
 -      FrameRateConversion frc (source_frame_rate(), dcp_frame_rate());
 +      FrameRateConversion frc (video_frame_rate(), dcp_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
        */
  
        if (frc.change_speed) {
 -              t *= source_frame_rate() * frc.factor() / dcp_frame_rate();
 +              t *= video_frame_rate() * frc.factor() / dcp_frame_rate();
        }
  
        return rint (t);
  }
  
 -int
 -Film::still_duration_in_frames () const
 -{
 -      return still_duration() * source_frame_rate();
 -}
 -
  /** @return a DCI-compliant name for a DCP of this film */
  string
  Film::dci_name (bool if_created_now) const
                }
        }
  
 -      switch (audio_channels()) {
 +      switch (audio_channels ()) {
        case 1:
                d << "_10";
                break;
@@@ -752,21 -911,110 +757,21 @@@ Film::set_use_dci_name (bool u
  }
  
  void
 -Film::set_content (string c)
 -{
 -      string check = directory ();
 -
 -      boost::filesystem::path slash ("/");
 -      string platform_slash = slash.make_preferred().string ();
 -
 -      if (!ends_with (check, platform_slash)) {
 -              check += platform_slash;
 -      }
 -      
 -      if (boost::filesystem::path(c).has_root_directory () && starts_with (c, check)) {
 -              c = c.substr (_directory.length() + 1);
 -      }
 -
 -      string old_content;
 -      
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              if (c == _content) {
 -                      return;
 -              }
 -
 -              old_content = _content;
 -              _content = c;
 -      }
 -
 -      /* Reset streams here in case the new content doesn't have one or the other */
 -      _content_audio_stream = shared_ptr<AudioStream> ();
 -      _subtitle_stream = shared_ptr<SubtitleStream> ();
 -
 -      /* Start off using content audio */
 -      set_use_content_audio (true);
 -
 -      /* Create a temporary decoder so that we can get information
 -         about the content.
 -      */
 -
 -      try {
 -              Decoders d = decoder_factory (shared_from_this(), DecodeOptions());
 -              
 -              set_size (d.video->native_size ());
 -              set_source_frame_rate (d.video->frames_per_second ());
 -              set_dcp_frame_rate (best_dcp_frame_rate (source_frame_rate ()));
 -              set_subtitle_streams (d.video->subtitle_streams ());
 -              if (d.audio) {
 -                      set_content_audio_streams (d.audio->audio_streams ());
 -              }
 -
 -              {
 -                      boost::mutex::scoped_lock lm (_state_mutex);
 -                      _content = c;
 -              }
 -              
 -              signal_changed (CONTENT);
 -              
 -              /* Start off with the first audio and subtitle streams */
 -              if (d.audio && !d.audio->audio_streams().empty()) {
 -                      set_content_audio_stream (d.audio->audio_streams().front());
 -              }
 -              
 -              if (!d.video->subtitle_streams().empty()) {
 -                      set_subtitle_stream (d.video->subtitle_streams().front());
 -              }
 -              
 -              examine_content ();
 -
 -      } catch (...) {
 -
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _content = old_content;
 -              throw;
 -
 -      }
 -
 -      /* Default format */
 -      switch (content_type()) {
 -      case STILL:
 -              set_format (Format::from_id ("var-185"));
 -              break;
 -      case VIDEO:
 -              set_format (Format::from_id ("185"));
 -              break;
 -      }
 -
 -      /* Still image DCPs must use external audio */
 -      if (content_type() == STILL) {
 -              set_use_content_audio (false);
 -      }
 -}
 -
 -void
 -Film::set_trust_content_header (bool t)
 +Film::set_trust_content_headers (bool t)
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _trust_content_header = t;
 +              _trust_content_headers = t;
        }
        
 -      signal_changed (TRUST_CONTENT_HEADER);
 +      signal_changed (TRUST_CONTENT_HEADERS);
  
 -      if (!_trust_content_header && !content().empty()) {
 +      if (!_trust_content_headers && !content().empty()) {
                /* We just said that we don't trust the content's header */
 -              examine_content ();
 +              ContentList c = content ();
 +              for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 +                      examine_content (*i);
 +              }
        }
  }
               
@@@ -908,13 -1156,50 +913,13 @@@ Film::set_trim_type (TrimType t
  }
  
  void
 -Film::set_dcp_ab (bool a)
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _dcp_ab = a;
 -      }
 -      signal_changed (DCP_AB);
 -}
 -
 -void
 -Film::set_content_audio_stream (shared_ptr<AudioStream> s)
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _content_audio_stream = s;
 -      }
 -      signal_changed (CONTENT_AUDIO_STREAM);
 -}
 -
 -void
 -Film::set_external_audio (vector<string> a)
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _external_audio = a;
 -      }
 -
 -      shared_ptr<SndfileDecoder> decoder (new SndfileDecoder (shared_from_this(), DecodeOptions()));
 -      if (decoder->audio_stream()) {
 -              _sndfile_stream = decoder->audio_stream ();
 -      }
 -      
 -      signal_changed (EXTERNAL_AUDIO);
 -}
 -
 -void
 -Film::set_use_content_audio (bool e)
 +Film::set_ab (bool a)
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _use_content_audio = e;
 +              _ab = a;
        }
 -
 -      signal_changed (USE_CONTENT_AUDIO);
 +      signal_changed (AB);
  }
  
  void
@@@ -937,6 -1222,26 +942,6 @@@ Film::set_audio_delay (int d
        signal_changed (AUDIO_DELAY);
  }
  
 -void
 -Film::set_still_duration (int d)
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _still_duration = d;
 -      }
 -      signal_changed (STILL_DURATION);
 -}
 -
 -void
 -Film::set_subtitle_stream (shared_ptr<SubtitleStream> s)
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _subtitle_stream = s;
 -      }
 -      signal_changed (SUBTITLE_STREAM);
 -}
 -
  void
  Film::set_with_subtitles (bool w)
  {
@@@ -1009,306 -1314,184 +1014,306 @@@ Film::set_dcp_frame_rate (int f
  }
  
  void
 -Film::set_size (libdcp::Size s)
 +Film::signal_changed (Property p)
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _size = s;
 +              _dirty = true;
 +      }
 +
 +      switch (p) {
 +      case Film::CONTENT:
 +              _playlist->setup (content ());
 +              set_dcp_frame_rate (best_dcp_frame_rate (video_frame_rate ()));
 +              set_audio_mapping (_playlist->default_audio_mapping ());
 +              break;
 +      default:
 +              break;
 +      }
 +
 +      if (ui_signaller) {
 +              ui_signaller->emit (boost::bind (boost::ref (Changed), p));
        }
 -      signal_changed (SIZE);
  }
  
  void
 -Film::set_length (SourceFrame l)
 +Film::set_dci_date_today ()
  {
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _length = l;
 -      }
 -      signal_changed (LENGTH);
 +      _dci_date = boost::gregorian::day_clock::local_day ();
  }
  
 -void
 -Film::unset_length ()
 +string
 +Film::info_path (int f) const
  {
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _length = boost::none;
 +      boost::filesystem::path p;
 +      p /= info_dir ();
 +
 +      stringstream s;
 +      s.width (8);
 +      s << setfill('0') << f << ".md5";
 +
 +      p /= s.str();
 +
 +      /* info_dir() will already have added any initial bit of the path,
 +         so don't call file() on this.
 +      */
 +      return p.string ();
 +}
 +
 +string
 +Film::j2c_path (int f, bool t) const
 +{
 +      boost::filesystem::path p;
 +      p /= "j2c";
 +      p /= video_state_identifier ();
 +
 +      stringstream s;
 +      s.width (8);
 +      s << setfill('0') << f << ".j2c";
 +
 +      if (t) {
 +              s << ".tmp";
        }
 -      signal_changed (LENGTH);
 +
 +      p /= s.str();
 +      return file (p.string ());
  }
  
 -void
 -Film::set_content_digest (string d)
 +/** Make an educated guess as to whether we have a complete DCP
 + *  or not.
 + *  @return true if we do.
 + */
 +
 +bool
 +Film::have_dcp () const
  {
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _content_digest = d;
 +      try {
 +              libdcp::DCP dcp (dir (dcp_name()));
 +              dcp.read ();
 +      } catch (...) {
 +              return false;
        }
 -      _dirty = true;
 +
 +      return true;
 +}
 +
 +shared_ptr<Player>
 +Film::player () const
 +{
 +      boost::mutex::scoped_lock lm (_state_mutex);
 +      return shared_ptr<Player> (new Player (shared_from_this (), _playlist));
  }
  
  void
 -Film::set_content_audio_streams (vector<shared_ptr<AudioStream> > s)
 +Film::add_content (shared_ptr<Content> c)
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _content_audio_streams = s;
 +              _content.push_back (c);
        }
 -      signal_changed (CONTENT_AUDIO_STREAMS);
 +
 +      signal_changed (CONTENT);
 +
 +      examine_content (c);
  }
  
  void
 -Film::set_subtitle_streams (vector<shared_ptr<SubtitleStream> > s)
 +Film::remove_content (shared_ptr<Content> c)
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _subtitle_streams = s;
 +              ContentList::iterator i = find (_content.begin(), _content.end(), c);
 +              if (i != _content.end ()) {
 +                      _content.erase (i);
 +              }
        }
 -      signal_changed (SUBTITLE_STREAMS);
 +
 +      signal_changed (CONTENT);
  }
  
  void
 -Film::set_source_frame_rate (float f)
 +Film::move_content_earlier (shared_ptr<Content> c)
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _source_frame_rate = f;
 +              ContentList::iterator i = find (_content.begin(), _content.end(), c);
 +              if (i == _content.begin () || i == _content.end()) {
 +                      return;
 +              }
 +
 +              ContentList::iterator j = i;
 +              --j;
 +
 +              swap (*i, *j);
        }
 -      signal_changed (SOURCE_FRAME_RATE);
 +
 +      signal_changed (CONTENT);
  }
 -      
 +
  void
 -Film::signal_changed (Property p)
 +Film::move_content_later (shared_ptr<Content> c)
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _dirty = true;
 -      }
 +              ContentList::iterator i = find (_content.begin(), _content.end(), c);
 +              if (i == _content.end()) {
 +                      return;
 +              }
  
 -      if (ui_signaller) {
 -              ui_signaller->emit (boost::bind (boost::ref (Changed), p));
 +              ContentList::iterator j = i;
 +              ++j;
 +              if (j == _content.end ()) {
 +                      return;
 +              }
 +
 +              swap (*i, *j);
        }
 +
 +      signal_changed (CONTENT);
 +
 +}
 +
 +ContentAudioFrame
 +Film::audio_length () const
 +{
 +      return _playlist->audio_length ();
  }
  
  int
  Film::audio_channels () const
  {
 -      shared_ptr<AudioStream> s = audio_stream ();
 -      if (!s) {
 -              return 0;
 -      }
 -
 -      return s->channels ();
 +      return _playlist->audio_channels ();
  }
  
 -void
 -Film::set_dci_date_today ()
 +int
 +Film::audio_frame_rate () const
  {
 -      _dci_date = boost::gregorian::day_clock::local_day ();
 +      return _playlist->audio_frame_rate ();
  }
  
 -boost::shared_ptr<AudioStream>
 -Film::audio_stream () const
 +bool
 +Film::has_audio () const
  {
 -      if (use_content_audio()) {
 -              return _content_audio_stream;
 -      }
 +      return _playlist->has_audio ();
 +}
  
 -      return _sndfile_stream;
 +float
 +Film::video_frame_rate () const
 +{
 +      return _playlist->video_frame_rate ();
  }
  
 -string
 -Film::info_path (int f) const
 +libdcp::Size
 +Film::video_size () const
  {
 -      boost::filesystem::path p;
 -      p /= info_dir ();
 +      return _playlist->video_size ();
 +}
  
 -      stringstream s;
 -      s.width (8);
 -      s << setfill('0') << f << ".md5";
 +ContentVideoFrame
 +Film::video_length () const
 +{
 +      return _playlist->video_length ();
 +}
  
 -      p /= s.str();
 +/** Unfortunately this is needed as the GUI has FFmpeg-specific controls */
 +shared_ptr<FFmpegContent>
 +Film::ffmpeg () const
 +{
 +      boost::mutex::scoped_lock lm (_state_mutex);
 +      
 +      for (ContentList::const_iterator i = _content.begin (); i != _content.end(); ++i) {
 +              shared_ptr<FFmpegContent> f = boost::dynamic_pointer_cast<FFmpegContent> (*i);
 +              if (f) {
 +                      return f;
 +              }
 +      }
  
 -      /* info_dir() will already have added any initial bit of the path,
 -         so don't call file() on this.
 -      */
 -      return p.string ();
 +      return shared_ptr<FFmpegContent> ();
  }
  
 -string
 -Film::j2c_path (int f, bool t) const
 +vector<FFmpegSubtitleStream>
 +Film::ffmpeg_subtitle_streams () const
  {
 -      boost::filesystem::path p;
 -      p /= "j2c";
 -      p /= video_state_identifier ();
 +      shared_ptr<FFmpegContent> f = ffmpeg ();
 +      if (f) {
 +              return f->subtitle_streams ();
 +      }
  
 -      stringstream s;
 -      s.width (8);
 -      s << setfill('0') << f << ".j2c";
 +      return vector<FFmpegSubtitleStream> ();
 +}
  
 -      if (t) {
 -              s << ".tmp";
 +boost::optional<FFmpegSubtitleStream>
 +Film::ffmpeg_subtitle_stream () const
 +{
 +      shared_ptr<FFmpegContent> f = ffmpeg ();
 +      if (f) {
 +              return f->subtitle_stream ();
        }
  
 -      p /= s.str();
 -      return file (p.string ());
 +      return boost::none;
  }
  
 -/** Make an educated guess as to whether we have a complete DCP
 - *  or not.
 - *  @return true if we do.
 - */
 +vector<FFmpegAudioStream>
 +Film::ffmpeg_audio_streams () const
 +{
 +      shared_ptr<FFmpegContent> f = ffmpeg ();
 +      if (f) {
 +              return f->audio_streams ();
 +      }
  
 -bool
 -Film::have_dcp () const
 +      return vector<FFmpegAudioStream> ();
 +}
 +
 +boost::optional<FFmpegAudioStream>
 +Film::ffmpeg_audio_stream () const
  {
 -      try {
 -              libdcp::DCP dcp (dir (dcp_name()));
 -              dcp.read ();
 -      } catch (...) {
 -              return false;
 +      shared_ptr<FFmpegContent> f = ffmpeg ();
 +      if (f) {
 +              return f->audio_stream ();
        }
  
 -      return true;
 +      return boost::none;
  }
  
 -bool
 -Film::has_audio () const
 +void
 +Film::set_ffmpeg_subtitle_stream (FFmpegSubtitleStream s)
  {
 -      if (use_content_audio()) {
 -              return audio_stream();
 +      shared_ptr<FFmpegContent> f = ffmpeg ();
 +      if (f) {
 +              f->set_subtitle_stream (s);
        }
 +}
  
 -      vector<string> const e = external_audio ();
 -      for (vector<string>::const_iterator i = e.begin(); i != e.end(); ++i) {
 -              if (!i->empty ()) {
 -                      return true;
 -              }
 +void
 +Film::set_ffmpeg_audio_stream (FFmpegAudioStream s)
 +{
 +      shared_ptr<FFmpegContent> f = ffmpeg ();
 +      if (f) {
 +              f->set_audio_stream (s);
 +      }
 +}
 +
 +void
 +Film::set_audio_mapping (AudioMapping m)
 +{
 +      {
 +              boost::mutex::scoped_lock lm (_state_mutex);
 +              _audio_mapping = m;
        }
  
 -      return false;
 +      signal_changed (AUDIO_MAPPING);
  }
  
 +void
 +Film::content_changed (boost::weak_ptr<Content> c, int p)
 +{
 +      if (p == VideoContentProperty::VIDEO_FRAME_RATE) {
 +              set_dcp_frame_rate (best_dcp_frame_rate (video_frame_rate ()));
 +      } else if (p == AudioContentProperty::AUDIO_CHANNELS) {
 +              set_audio_mapping (_playlist->default_audio_mapping ());
 +      }               
 +
 +      if (ui_signaller) {
 +              ui_signaller->emit (boost::bind (boost::ref (ContentChanged), c, p));
 +      }
 +}
diff --combined src/lib/format.cc
index 3d18edb3e51fedb2f8bd9dd56fe4f8d02951aa9a,8c3d0d8ad7f8f5eea541258032415c1b6cab12b7..a83d53ebd1aa3ba01a486c488cb35d90b3645e67
@@@ -29,7 -29,6 +29,7 @@@
  #include <iostream>
  #include "format.h"
  #include "film.h"
 +#include "playlist.h"
  
  #include "i18n.h"
  
@@@ -51,7 -50,7 +51,7 @@@ FixedFormat::name () cons
                s << _nickname << N_(" (");
        }
  
-       s << setprecision(3) << (_ratio / 100.0) << N_(":1");
+       s << setprecision(3) << _ratio << N_(":1");
  
        if (!_nickname.empty ()) {
                s << N_(")");
@@@ -73,51 -72,51 +73,51 @@@ Format::setup_formats (
  {
        /// TRANSLATORS: these are film picture aspect ratios; "Academy" means 1.37, "Flat" 1.85 and "Scope" 2.39.
        _formats.push_back (
-               new FixedFormat (119, libdcp::Size (1285, 1080), N_("119"), _("1.19"), N_("F")
+               new FixedFormat (1.19, libdcp::Size (1285, 1080), N_("119"), _("1.19"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (133, libdcp::Size (1436, 1080), N_("133"), _("1.33"), N_("F")
+               new FixedFormat (4.0 / 3.0, libdcp::Size (1436, 1080), N_("133"), _("4:3"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (138, libdcp::Size (1485, 1080), N_("138"), _("1.375"), N_("F")
+               new FixedFormat (1.38, libdcp::Size (1485, 1080), N_("138"), _("1.375"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (133, libdcp::Size (1998, 1080), N_("133-in-flat"), _("4:3 within Flat"), N_("F")
+               new FixedFormat (4.0 / 3.0, libdcp::Size (1998, 1080), N_("133-in-flat"), _("4:3 within Flat"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (137, libdcp::Size (1480, 1080), N_("137"), _("Academy"), N_("F")
+               new FixedFormat (1.37, libdcp::Size (1480, 1080), N_("137"), _("Academy"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (166, libdcp::Size (1793, 1080), N_("166"), _("1.66"), N_("F")
+               new FixedFormat (1.66, libdcp::Size (1793, 1080), N_("166"), _("1.66"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (166, libdcp::Size (1998, 1080), N_("166-in-flat"), _("1.66 within Flat"), N_("F")
+               new FixedFormat (1.66, libdcp::Size (1998, 1080), N_("166-in-flat"), _("1.66 within Flat"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (178, libdcp::Size (1998, 1080), N_("178-in-flat"), _("16:9 within Flat"), N_("F")
+               new FixedFormat (1.78, libdcp::Size (1998, 1080), N_("178-in-flat"), _("16:9 within Flat"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (178, libdcp::Size (1920, 1080), N_("178"), _("16:9"), N_("F")
+               new FixedFormat (1.78, libdcp::Size (1920, 1080), N_("178"), _("16:9"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (185, libdcp::Size (1998, 1080), N_("185"), _("Flat"), N_("F")
+               new FixedFormat (1.85, libdcp::Size (1998, 1080), N_("185"), _("Flat"), N_("F")
                        ));
        
        _formats.push_back (
-               new FixedFormat (178, libdcp::Size (2048, 858), N_("178-in-scope"), _("16:9 within Scope"), N_("S")
+               new FixedFormat (1.78, libdcp::Size (2048, 858), N_("178-in-scope"), _("16:9 within Scope"), N_("S")
                        ));
        
        _formats.push_back (
-               new FixedFormat (239, libdcp::Size (2048, 858), N_("239"), _("Scope"), N_("S")
+               new FixedFormat (2.39, libdcp::Size (2048, 858), N_("239"), _("Scope"), N_("S")
                        ));
                
        _formats.push_back (
@@@ -182,12 -181,12 +182,12 @@@ Format::all (
        return _formats;
  }
  
- /** @param r Ratio multiplied by 100 (e.g. 185)
+ /** @param r Ratio
   *  @param dcp Size (in pixels) of the images that we should put in a DCP.
   *  @param id ID (e.g. 185)
   *  @param n Nick name (e.g. Flat)
   */
- FixedFormat::FixedFormat (int r, libdcp::Size dcp, string id, string n, string d)
+ FixedFormat::FixedFormat (float r, libdcp::Size dcp, string id, string n, string d)
        : Format (dcp, id, n, d)
        , _ratio (r)
  {
  int
  Format::dcp_padding (shared_ptr<const Film> f) const
  {
-       int p = rint ((_dcp_size.width - (_dcp_size.height * ratio_as_float(f))) / 2.0);
+       int p = rint ((_dcp_size.width - (_dcp_size.height * ratio(f))) / 2.0);
  
        /* This comes out -ve for Scope; bodge it */
        if (p < 0) {
  }
  
  float
- Format::container_ratio_as_float () const
+ Format::container_ratio () const
  {
        return static_cast<float> (_dcp_size.width) / _dcp_size.height;
  }
@@@ -222,16 -221,10 +222,10 @@@ VariableFormat::VariableFormat (libdcp:
  
  }
  
- int
- VariableFormat::ratio_as_integer (shared_ptr<const Film> f) const
- {
-       return rint (ratio_as_float (f) * 100);
- }
  float
- VariableFormat::ratio_as_float (shared_ptr<const Film> f) const
+ VariableFormat::ratio (shared_ptr<const Film> f) const
  {
 -      libdcp::Size const c = f->cropped_size (f->size ());
 +      libdcp::Size const c = f->cropped_size (f->video_size ());
        return float (c.width) / c.height;
  }
  
diff --combined src/lib/format.h
index 7958ca5341604e199a181b2295e0708b3614d542,e953062329d9b583a0cfd078159499fb991320dc..f240c89fc85db4b76ee45f34291e8294dfdbd5b4
@@@ -38,18 -38,10 +38,10 @@@ public
                , _dci_name (d)
        {}
  
-       /** @return the aspect ratio multiplied by 100
-        *  (e.g. 239 for Cinemascope 2.39:1)
-        */
-       virtual int ratio_as_integer (boost::shared_ptr<const Film> f) const = 0;
-       /** @return the ratio as a floating point number */
-       virtual float ratio_as_float (boost::shared_ptr<const Film> f) const = 0;
-       /** @return the ratio of the container (including any padding) as a floating point number */
-       float container_ratio_as_float () const;
+       /** @return the ratio of the container (including any padding) */
+       float container_ratio () const;
  
 -      int dcp_padding (boost::shared_ptr<const Film> f) const;
 +      int dcp_padding (boost::shared_ptr<const Film>) const;
  
        /** @return size in pixels of the images that we should
         *  put in a DCP for this ratio.  This size will not correspond
@@@ -84,6 -76,9 +76,9 @@@
        static void setup_formats ();
  
  protected:    
+         /** @return the ratio */
+       virtual float ratio (boost::shared_ptr<const Film> f) const = 0;
        /** libdcp::Size in pixels of the images that we should
         *  put in a DCP for this ratio.  This size will not correspond
         *  to the ratio when we are doing things like 16:9 in a Flat frame.
@@@ -107,22 -102,17 +102,17 @@@ private
  class FixedFormat : public Format
  {
  public:
-       FixedFormat (int, libdcp::Size, std::string, std::string, std::string);
+       FixedFormat (float, libdcp::Size, std::string, std::string, std::string);
  
-       int ratio_as_integer (boost::shared_ptr<const Film>) const {
+       float ratio (boost::shared_ptr<const Film>) const {
                return _ratio;
        }
  
-       float ratio_as_float (boost::shared_ptr<const Film>) const {
-               return _ratio / 100.0;
-       }
        std::string name () const;
        
  private:
  
-       /** Ratio expressed as the actual ratio multiplied by 100 */
-       int _ratio;
+       float _ratio;
  };
  
  class VariableFormat : public Format
  public:
        VariableFormat (libdcp::Size, std::string, std::string, std::string);
  
-       int ratio_as_integer (boost::shared_ptr<const Film> f) const;
-       float ratio_as_float (boost::shared_ptr<const Film> f) const;
+       float ratio (boost::shared_ptr<const Film> f) const;
  
        std::string name () const;
  };
index 7049b7d6e31945ace55eadd9e16bc4ef6f04a170,5ce22c29622a403c33425a5ce6e8e9a5f3e26de5..3888347ca73329359192539aa81a21fd3cc3e1ca
@@@ -20,7 -20,6 +20,7 @@@
  #include <iostream>
  #include <boost/filesystem.hpp>
  #include <Magick++.h>
 +#include "imagemagick_content.h"
  #include "imagemagick_decoder.h"
  #include "image.h"
  #include "film.h"
@@@ -32,53 -31,64 +32,53 @@@ using std::cout
  using boost::shared_ptr;
  using libdcp::Size;
  
 -ImageMagickDecoder::ImageMagickDecoder (
 -      boost::shared_ptr<Film> f, DecodeOptions o)
 -      : Decoder (f, o)
 -      , VideoDecoder (f, o)
 +ImageMagickDecoder::ImageMagickDecoder (shared_ptr<const Film> f, shared_ptr<const ImageMagickContent> c)
 +      : Decoder (f)
 +      , VideoDecoder (f)
 +      , _imagemagick_content (c)
 +      , _position (0)
  {
 -      if (boost::filesystem::is_directory (_film->content_path())) {
 -              for (
 -                      boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (_film->content_path());
 -                      i != boost::filesystem::directory_iterator();
 -                      ++i) {
 -
 -                      if (still_image_file (i->path().string())) {
 -                              _files.push_back (i->path().string());
 -                      }
 -              }
 -      } else {
 -              _files.push_back (_film->content_path ());
 -      }
 -
 -      _iter = _files.begin ();
 +      
  }
  
  libdcp::Size
  ImageMagickDecoder::native_size () const
  {
 -      if (_files.empty ()) {
 -              throw DecodeError (_("no still image files found"));
 -      }
 -
 -      /* Look at the first file and assume its size holds for all */
        using namespace MagickCore;
 -      Magick::Image* image = new Magick::Image (_film->content_path ());
 +      Magick::Image* image = new Magick::Image (_imagemagick_content->file().string());
        libdcp::Size const s = libdcp::Size (image->columns(), image->rows());
        delete image;
  
        return s;
  }
  
 +int
 +ImageMagickDecoder::video_length () const
 +{
 +      return _imagemagick_content->video_length ();
 +}
 +
  bool
  ImageMagickDecoder::pass ()
  {
 -      if (_iter == _files.end()) {
 -              if (video_frame() >= _film->still_duration_in_frames()) {
 -                      return true;
 -              }
 +      if (_position < 0 || _position >= _imagemagick_content->video_length ()) {
 +              return true;
 +      }
  
-       if (have_last_video ()) {
-               repeat_last_video (double (_position) / 24);
 -              emit_video (_image, true, double (video_frame()) / frames_per_second());
++      if (_image) {
++              emit_video (_image, true, double (_position) / 24);
 +              _position++;
                return false;
        }
        
 -      Magick::Image* magick_image = new Magick::Image (_film->content_path ());
 +      Magick::Image* magick_image = new Magick::Image (_imagemagick_content->file().string ());
        
        libdcp::Size size = native_size ();
--      shared_ptr<Image> image (new SimpleImage (PIX_FMT_RGB24, size, false));
++      _image.reset (new SimpleImage (PIX_FMT_RGB24, size, false));
  
        using namespace MagickCore;
        
--      uint8_t* p = image->data()[0];
++      uint8_t* p = _image->data()[0];
        for (int y = 0; y < size.height; ++y) {
                for (int x = 0; x < size.width; ++x) {
                        Magick::Color c = magick_image->pixelColor (x, y);
  
        delete magick_image;
  
-       image = image->crop (_film->crop(), true);
-       
-       emit_video (image, double (_position) / 24);
 -      _image = image->crop (_film->crop(), true);
 -
 -      emit_video (_image, false, double (video_frame()) / frames_per_second());
++      _image = _image->crop (_film->crop(), true);
++      emit_video (_image, false, double (_position) / 24);
  
 -      ++_iter;
 +      ++_position;
        return false;
  }
  
@@@ -105,16 -115,44 +104,16 @@@ ImageMagickDecoder::pixel_format () con
        return PIX_FMT_RGB24;
  }
  
 -bool
 -ImageMagickDecoder::seek_to_last ()
 -{
 -      if (_iter == _files.end()) {
 -              _iter = _files.begin();
 -      } else {
 -              --_iter;
 -      }
 -
 -      return false;
 -}
 -
  bool
  ImageMagickDecoder::seek (double t)
  {
 -      int const f = t * frames_per_second();
 -      
 -      _iter = _files.begin ();
 -      for (int i = 0; i < f; ++i) {
 -              if (_iter == _files.end()) {
 -                      return true;
 -              }
 -              ++_iter;
 -      }
 -      
 -      return false;
 -}
 +      int const f = t * _imagemagick_content->video_frame_rate ();
  
 -void
 -ImageMagickDecoder::film_changed (Film::Property p)
 -{
 -      if (p == Film::CROP) {
 -              OutputChanged ();
 +      if (f >= _imagemagick_content->video_length()) {
 +              _position = 0;
 +              return true;
        }
 -}
  
 -float
 -ImageMagickDecoder::frames_per_second () const
 -{
 -      return _film->source_frame_rate ();
 +      _position = f;
 +      return false;
  }
index 40a89bb1594007ad65eb798fab7fdb7eb9be43c1,80a08f81f8374a3126a425ee413d0c0eedf21e29..e7c9dee9a3ac35373668433e6208d02128aafc0a
@@@ -23,24 -23,37 +23,24 @@@ namespace Magick 
        class Image;
  }
  
 +class ImageMagickContent;
 +
  class ImageMagickDecoder : public VideoDecoder
  {
  public:
 -      ImageMagickDecoder (boost::shared_ptr<Film>, DecodeOptions);
 -
 -      float frames_per_second () const;
 +      ImageMagickDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const ImageMagickContent>);
  
 -      libdcp::Size native_size () const;
 -
 -      SourceFrame length () const {
 -              /* We don't know */
 -              return 0;
 +      float video_frame_rate () const {
 +              return 24;
        }
  
 -      int audio_channels () const {
 -              return 0;
 -      }
 -
 -      int audio_sample_rate () const {
 -              return 0;
 -      }
 -
 -      int64_t audio_channel_layout () const {
 -              return 0;
 -      }
 +      libdcp::Size native_size () const;
 +      ContentVideoFrame video_length () const;
  
        bool seek (double);
 -      bool seek_to_last ();
 +      bool pass ();
  
  protected:
 -      bool pass ();
        PixelFormat pixel_format () const;
  
        int time_base_numerator () const {
        }
  
  private:
 -      void film_changed (Film::Property);
 -      
 -      std::list<std::string> _files;
 -      std::list<std::string>::iterator _iter;
 -
 +      boost::shared_ptr<const ImageMagickContent> _imagemagick_content;
+       boost::shared_ptr<Image> _image;
 +      ContentVideoFrame _position;
  };
diff --combined src/lib/matcher.cc
index 77ed9b6515bbb3e40add4a89e3b4d6d6d81a0d52,34ddc86d6d931eb0b6f61e8eba4c7abd0d68f651..edbb084de7ebd6e4a872cdaf3218a00ab5199d03
  #include "i18n.h"
  
  using std::min;
+ using std::cout;
+ using std::list;
  using boost::shared_ptr;
  
  Matcher::Matcher (shared_ptr<Log> log, int sample_rate, float frames_per_second)
-       : AudioVideoProcessor (log)
+       : Processor (log)
        , _sample_rate (sample_rate)
        , _frames_per_second (frames_per_second)
        , _video_frames (0)
        , _audio_frames (0)
+       , _had_first_video (false)
+       , _had_first_audio (false)
  {
  
  }
  
  void
- Matcher::process_video (shared_ptr<Image> i, bool same, shared_ptr<Subtitle> s)
 -Matcher::process_video (boost::shared_ptr<Image> image, bool same, boost::shared_ptr<Subtitle> sub, double t)
++Matcher::process_video (shared_ptr<Image> image, bool same, boost::shared_ptr<Subtitle> sub, double t)
  {
-       Video (i, same, s);
-       _video_frames++;
+       _pixel_format = image->pixel_format ();
+       _size = image->size ();
  
-       _pixel_format = i->pixel_format ();
-       _size = i->size ();
+       _log->log(String::compose("Matcher video @ %1 [audio=%2, video=%3, pending_audio=%4]", t, _audio_frames, _video_frames, _pending_audio.size()));
+       if (!_first_input) {
+               _first_input = t;
+       }
+       bool const this_is_first_video = !_had_first_video;
+       _had_first_video = true;
+       if (this_is_first_video && _had_first_audio) {
+               /* First video since we got audio */
+               fix_start (t);
+       }
+       /* Video before audio is fine, since we can make up an arbitrary difference
+          with audio samples (contrasting with video which is quantised to frames)
+       */
+       /* Difference between where this video is and where it should be */
+       double const delta = t - _first_input.get() - _video_frames / _frames_per_second;
+       double const one_frame = 1 / _frames_per_second;
+       
+       if (delta > one_frame) {
+               /* Insert frames to make up the difference */
+               int const extra = rint (delta / one_frame);
+               for (int i = 0; i < extra; ++i) {
+                       repeat_last_video ();
+                       _log->log (String::compose ("Extra video frame inserted at %1s", _video_frames / _frames_per_second));
+               }
+       }
+               
+       if (delta > -one_frame) {
+               Video (image, same, sub);
+               ++_video_frames;
+       } else {
+               /* We are omitting a frame to keep things right */
+               _log->log (String::compose ("Frame removed at %1s", t));
+       }
+       
+       _last_image = image;
+       _last_subtitle = sub;
  }
  
  void
- Matcher::process_audio (shared_ptr<AudioBuffers> b)
 -Matcher::process_audio (boost::shared_ptr<AudioBuffers> b, double t)
++Matcher::process_audio (shared_ptr<AudioBuffers> b, double t)
  {
-       Audio (b);
-       _audio_frames += b->frames ();
        _channels = b->channels ();
+       _log->log (String::compose ("Matcher audio @ %1 [video=%2, audio=%3, pending_audio=%4]", t, _video_frames, _audio_frames, _pending_audio.size()));
+       if (!_first_input) {
+               _first_input = t;
+       }
+       bool const this_is_first_audio = _had_first_audio;
+       _had_first_audio = true;
+       
+       if (!_had_first_video) {
+               /* No video yet; we must postpone these data until we have some */
+               _pending_audio.push_back (AudioRecord (b, t));
+       } else if (this_is_first_audio && !_had_first_video) {
+               /* First audio since we got video */
+               _pending_audio.push_back (AudioRecord (b, t));
+               fix_start (_first_input.get ());
+       } else {
+               /* Normal running.  We assume audio time stamps are consecutive */
+               Audio (b);
+               _audio_frames += b->frames ();
+       }
  }
  
  void
@@@ -62,39 -124,58 +124,58 @@@ Matcher::process_end (
                /* We won't do anything */
                return;
        }
+       _log->log (String::compose ("Matcher has seen %1 video frames (which equals %2 audio frames) and %3 audio frames",
+                                   _video_frames, video_frames_to_audio_frames (_video_frames, _sample_rate, _frames_per_second), _audio_frames));
        
-       int64_t audio_short_by_frames = video_frames_to_audio_frames (_video_frames, _sample_rate, _frames_per_second) - _audio_frames;
-       _log->log (
-               String::compose (
-                       N_("Matching processor has seen %1 video frames (which equals %2 audio frames) and %3 audio frames"),
-                       _video_frames,
-                       video_frames_to_audio_frames (_video_frames, _sample_rate, _frames_per_second),
-                       _audio_frames
-                       )
-               );
+       match ((double (_audio_frames) / _sample_rate) - (double (_video_frames) / _frames_per_second));
+ }
+ void
+ Matcher::fix_start (double first_video)
+ {
+       assert (!_pending_audio.empty ());
+       _log->log (String::compose ("Fixing start; video at %1, audio at %2", first_video, _pending_audio.front().time));
+       match (first_video - _pending_audio.front().time);
+       for (list<AudioRecord>::iterator i = _pending_audio.begin(); i != _pending_audio.end(); ++i) {
+               process_audio (i->audio, i->time);
+       }
        
-       if (audio_short_by_frames < 0) {
-               
-               _log->log (String::compose (N_("%1 too many audio frames"), -audio_short_by_frames));
-               
-               /* We have seen more audio than video.  Emit enough black video frames so that we reverse this */
-               int const black_video_frames = ceil (-audio_short_by_frames * _frames_per_second / _sample_rate);
+       _pending_audio.clear ();
+ }
+ void
+ Matcher::match (double extra_video_needed)
+ {
+       _log->log (String::compose ("Match %1", extra_video_needed));
+       
+       if (extra_video_needed > 0) {
+               /* Emit black video frames */
                
+               int const black_video_frames = ceil (extra_video_needed * _frames_per_second);
                _log->log (String::compose (N_("Emitting %1 frames of black video"), black_video_frames));
  
                shared_ptr<Image> black (new SimpleImage (_pixel_format.get(), _size.get(), true));
                black->make_black ();
                for (int i = 0; i < black_video_frames; ++i) {
                        Video (black, i != 0, shared_ptr<Subtitle>());
+                       ++_video_frames;
                }
-               
-               /* Now recompute our check value */
-               audio_short_by_frames = video_frames_to_audio_frames (_video_frames, _sample_rate, _frames_per_second) - _audio_frames;
+               extra_video_needed -= black_video_frames / _frames_per_second;
        }
-       
-       if (audio_short_by_frames > 0) {
-               _log->log (String::compose (N_("Emitted %1 too few audio frames"), audio_short_by_frames));
+       if (extra_video_needed < 0) {
+               
+               /* Emit silence */
+               
+               int64_t to_do = -extra_video_needed * _sample_rate;
+               _log->log (String::compose (N_("Emitting %1 frames of silence"), to_do));
  
                /* Do things in half second blocks as I think there may be limits
                   to what FFmpeg (and in particular the resampler) can cope with.
                shared_ptr<AudioBuffers> b (new AudioBuffers (_channels.get(), block));
                b->make_silent ();
                
-               int64_t to_do = audio_short_by_frames;
                while (to_do > 0) {
                        int64_t const this_time = min (to_do, block);
                        b->set_frames (this_time);
                }
        }
  }
+ void
+ Matcher::repeat_last_video ()
+ {
+       if (!_last_image) {
+               _last_image.reset (new SimpleImage (_pixel_format.get(), _size.get(), true));
+               _last_image->make_black ();
+       }
+       Video (_last_image, true, _last_subtitle);
+       ++_video_frames;
+ }
diff --combined src/lib/player.cc
index c77059b0ab546ce5fdb503fa12b6becce155e3e1,0000000000000000000000000000000000000000..635b67cad728e3cc0d08905b91e427500eef9c26
mode 100644,000000..100644
--- /dev/null
@@@ -1,272 -1,0 +1,283 @@@
-       , _video_sync (true)
 +/*
 +    Copyright (C) 2013 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
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include "player.h"
 +#include "film.h"
 +#include "ffmpeg_decoder.h"
 +#include "ffmpeg_content.h"
 +#include "imagemagick_decoder.h"
 +#include "imagemagick_content.h"
 +#include "sndfile_decoder.h"
 +#include "sndfile_content.h"
 +#include "playlist.h"
 +#include "job.h"
 +
 +using std::list;
 +using std::cout;
 +using boost::shared_ptr;
 +using boost::weak_ptr;
 +using boost::dynamic_pointer_cast;
 +
 +Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
 +      : _film (f)
 +      , _playlist (p)
 +      , _video (true)
 +      , _audio (true)
 +      , _subtitles (true)
 +      , _have_valid_decoders (false)
-               Audio (_audio_buffers);
 +{
 +      _playlist->Changed.connect (bind (&Player::playlist_changed, this));
 +      _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2));
 +}
 +
 +void
 +Player::disable_video ()
 +{
 +      _video = false;
 +}
 +
 +void
 +Player::disable_audio ()
 +{
 +      _audio = false;
 +}
 +
 +void
 +Player::disable_subtitles ()
 +{
 +      _subtitles = false;
 +}
 +
 +bool
 +Player::pass ()
 +{
 +      if (!_have_valid_decoders) {
 +              setup_decoders ();
 +              _have_valid_decoders = true;
 +      }
 +      
 +      bool done = true;
 +      
 +      if (_video_decoder != _video_decoders.end ()) {
 +              if ((*_video_decoder)->pass ()) {
 +                      _video_decoder++;
 +              }
 +              
 +              if (_video_decoder != _video_decoders.end ()) {
 +                      done = false;
 +              }
 +      }
 +
 +      if (_playlist->audio_from() == Playlist::AUDIO_SNDFILE) {
 +              for (list<shared_ptr<SndfileDecoder> >::iterator i = _sndfile_decoders.begin(); i != _sndfile_decoders.end(); ++i) {
 +                      if (!(*i)->pass ()) {
 +                              done = false;
 +                      }
 +              }
 +
- Player::process_video (shared_ptr<Image> i, bool same, shared_ptr<Subtitle> s)
++              Audio (_audio_buffers, _audio_time.get());
 +              _audio_buffers.reset ();
++              _audio_time = boost::none;
 +      }
 +
 +      return done;
 +}
 +
 +void
 +Player::set_progress (shared_ptr<Job> job)
 +{
 +      /* Assume progress can be divined from how far through the video we are */
 +
 +      if (_video_decoder == _video_decoders.end() || !_playlist->video_length()) {
 +              return;
 +      }
 +      
 +      ContentVideoFrame p = 0;
 +      list<shared_ptr<VideoDecoder> >::iterator i = _video_decoders.begin ();
 +      while (i != _video_decoders.end() && i != _video_decoder) {
 +              p += (*i)->video_length ();
 +      }
 +
 +      job->set_progress (float ((*_video_decoder)->video_frame ()) / _playlist->video_length ());
 +}
 +
 +void
-       Video (i, same, s);
++Player::process_video (shared_ptr<Image> i, bool same, shared_ptr<Subtitle> s, double t)
 +{
- Player::process_audio (weak_ptr<const AudioContent> c, shared_ptr<AudioBuffers> b)
++      /* XXX: this time will need mangling to add on the offset of the start of the content */
++      Video (i, same, s, t);
 +}
 +
 +void
-               Audio (_audio_buffers);
++Player::process_audio (weak_ptr<const AudioContent> c, shared_ptr<AudioBuffers> b, double t)
 +{
++      /* XXX: this time will need mangling to add on the offset of the start of the content */
 +      AudioMapping mapping = _film->audio_mapping ();
 +      if (!_audio_buffers) {
 +              _audio_buffers.reset (new AudioBuffers (mapping.dcp_channels(), b->frames ()));
 +              _audio_buffers->make_silent ();
++              _audio_time = t;
 +      }
 +
 +      for (int i = 0; i < b->channels(); ++i) {
 +              list<libdcp::Channel> dcp = mapping.content_to_dcp (AudioMapping::Channel (c, i));
 +              for (list<libdcp::Channel>::iterator j = dcp.begin(); j != dcp.end(); ++j) {
 +                      _audio_buffers->accumulate (b, i, static_cast<int> (*j));
 +              }
 +      }
 +
 +      if (_playlist->audio_from() == Playlist::AUDIO_FFMPEG) {
 +              /* We can just emit this audio now as it will all be here */
-                                               _subtitles,
-                                               _video_sync
++              Audio (_audio_buffers, t);
 +              _audio_buffers.reset ();
++              _audio_time = boost::none;
 +      }
 +}
 +
 +/** @return true on error */
 +bool
 +Player::seek (double t)
 +{
 +      if (!_have_valid_decoders) {
 +              setup_decoders ();
 +              _have_valid_decoders = true;
 +      }
 +
 +      /* Find the decoder that contains this position */
 +      _video_decoder = _video_decoders.begin ();
 +      while (_video_decoder != _video_decoders.end ()) {
 +              double const this_length = double ((*_video_decoder)->video_length()) / _film->video_frame_rate ();
 +              if (t < this_length) {
 +                      break;
 +              }
 +              t -= this_length;
 +              ++_video_decoder;
 +      }
 +      
 +      if (_video_decoder != _video_decoders.end()) {
 +              (*_video_decoder)->seek (t);
 +      } else {
 +              return true;
 +      }
 +
 +      /* XXX: don't seek audio because we don't need to... */
 +
 +      return false;
 +}
 +
++
++void
++Player::seek_back ()
++{
++      /* XXX */
++}
++
++void
++Player::seek_forward ()
++{
++      /* XXX */
++}
++
++
 +void
 +Player::setup_decoders ()
 +{
 +      _video_decoders.clear ();
 +      _video_decoder = _video_decoders.end ();
 +      _sndfile_decoders.clear ();
 +      
 +      if (_video) {
 +              list<shared_ptr<const VideoContent> > vc = _playlist->video ();
 +              for (list<shared_ptr<const VideoContent> >::iterator i = vc.begin(); i != vc.end(); ++i) {
 +
 +                      shared_ptr<VideoDecoder> d;
 +                      
 +                      /* 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 && _playlist->audio_from() == Playlist::AUDIO_FFMPEG,
-                                       fd->Audio.connect (bind (&Player::process_audio, this, fc, _1));
++                                              _subtitles
 +                                              )
 +                                      );
 +
 +                              if (_playlist->audio_from() == Playlist::AUDIO_FFMPEG) {
-                       d->Audio.connect (bind (&Player::process_audio, this, *i, _1));
++                                      fd->Audio.connect (bind (&Player::process_audio, this, fc, _1, _2));
 +                              }
 +
 +                              d = fd;
 +                      }
 +
 +                      shared_ptr<const ImageMagickContent> ic = dynamic_pointer_cast<const ImageMagickContent> (*i);
 +                      if (ic) {
 +                              d.reset (new ImageMagickDecoder (_film, ic));
 +                      }
 +
 +                      d->connect_video (shared_from_this ());
 +                      _video_decoders.push_back (d);
 +              }
 +
 +              _video_decoder = _video_decoders.begin ();
 +      }
 +
 +      if (_audio && _playlist->audio_from() == Playlist::AUDIO_SNDFILE) {
 +              list<shared_ptr<const SndfileContent> > sc = _playlist->sndfile ();
 +              for (list<shared_ptr<const SndfileContent> >::iterator i = sc.begin(); i != sc.end(); ++i) {
 +                      shared_ptr<SndfileDecoder> d (new SndfileDecoder (_film, *i));
 +                      _sndfile_decoders.push_back (d);
- void
- Player::disable_video_sync ()
- {
-       _video_sync = false;
- }
++                      d->Audio.connect (bind (&Player::process_audio, this, *i, _1, _2));
 +              }
 +      }
 +}
 +
 +double
 +Player::last_video_time () const
 +{
 +      double t = 0;
 +      for (list<shared_ptr<VideoDecoder> >::const_iterator i = _video_decoders.begin(); i != _video_decoder; ++i) {
 +              t += (*i)->video_length() / (*i)->video_frame_rate ();
 +      }
 +
 +      return t + (*_video_decoder)->last_content_time ();
 +}
 +
 +void
 +Player::content_changed (weak_ptr<Content> w, int p)
 +{
 +      shared_ptr<Content> c = w.lock ();
 +      if (!c) {
 +              return;
 +      }
 +
 +      if (p == VideoContentProperty::VIDEO_LENGTH) {
 +              if (dynamic_pointer_cast<FFmpegContent> (c)) {
 +                      /* FFmpeg content length changes are serious; we need new decoders */
 +                      _have_valid_decoders = false;
 +              }
 +      }
 +}
 +
 +void
 +Player::playlist_changed ()
 +{
 +      _have_valid_decoders = false;
 +}
diff --combined src/lib/player.h
index 9a55b8599a4031dce55af54e202e060af32dda45,0000000000000000000000000000000000000000..539fae67a2ad7730e226af84bffd95f37f1dad14
mode 100644,000000..100644
--- /dev/null
@@@ -1,78 -1,0 +1,78 @@@
- class Player : public VideoSource, public AudioSource, public VideoSink, public boost::enable_shared_from_this<Player>
 +/*
 +    Copyright (C) 2013 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
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#ifndef DCPOMATIC_PLAYER_H
 +#define DCPOMATIC_PLAYER_H
 +
 +#include <list>
 +#include <boost/shared_ptr.hpp>
 +#include <boost/enable_shared_from_this.hpp>
 +#include "video_source.h"
 +#include "audio_source.h"
 +#include "video_sink.h"
 +#include "audio_sink.h"
 +
 +class VideoDecoder;
 +class SndfileDecoder;
 +class Job;
 +class Film;
 +class Playlist;
 +class AudioContent;
 +
-       void disable_video_sync ();
++class Player : public TimedVideoSource, public TimedAudioSource, public TimedVideoSink, public boost::enable_shared_from_this<Player>
 +{
 +public:
 +      Player (boost::shared_ptr<const Film>, boost::shared_ptr<const Playlist>);
 +
 +      void disable_video ();
 +      void disable_audio ();
 +      void disable_subtitles ();
-       void process_video (boost::shared_ptr<Image> i, bool same, boost::shared_ptr<Subtitle> s);
-       void process_audio (boost::weak_ptr<const AudioContent>, boost::shared_ptr<AudioBuffers>);
 +
 +      bool pass ();
 +      void set_progress (boost::shared_ptr<Job>);
 +      bool seek (double);
++      void seek_back ();
++      void seek_forward ();
 +
 +      double last_video_time () const;
 +
 +private:
-       bool _video_sync;
++      void process_video (boost::shared_ptr<Image> i, bool same, boost::shared_ptr<Subtitle> s, double);
++      void process_audio (boost::weak_ptr<const AudioContent>, boost::shared_ptr<AudioBuffers>, double);
 +      void setup_decoders ();
 +      void playlist_changed ();
 +      void content_changed (boost::weak_ptr<Content>, int);
 +
 +      boost::shared_ptr<const Film> _film;
 +      boost::shared_ptr<const Playlist> _playlist;
 +      
 +      bool _video;
 +      bool _audio;
 +      bool _subtitles;
 +      
 +      bool _have_valid_decoders;
 +      std::list<boost::shared_ptr<VideoDecoder> > _video_decoders;
 +      std::list<boost::shared_ptr<VideoDecoder> >::iterator _video_decoder;
 +      std::list<boost::shared_ptr<SndfileDecoder> > _sndfile_decoders;
 +
 +      boost::shared_ptr<AudioBuffers> _audio_buffers;
++      boost::optional<double> _audio_time;
 +};
 +
 +#endif
diff --combined src/lib/processor.h
index 5dbafab7f9d6d7c518e0a98e15383bd385640e64,603239f8ff7094ebadb0a09d22304654d88cb427..7b7735faafa139a43b60f69f1796387edf79e413
@@@ -21,8 -21,8 +21,8 @@@
   *  @brief Parent class for classes which accept and then emit video or audio data.
   */
  
 -#ifndef DVDOMATIC_PROCESSOR_H
 -#define DVDOMATIC_PROCESSOR_H
 +#ifndef DCPOMATIC_PROCESSOR_H
 +#define DCPOMATIC_PROCESSOR_H
  
  #include "video_source.h"
  #include "video_sink.h"
@@@ -67,6 -67,15 +67,15 @@@ public
        {}
  };
  
+ class TimedAudioVideoProcessor : public Processor, public TimedVideoSource, public TimedVideoSink, public TimedAudioSource, public TimedAudioSink
+ {
+ public:
+       TimedAudioVideoProcessor (boost::shared_ptr<Log> log)
+               : Processor (log)
+       {}
+ };
+                               
  /** @class AudioProcessor
   *  @brief A processor which handles just audio data.
   */
@@@ -95,4 -104,12 +104,12 @@@ public
        {}
  };
  
+ class TimedVideoProcessor : public Processor, public TimedVideoSource, public TimedVideoSink
+ {
+ public:
+       TimedVideoProcessor (boost::shared_ptr<Log> log)
+               : Processor (log)
+       {}
+ };    
  #endif
index c7311112acdd28cf4303acb5fb581912bdaa932e,af59c049c31dba1acc59d9dad4132a2de5b8dcaa..4db45f1d40f14b53552997665fbff8198ba59df9
@@@ -19,7 -19,6 +19,7 @@@
  
  #include <iostream>
  #include <sndfile.h>
 +#include "sndfile_content.h"
  #include "sndfile_decoder.h"
  #include "film.h"
  #include "exceptions.h"
  
  using std::vector;
  using std::string;
 -using std::stringstream;
  using std::min;
  using std::cout;
  using boost::shared_ptr;
 -using boost::optional;
  
 -SndfileDecoder::SndfileDecoder (shared_ptr<Film> f, DecodeOptions o)
 -      : Decoder (f, o)
 -      , AudioDecoder (f, o)
 +SndfileDecoder::SndfileDecoder (shared_ptr<const Film> f, shared_ptr<const SndfileContent> c)
 +      : Decoder (f)
 +      , AudioDecoder (f)
 +      , _sndfile_content (c)
  {
 -      sf_count_t frames;
 -      vector<SNDFILE*> sf = open_files (frames);
 -      close_files (sf);
 -}
 -
 -vector<SNDFILE*>
 -SndfileDecoder::open_files (sf_count_t & frames)
 -{
 -      vector<string> const files = _film->external_audio ();
 -
 -      int N = 0;
 -      for (size_t i = 0; i < files.size(); ++i) {
 -              if (!files[i].empty()) {
 -                      N = i + 1;
 -              }
 +      _sndfile = sf_open (_sndfile_content->file().string().c_str(), SFM_READ, &_info);
 +      if (!_sndfile) {
 +              throw DecodeError (_("could not open audio file for reading"));
        }
  
 -      if (N == 0) {
 -              return vector<SNDFILE*> ();
 -      }
 -
 -      bool first = true;
 -      frames = 0;
 -      
 -      vector<SNDFILE*> sndfiles;
 -      for (size_t i = 0; i < (size_t) N; ++i) {
 -              if (files[i].empty ()) {
 -                      sndfiles.push_back (0);
 -              } else {
 -                      SF_INFO info;
 -                      SNDFILE* s = sf_open (files[i].c_str(), SFM_READ, &info);
 -                      if (!s) {
 -                              throw DecodeError (_("could not open external audio file for reading"));
 -                      }
 -
 -                      if (info.channels != 1) {
 -                              throw DecodeError (_("external audio files must be mono"));
 -                      }
 -                      
 -                      sndfiles.push_back (s);
++      _done = 0;
 +      _remaining = _info.frames;
 +}
  
 -                      if (first) {
 -                              shared_ptr<SndfileStream> st (
 -                                      new SndfileStream (
 -                                              info.samplerate, av_get_default_channel_layout (N)
 -                                              )
 -                                      );
 -                              
 -                              _audio_streams.push_back (st);
 -                              _audio_stream = st;
 -                              frames = info.frames;
 -                              first = false;
 -                      } else {
 -                              if (info.frames != frames) {
 -                                      throw DecodeError (_("external audio files have differing lengths"));
 -                              }
 -                      }
 -              }
 +SndfileDecoder::~SndfileDecoder ()
 +{
 +      if (_sndfile) {
 +              sf_close (_sndfile);
        }
 -
 -      return sndfiles;
  }
  
  bool
  SndfileDecoder::pass ()
  {
 -      sf_count_t frames;
 -      vector<SNDFILE*> sndfiles = open_files (frames);
 -      if (sndfiles.empty()) {
 -              return true;
 -      }
 -
        /* Do things in half second blocks as I think there may be limits
           to what FFmpeg (and in particular the resampler) can cope with.
        */
 -      sf_count_t const block = _audio_stream->sample_rate() / 2;
 -      shared_ptr<AudioBuffers> audio (new AudioBuffers (_audio_stream->channels(), block));
 -      sf_count_t done = 0;
 -      while (frames > 0) {
 -              sf_count_t const this_time = min (block, frames);
 -              for (size_t i = 0; i < sndfiles.size(); ++i) {
 -                      if (!sndfiles[i]) {
 -                              audio->make_silent (i);
 -                      } else {
 -                              sf_read_float (sndfiles[i], audio->data(i), block);
 -                      }
 -              }
 -
 -              audio->set_frames (this_time);
 -              Audio (audio, double(done) / _audio_stream->sample_rate());
 -              done += this_time;
 -              frames -= this_time;
 -      }
 -
 -      close_files (sndfiles);
 -
 -      return true;
 -}
 -
 -void
 -SndfileDecoder::close_files (vector<SNDFILE*> const & sndfiles)
 -{
 -      for (size_t i = 0; i < sndfiles.size(); ++i) {
 -              sf_close (sndfiles[i]);
 -      }
 -}
 -
 -shared_ptr<SndfileStream>
 -SndfileStream::create ()
 -{
 -      return shared_ptr<SndfileStream> (new SndfileStream);
 -}
 -
 -shared_ptr<SndfileStream>
 -SndfileStream::create (string t, optional<int> v)
 -{
 -      if (!v) {
 -              /* version < 1; no type in the string, and there's only FFmpeg streams anyway */
 -              return shared_ptr<SndfileStream> ();
 -      }
 -
 -      stringstream s (t);
 -      string type;
 -      s >> type;
 -      if (type != N_("external")) {
 -              return shared_ptr<SndfileStream> ();
 -      }
 -
 -      return shared_ptr<SndfileStream> (new SndfileStream (t, v));
 +      sf_count_t const block = _sndfile_content->audio_frame_rate() / 2;
 +      sf_count_t const this_time = min (block, _remaining);
 +      
 +      shared_ptr<AudioBuffers> audio (new AudioBuffers (_sndfile_content->audio_channels(), this_time));
 +      sf_read_float (_sndfile, audio->data(0), this_time);
 +      audio->set_frames (this_time);
-       Audio (audio);
++      Audio (audio, double(_done) / audio_frame_rate());
++      _done += this_time;
 +      _remaining -= this_time;
 +
 +      return (_remaining == 0);
  }
  
 -SndfileStream::SndfileStream (string t, optional<int> v)
 +int
 +SndfileDecoder::audio_channels () const
  {
 -      assert (v);
 -
 -      stringstream s (t);
 -      string type;
 -      s >> type >> _sample_rate >> _channel_layout;
 +      return _info.channels;
  }
  
 -SndfileStream::SndfileStream ()
 +ContentAudioFrame
 +SndfileDecoder::audio_length () const
  {
 -
 +      return _info.frames;
  }
  
 -string
 -SndfileStream::to_string () const
 +int
 +SndfileDecoder::audio_frame_rate () const
  {
 -      return String::compose (N_("external %1 %2"), _sample_rate, _channel_layout);
 +      return _info.samplerate;
  }
index 2900afea0b3f258aaa5c1031328a716b800f9bef,e16eab6731e2f23d4bc32e9f4e6dfd1a8c2fced7..b999a66d15f40644bb0313614c5d9c771e7b17cf
  #include <sndfile.h>
  #include "decoder.h"
  #include "audio_decoder.h"
 -#include "stream.h"
  
 -class SndfileStream : public AudioStream
 -{
 -public:
 -      SndfileStream (int sample_rate, int64_t layout)
 -              : AudioStream (sample_rate, layout)
 -      {}
 -                             
 -      std::string to_string () const;
 -
 -      static boost::shared_ptr<SndfileStream> create ();
 -      static boost::shared_ptr<SndfileStream> create (std::string t, boost::optional<int> v);
 -
 -private:
 -      friend class stream_test;
 -      
 -      SndfileStream ();
 -      SndfileStream (std::string t, boost::optional<int> v);
 -};
 +class SndfileContent;
  
  class SndfileDecoder : public AudioDecoder
  {
  public:
 -      SndfileDecoder (boost::shared_ptr<Film>, DecodeOptions);
 +      SndfileDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const SndfileContent>);
 +      ~SndfileDecoder ();
  
        bool pass ();
  
 +      int audio_channels () const;
 +      ContentAudioFrame audio_length () const;
 +      int audio_frame_rate () const;
 +
  private:
 -      std::vector<SNDFILE*> open_files (sf_count_t &);
 -      void close_files (std::vector<SNDFILE*> const &);
 +      SNDFILE* open_file (sf_count_t &);
 +      void close_file (SNDFILE*);
 +
 +      boost::shared_ptr<const SndfileContent> _sndfile_content;
 +      SNDFILE* _sndfile;
 +      SF_INFO _info;
++      ContentAudioFrame _done;
 +      ContentAudioFrame _remaining;
  };
diff --combined src/lib/transcoder.cc
index 2a8ce50448c1ed31dc778767d702561e71d63522,23fb5b7880731789fdb82976fa7cc35c40e0f3bb..ea3f27ad8f94f10175e1b6e81237b5e1d6059c12
  #include <boost/signals2.hpp>
  #include "transcoder.h"
  #include "encoder.h"
 -#include "decoder_factory.h"
  #include "film.h"
  #include "matcher.h"
  #include "delay_line.h"
 -#include "options.h"
  #include "gain.h"
  #include "video_decoder.h"
  #include "audio_decoder.h"
 +#include "player.h"
  
  using std::string;
  using boost::shared_ptr;
@@@ -42,70 -43,72 +42,57 @@@ using boost::dynamic_pointer_cast
  
  /** Construct a transcoder using a Decoder that we create and a supplied Encoder.
   *  @param f Film that we are transcoding.
 - *  @param o Decode options.
   *  @param j Job that we are running under, or 0.
   *  @param e Encoder to use.
   */
 -Transcoder::Transcoder (shared_ptr<Film> f, DecodeOptions o, Job* j, shared_ptr<Encoder> e)
 +Transcoder::Transcoder (shared_ptr<Film> f, shared_ptr<Job> j)
        : _job (j)
 -      , _encoder (e)
 -      , _decoders (decoder_factory (f, o))
 +      , _player (f->player ())
 +      , _encoder (new Encoder (f))
  {
-       if (f->has_audio ()) {
-               _matcher.reset (new Matcher (f->log(), f->audio_frame_rate(), f->video_frame_rate()));
-               _delay_line.reset (new DelayLine (f->log(), f->audio_channels(), f->audio_delay() * f->audio_frame_rate() / 1000));
-               _gain.reset (new Gain (f->log(), f->audio_gain()));
-       }
 -      assert (_encoder);
 -
 -      shared_ptr<AudioStream> st = f->audio_stream();
 -      _matcher.reset (new Matcher (f->log(), st->sample_rate(), f->source_frame_rate()));
 -      _delay_line.reset (new DelayLine (f->log(), f->audio_delay() / 1000.0f));
++      _matcher.reset (new Matcher (f->log(), f->audio_frame_rate(), f->video_frame_rate()));
++      _delay_line.reset (new DelayLine (f->log(), f->audio_delay() * f->audio_frame_rate() / 1000));
+       _gain.reset (new Gain (f->log(), f->audio_gain()));
  
 -      /* Set up the decoder to use the film's set streams */
 -      _decoders.video->set_subtitle_stream (f->subtitle_stream ());
 -      _decoders.audio->set_audio_stream (f->audio_stream ());
 +      if (!f->with_subtitles ()) {
 +              _player->disable_subtitles ();
 +      }
  
-       if (_matcher) {
-               _player->connect_video (_matcher);
-               _matcher->connect_video (_encoder);
-       } else {
-               _player->connect_video (_encoder);
-       }
 -      _decoders.video->connect_video (_delay_line);
++      _player->connect_video (_delay_line);
+       _delay_line->connect_video (_matcher);
+       _matcher->connect_video (_encoder);
        
-       if (_matcher && _delay_line && f->has_audio ()) {
-               _player->connect_audio (_delay_line);
-               _delay_line->connect_audio (_matcher);
-               _matcher->connect_audio (_gain);
-               _gain->connect_audio (_encoder);
-       }
 -      _decoders.audio->connect_audio (_delay_line);
++      _player->connect_audio (_delay_line);
+       _delay_line->connect_audio (_matcher);
+       _matcher->connect_audio (_gain);
+       _gain->connect_audio (_encoder);
  }
  
 -/** Run the decoder, passing its output to the encoder, until the decoder
 - *  has no more data to present.
 - */
  void
  Transcoder::go ()
  {
        _encoder->process_begin ();
 -      try {
 -              bool done[2] = { false, false };
 -              
 -              while (1) {
 -                      if (!done[0]) {
 -                              done[0] = _decoders.video->pass ();
 -                              if (_job) {
 -                                      _decoders.video->set_progress (_job);
 -                              }
 -                      }
 -
 -                      if (!done[1] && _decoders.audio && dynamic_pointer_cast<Decoder> (_decoders.audio) != dynamic_pointer_cast<Decoder> (_decoders.video)) {
 -                              done[1] = _decoders.audio->pass ();
 -                      } else {
 -                              done[1] = true;
 -                      }
 -
 -                      if (done[0] && done[1]) {
 -                              break;
 -                      }
 +      while (1) {
 +              if (_player->pass ()) {
 +                      break;
                }
 -              
 -      } catch (...) {
 -              _encoder->process_end ();
 -              throw;
 +              _player->set_progress (_job);
        }
 -      
 +
-       if (_delay_line) {
-               _delay_line->process_end ();
-       }
-       if (_matcher) {
-               _matcher->process_end ();
-       }
-       if (_gain) {
-               _gain->process_end ();
-       }
+       _delay_line->process_end ();
+       _matcher->process_end ();
+       _gain->process_end ();
        _encoder->process_end ();
  }
 +
 +float
 +Transcoder::current_encoding_rate () const
 +{
 +      return _encoder->current_encoding_rate ();
 +}
 +
 +int
 +Transcoder::video_frames_out () const
 +{
 +      return _encoder->video_frames_out ();
 +}
diff --combined src/lib/util.cc
index ad08c6ab47fed8c022f4bd2b26df04e0c4b996d2,e43b598ab3ef3e635baac415cce0ad7f511c47fd..56932720c24a1c1f777a21cd11f08641e2918368
@@@ -27,7 -27,7 +27,7 @@@
  #include <iostream>
  #include <fstream>
  #include <climits>
 -#ifdef DVDOMATIC_POSIX
 +#ifdef DCPOMATIC_POSIX
  #include <execinfo.h>
  #include <cxxabi.h>
  #endif
@@@ -63,26 -63,8 +63,26 @@@ extern "C" 
  
  #include "i18n.h"
  
 -using namespace std;
 -using namespace boost;
 +using std::string;
 +using std::stringstream;
 +using std::setfill;
 +using std::ostream;
 +using std::endl;
 +using std::vector;
 +using std::hex;
 +using std::setw;
 +using std::ifstream;
 +using std::ios;
 +using std::min;
 +using std::max;
 +using std::list;
 +using std::multimap;
 +using std::istream;
 +using std::numeric_limits;
 +using std::pair;
 +using boost::shared_ptr;
 +using boost::thread;
 +using boost::lexical_cast;
  using libdcp::Size;
  
  thread::id ui_thread;
@@@ -148,7 -130,7 +148,7 @@@ seconds_to_approximate_hms (int s
        return ap.str ();
  }
  
 -#ifdef DVDOMATIC_POSIX
 +#ifdef DCPOMATIC_POSIX
  /** @param l Mangled C++ identifier.
   *  @return Demangled version.
   */
@@@ -247,11 -229,11 +247,11 @@@ seconds (struct timeval t
        return t.tv_sec + (double (t.tv_usec) / 1e6);
  }
  
 -/** Call the required functions to set up DVD-o-matic's static arrays, etc.
 +/** Call the required functions to set up DCP-o-matic's static arrays, etc.
   *  Must be called from the UI thread, if there is one.
   */
  void
 -dvdomatic_setup ()
 +dcpomatic_setup ()
  {
        avfilter_register_all ();
        
        Filter::setup_filters ();
        SoundProcessor::setup_sound_processors ();
  
 -      ui_thread = this_thread::get_id ();
 +      ui_thread = boost::this_thread::get_id ();
  }
  
 -#ifdef DVDOMATIC_WINDOWS
 +#ifdef DCPOMATIC_WINDOWS
  boost::filesystem::path
  mo_path ()
  {
  #endif
  
  void
 -dvdomatic_setup_i18n (string lang)
 +dcpomatic_setup_i18n (string lang)
  {
 -#ifdef DVDOMATIC_POSIX
 +#ifdef DCPOMATIC_POSIX
        lang += ".UTF8";
  #endif
  
        }
  
        setlocale (LC_ALL, "");
 -      textdomain ("libdvdomatic");
 +      textdomain ("libdcpomatic");
  
 -#ifdef DVDOMATIC_WINDOWS
 -      bindtextdomain ("libdvdomatic", mo_path().string().c_str());
 -      bind_textdomain_codeset ("libdvdomatic", "UTF8");
 +#ifdef DCPOMATIC_WINDOWS
 +      bindtextdomain ("libdcpomatic", mo_path().string().c_str());
 +      bind_textdomain_codeset ("libdcpomatic", "UTF8");
  #endif        
  
 -#ifdef DVDOMATIC_POSIX
 -      bindtextdomain ("libdvdomatic", POSIX_LOCALE_PREFIX);
 +#ifdef DCPOMATIC_POSIX
 +      bindtextdomain ("libdcpomatic", POSIX_LOCALE_PREFIX);
  #endif
  }
  
@@@ -366,11 -348,11 +366,11 @@@ md5_digest (void const * data, int size
   *  @return MD5 digest of file's contents.
   */
  string
 -md5_digest (string file)
 +md5_digest (boost::filesystem::path file)
  {
 -      ifstream f (file.c_str(), ios::binary);
 +      ifstream f (file.string().c_str(), ios::binary);
        if (!f.good ()) {
 -              throw OpenFileError (file);
 +              throw OpenFileError (file.string());
        }
        
        f.seekg (0, ios::end);
@@@ -495,6 -477,16 +495,6 @@@ dcp_audio_sample_rate (int fs
        return 96000;
  }
  
 -bool operator== (Crop const & a, Crop const & b)
 -{
 -      return (a.left == b.left && a.right == b.right && a.top == b.top && a.bottom == b.bottom);
 -}
 -
 -bool operator!= (Crop const & a, Crop const & b)
 -{
 -      return !(a == b);
 -}
 -
  /** @param index Colour LUT index.
   *  @return Human-readable name.
   */
@@@ -517,16 -509,16 +517,16 @@@ Socket::Socket (int timeout
        , _socket (_io_service)
        , _timeout (timeout)
  {
 -      _deadline.expires_at (posix_time::pos_infin);
 +      _deadline.expires_at (boost::posix_time::pos_infin);
        check ();
  }
  
  void
  Socket::check ()
  {
 -      if (_deadline.expires_at() <= asio::deadline_timer::traits_type::now ()) {
 +      if (_deadline.expires_at() <= boost::asio::deadline_timer::traits_type::now ()) {
                _socket.close ();
 -              _deadline.expires_at (posix_time::pos_infin);
 +              _deadline.expires_at (boost::posix_time::pos_infin);
        }
  
        _deadline.async_wait (boost::bind (&Socket::check, this));
   *  @param endpoint End-point to connect to.
   */
  void
 -Socket::connect (asio::ip::basic_resolver_entry<asio::ip::tcp> const & endpoint)
 +Socket::connect (boost::asio::ip::basic_resolver_entry<boost::asio::ip::tcp> const & endpoint)
  {
 -      _deadline.expires_from_now (posix_time::seconds (_timeout));
 -      system::error_code ec = asio::error::would_block;
 -      _socket.async_connect (endpoint, lambda::var(ec) = lambda::_1);
 +      _deadline.expires_from_now (boost::posix_time::seconds (_timeout));
 +      boost::system::error_code ec = boost::asio::error::would_block;
 +      _socket.async_connect (endpoint, boost::lambda::var(ec) = boost::lambda::_1);
        do {
                _io_service.run_one();
 -      } while (ec == asio::error::would_block);
 +      } while (ec == boost::asio::error::would_block);
  
        if (ec || !_socket.is_open ()) {
                throw NetworkError (_("connect timed out"));
  void
  Socket::write (uint8_t const * data, int size)
  {
 -      _deadline.expires_from_now (posix_time::seconds (_timeout));
 -      system::error_code ec = asio::error::would_block;
 +      _deadline.expires_from_now (boost::posix_time::seconds (_timeout));
 +      boost::system::error_code ec = boost::asio::error::would_block;
  
 -      asio::async_write (_socket, asio::buffer (data, size), lambda::var(ec) = lambda::_1);
 +      boost::asio::async_write (_socket, boost::asio::buffer (data, size), boost::lambda::var(ec) = boost::lambda::_1);
        
        do {
                _io_service.run_one ();
 -      } while (ec == asio::error::would_block);
 +      } while (ec == boost::asio::error::would_block);
  
        if (ec) {
                throw NetworkError (ec.message ());
@@@ -585,14 -577,14 +585,14 @@@ Socket::write (uint32_t v
  void
  Socket::read (uint8_t* data, int size)
  {
 -      _deadline.expires_from_now (posix_time::seconds (_timeout));
 -      system::error_code ec = asio::error::would_block;
 +      _deadline.expires_from_now (boost::posix_time::seconds (_timeout));
 +      boost::system::error_code ec = boost::asio::error::would_block;
  
 -      asio::async_read (_socket, asio::buffer (data, size), lambda::var(ec) = lambda::_1);
 +      boost::asio::async_read (_socket, boost::asio::buffer (data, size), boost::lambda::var(ec) = boost::lambda::_1);
  
        do {
                _io_service.run_one ();
 -      } while (ec == asio::error::would_block);
 +      } while (ec == boost::asio::error::would_block);
        
        if (ec) {
                throw NetworkError (ec.message ());
@@@ -869,39 -861,37 +869,39 @@@ AudioBuffers::move (int from, int to, i
        }
  }
  
 +/** Add data from from `from', `from_channel' to our channel `to_channel' */
 +void
 +AudioBuffers::accumulate (shared_ptr<AudioBuffers> from, int from_channel, int to_channel)
 +{
 +      int const N = frames ();
 +      assert (from->frames() == N);
 +
 +      float* s = from->data (from_channel);
 +      float* d = _data[to_channel];
 +
 +      for (int i = 0; i < N; ++i) {
 +              *d++ += *s++;
 +      }
 +}
 +
  /** Trip an assert if the caller is not in the UI thread */
  void
  ensure_ui_thread ()
  {
 -      assert (this_thread::get_id() == ui_thread);
 +      assert (boost::this_thread::get_id() == ui_thread);
  }
  
 -/** @param v Source video frame.
 +/** @param v Content video frame.
   *  @param audio_sample_rate Source audio sample rate.
   *  @param frames_per_second Number of video frames per second.
   *  @return Equivalent number of audio frames for `v'.
   */
  int64_t
 -video_frames_to_audio_frames (SourceFrame v, float audio_sample_rate, float frames_per_second)
 +video_frames_to_audio_frames (ContentVideoFrame v, float audio_sample_rate, float frames_per_second)
  {
        return ((int64_t) v * audio_sample_rate / frames_per_second);
  }
  
 -/** @param f Filename.
 - *  @return true if this file is a still image, false if it is something else.
 - */
 -bool
 -still_image_file (string f)
 -{
 -      string ext = boost::filesystem::path(f).extension().string();
 -
 -      transform (ext.begin(), ext.end(), ext.begin(), ::tolower);
 -      
 -      return (ext == N_(".tif") || ext == N_(".tiff") || ext == N_(".jpg") || ext == N_(".jpeg") || ext == N_(".png") || ext == N_(".bmp"));
 -}
 -
  /** @return A pair containing CPU model name and the number of processors */
  pair<string, int>
  cpu_info ()
        pair<string, int> info;
        info.second = 0;
        
 -#ifdef DVDOMATIC_POSIX
 +#ifdef DCPOMATIC_POSIX
        ifstream f (N_("/proc/cpuinfo"));
        while (f.good ()) {
                string l;
@@@ -948,6 -938,58 +948,6 @@@ audio_channel_name (int c
        return channels[c];
  }
  
 -AudioMapping::AudioMapping (int c)
 -      : _source_channels (c)
 -{
 -
 -}
 -
 -optional<libdcp::Channel>
 -AudioMapping::source_to_dcp (int c) const
 -{
 -      if (c >= _source_channels) {
 -              return optional<libdcp::Channel> ();
 -      }
 -
 -      if (_source_channels == 1) {
 -              /* mono sources to centre */
 -              return libdcp::CENTRE;
 -      }
 -      
 -      return static_cast<libdcp::Channel> (c);
 -}
 -
 -optional<int>
 -AudioMapping::dcp_to_source (libdcp::Channel c) const
 -{
 -      if (_source_channels == 1) {
 -              if (c == libdcp::CENTRE) {
 -                      return 0;
 -              } else {
 -                      return optional<int> ();
 -              }
 -      }
 -
 -      if (static_cast<int> (c) >= _source_channels) {
 -              return optional<int> ();
 -      }
 -      
 -      return static_cast<int> (c);
 -}
 -
 -int
 -AudioMapping::dcp_channels () const
 -{
 -      if (_source_channels == 1) {
 -              /* The source is mono, so to put the mono channel into
 -                 the centre we need to generate a 5.1 soundtrack.
 -              */
 -              return 6;
 -      }
 -
 -      return _source_channels;
 -}
 -
  FrameRateConversion::FrameRateConversion (float source, int dcp)
        : skip (false)
        , repeat (false)
                }
        }
  }
+ LocaleGuard::LocaleGuard ()
+       : _old (0)
+ {
+       char const * old = setlocale (LC_NUMERIC, 0);
+         if (old) {
+                 _old = strdup (old);
+                 if (strcmp (_old, "POSIX")) {
+                         setlocale (LC_NUMERIC, "POSIX");
+                 }
+         }
+ }
+ LocaleGuard::~LocaleGuard ()
+ {
+       setlocale (LC_NUMERIC, _old);
+       free (_old);
+ }
diff --combined src/lib/util.h
index 065801a889b3888769794522d80c094b91782eff,31d0fc96707fb976759fed42e69b51225a845389..02cc742aa52f62a8fac1a467d1c7a44b310f8625
@@@ -22,8 -22,8 +22,8 @@@
   *  @brief Some utility functions and classes.
   */
  
 -#ifndef DVDOMATIC_UTIL_H
 -#define DVDOMATIC_UTIL_H
 +#ifndef DCPOMATIC_UTIL_H
 +#define DCPOMATIC_UTIL_H
  
  #include <string>
  #include <vector>
@@@ -37,9 -37,8 +37,9 @@@ extern "C" 
  #include <libavfilter/avfilter.h>
  }
  #include "compose.hpp"
 +#include "types.h"
  
 -#ifdef DVDOMATIC_DEBUG
 +#ifdef DCPOMATIC_DEBUG
  #define TIMING(...) _film->log()->microsecond_log (String::compose (__VA_ARGS__), Log::TIMING);
  #else
  #define TIMING(...)
@@@ -55,17 -54,19 +55,17 @@@ extern std::string seconds_to_approxima
  extern void stacktrace (std::ostream &, int);
  extern std::string dependency_version_summary ();
  extern double seconds (struct timeval);
 -extern void dvdomatic_setup ();
 -extern void dvdomatic_setup_i18n (std::string);
 +extern void dcpomatic_setup ();
 +extern void dcpomatic_setup_i18n (std::string);
  extern std::vector<std::string> split_at_spaces_considering_quotes (std::string);
 -extern std::string md5_digest (std::string);
 +extern std::string md5_digest (boost::filesystem::path);
  extern std::string md5_digest (void const *, int);
  extern void ensure_ui_thread ();
  extern std::string audio_channel_name (int);
 -#ifdef DVDOMATIC_WINDOWS
 +#ifdef DCPOMATIC_WINDOWS
  extern boost::filesystem::path mo_path ();
  #endif
  
 -typedef int SourceFrame;
 -
  struct FrameRateConversion
  {
        FrameRateConversion (float, int);
  
  int best_dcp_frame_rate (float);
  
 -enum ContentType {
 -      STILL, ///< content is still images
 -      VIDEO  ///< content is a video
 -};
 -
 -/** @struct Crop
 - *  @brief A description of the crop of an image or video.
 - */
 -struct Crop
 -{
 -      Crop () : left (0), right (0), top (0), bottom (0) {}
 -
 -      /** Number of pixels to remove from the left-hand side */
 -      int left;
 -      /** Number of pixels to remove from the right-hand side */
 -      int right;
 -      /** Number of pixels to remove from the top */
 -      int top;
 -      /** Number of pixels to remove from the bottom */
 -      int bottom;
 -};
 -
 -extern bool operator== (Crop const & a, Crop const & b);
 -extern bool operator!= (Crop const & a, Crop const & b);
 -
 -/** @struct Position
 - *  @brief A position.
 - */
 -struct Position
 -{
 -      Position ()
 -              : x (0)
 -              , y (0)
 -      {}
 -
 -      Position (int x_, int y_)
 -              : x (x_)
 -              , y (y_)
 -      {}
 -
 -      /** x coordinate */
 -      int x;
 -      /** y coordinate */
 -      int y;
 -};
 -
 -/** @struct Rect
 - *  @brief A rectangle.
 - */
 -struct Rect
 -{
 -      Rect ()
 -              : x (0)
 -              , y (0)
 -              , width (0)
 -              , height (0)
 -      {}
 -
 -      Rect (int x_, int y_, int w_, int h_)
 -              : x (x_)
 -              , y (y_)
 -              , width (w_)
 -              , height (h_)
 -      {}
 -
 -      int x;
 -      int y;
 -      int width;
 -      int height;
 -
 -      Position position () const {
 -              return Position (x, y);
 -      }
 -
 -      libdcp::Size size () const {
 -              return libdcp::Size (width, height);
 -      }
 -
 -      Rect intersection (Rect const & other) const;
 -};
 -
  extern std::string crop_string (Position, libdcp::Size);
  extern int dcp_audio_sample_rate (int);
  extern std::string colour_lut_index_to_name (int index);
@@@ -117,7 -199,7 +117,7 @@@ extern std::string get_optional_string 
  
  /** @class Socket
   *  @brief A class to wrap a boost::asio::ip::tcp::socket with some things
 - *  that are useful for DVD-o-matic.
 + *  that are useful for DCP-o-matic.
   *
   *  This class wraps some things that I could not work out how to do with boost;
   *  most notably, sync read/write calls with timeouts.
@@@ -182,7 -264,6 +182,7 @@@ public
  
        void copy_from (AudioBuffers* from, int frames_to_copy, int read_offset, int write_offset);
        void move (int from, int to, int frames);
 +      void accumulate (boost::shared_ptr<AudioBuffers>, int, int);
  
  private:
        /** Number of channels */
        float** _data;
  };
  
 -class AudioMapping
 -{
 -public:
 -      AudioMapping (int);
 -
 -      boost::optional<libdcp::Channel> source_to_dcp (int c) const;
 -      boost::optional<int> dcp_to_source (libdcp::Channel c) const;
 -      int dcp_channels () const;
 -
 -private:
 -      int _source_channels;
 -};
 -
 -extern int64_t video_frames_to_audio_frames (SourceFrame v, float audio_sample_rate, float frames_per_second);
 -extern bool still_image_file (std::string);
 +extern int64_t video_frames_to_audio_frames (ContentVideoFrame v, float audio_sample_rate, float frames_per_second);
  extern std::pair<std::string, int> cpu_info ();
  
+ class LocaleGuard
+ {
+ public:
+       LocaleGuard ();
+       ~LocaleGuard ();
+       
+ private:
+       char* _old;
+ };
  #endif
  
diff --combined src/lib/video_decoder.cc
index 99d711693e44fa942b268375f7f2f6bfda955c3e,16a076698eff8c652061a693dc95960f38e0cf9e..fd82384416b6166a461b0f9d9d43e1430ae2695b
  #include "film.h"
  #include "image.h"
  #include "log.h"
 -#include "options.h"
  #include "job.h"
  
  #include "i18n.h"
  
+ using std::cout;
  using boost::shared_ptr;
  using boost::optional;
  
 -VideoDecoder::VideoDecoder (shared_ptr<Film> f, DecodeOptions o)
 -      : Decoder (f, o)
 +VideoDecoder::VideoDecoder (shared_ptr<const Film> f)
 +      : Decoder (f)
        , _video_frame (0)
 -      , _last_source_time (0)
 +      , _last_content_time (0)
  {
  
  }
   *  @param t Time of the frame within the source, in seconds.
   */
  void
- VideoDecoder::emit_video (shared_ptr<Image> image, double t)
+ VideoDecoder::emit_video (shared_ptr<Image> image, bool same, double t)
  {
        shared_ptr<Subtitle> sub;
        if (_timed_subtitle && _timed_subtitle->displayed_at (t)) {
                sub = _timed_subtitle->subtitle ();
        }
  
-       signal_video (image, false, sub, t);
- }
- bool
- VideoDecoder::have_last_video () const
- {
-       return _last_image;
- }
- /** Called by subclasses to repeat the last video frame that we
-  *  passed to emit_video().  If emit_video hasn't yet been called,
-  *  we will generate a black frame.
-  */
- void
- VideoDecoder::repeat_last_video (double t)
- {
-       if (!_last_image) {
-               _last_image.reset (new SimpleImage (pixel_format(), native_size(), true));
-               _last_image->make_black ();
-       }
-       signal_video (_last_image, true, _last_subtitle, t);
- }
- /** Emit our signal to say that some video data is ready.
-  *  @param image Video frame.
-  *  @param same true if `image' is the same as the last one we emitted.
-  *  @param sub Subtitle for this frame, or 0.
-  */
- void
- VideoDecoder::signal_video (shared_ptr<Image> image, bool same, shared_ptr<Subtitle> sub, double t)
- {
 +      TIMING (N_("Decoder emits %1"), _video_frame);
-       Video (image, same, sub);
+       Video (image, same, sub, t);
        ++_video_frame;
 -      
 -      _last_source_time = t;
 +
-       _last_image = image;
-       _last_subtitle = sub;
 +      _last_content_time = t;
  }
  
  /** Set up the current subtitle.  This will be put onto frames that
@@@ -107,12 -74,21 +74,12 @@@ VideoDecoder::emit_subtitle (shared_ptr
        }
  }
  
 -/** Set which stream of subtitles we should use from our source.
 - *  @param s Stream to use.
 - */
 -void
 -VideoDecoder::set_subtitle_stream (shared_ptr<SubtitleStream> s)
 -{
 -      _subtitle_stream = s;
 -}
 -
  void
  VideoDecoder::set_progress (Job* j) const
  {
        assert (j);
 -      
 -      if (_film->length()) {
 -              j->set_progress (float (_video_frame) / _film->length().get());
 +
 +      if (_film->video_length()) {
 +              j->set_progress (float (_video_frame) / _film->video_length());
        }
  }
diff --combined src/lib/video_decoder.h
index 23817c05536f4c568cef8cdc7a8358ec3991c074,6e4fd48c0019710a2632e921d35645efa119c239..0b05b2f7143690237754d810d8557d8e80cb9d77
  
  */
  
 -#ifndef DVDOMATIC_VIDEO_DECODER_H
 -#define DVDOMATIC_VIDEO_DECODER_H
 +#ifndef DCPOMATIC_VIDEO_DECODER_H
 +#define DCPOMATIC_VIDEO_DECODER_H
  
  #include "video_source.h"
 -#include "stream.h"
  #include "decoder.h"
  
- class VideoDecoder : public VideoSource, public virtual Decoder
 +class VideoContent;
 +
+ class VideoDecoder : public TimedVideoSource, public virtual Decoder
  {
  public:
 -      VideoDecoder (boost::shared_ptr<Film>, DecodeOptions);
 +      VideoDecoder (boost::shared_ptr<const Film>);
  
 -      /** @return video frames per second, or 0 if unknown */
 -      virtual float frames_per_second () const = 0;
 +      /** @return video frame rate second, or 0 if unknown */
 +      virtual float video_frame_rate () const = 0;
        /** @return native size in pixels */
        virtual libdcp::Size native_size () const = 0;
 -      /** @return length (in source video frames), according to our content's header */
 -      virtual SourceFrame length () const = 0;
 +      /** @return length according to our content's header */
 +      virtual ContentVideoFrame video_length () const = 0;
  
        virtual int time_base_numerator () const = 0;
        virtual int time_base_denominator () const = 0;
        virtual int sample_aspect_ratio_numerator () const = 0;
        virtual int sample_aspect_ratio_denominator () const = 0;
  
 -      virtual void set_subtitle_stream (boost::shared_ptr<SubtitleStream>);
 -
        void set_progress (Job *) const;
        
        int video_frame () const {
                return _video_frame;
        }
  
 -      boost::shared_ptr<SubtitleStream> subtitle_stream () const {
 -              return _subtitle_stream;
 -      }
 -
 -      std::vector<boost::shared_ptr<SubtitleStream> > subtitle_streams () const {
 -              return _subtitle_streams;
 -      }
 -
 -      double last_source_time () const {
 -              return _last_source_time;
 +      double last_content_time () const {
 +              return _last_content_time;
        }
  
  protected:
        
        virtual PixelFormat pixel_format () const = 0;
  
-       void emit_video (boost::shared_ptr<Image>, double);
+       void emit_video (boost::shared_ptr<Image>, bool, double);
        void emit_subtitle (boost::shared_ptr<TimedSubtitle>);
-       bool have_last_video () const;
-       void repeat_last_video (double);
  
 -      /** Subtitle stream to use when decoding */
 -      boost::shared_ptr<SubtitleStream> _subtitle_stream;
 -      /** Subtitle streams that this decoder's content has */
 -      std::vector<boost::shared_ptr<SubtitleStream> > _subtitle_streams;
 -
  private:
-       void signal_video (boost::shared_ptr<Image>, bool, boost::shared_ptr<Subtitle>, double);
        int _video_frame;
 -      double _last_source_time;
 +      double _last_content_time;
        
        boost::shared_ptr<TimedSubtitle> _timed_subtitle;
-       boost::shared_ptr<Image> _last_image;
-       boost::shared_ptr<Subtitle> _last_subtitle;
  };
  
  #endif
diff --combined src/lib/video_sink.h
index c68651005121c1e3afbe14c606f54eceddc61668,32c7f3b384f145cbb1bf83ba6de63294675fa253..167d3980eb020531457f8240ddc99803483f3343
@@@ -17,8 -17,8 +17,8 @@@
  
  */
  
 -#ifndef DVDOMATIC_VIDEO_SINK_H
 -#define DVDOMATIC_VIDEO_SINK_H
 +#ifndef DCPOMATIC_VIDEO_SINK_H
 +#define DCPOMATIC_VIDEO_SINK_H
  
  #include <boost/shared_ptr.hpp>
  #include "util.h"
@@@ -37,4 -37,16 +37,16 @@@ public
        virtual void process_video (boost::shared_ptr<Image> i, bool same, boost::shared_ptr<Subtitle> s) = 0;
  };
  
+ class TimedVideoSink
+ {
+ public:
+       /** Call with a frame of video.
+        *  @param i Video frame image.
+        *  @param same true if i is the same as last time we were called.
+        *  @param s A subtitle that should be on this frame, or 0.
+        *  @param t Source timestamp.
+        */
+       virtual void process_video (boost::shared_ptr<Image> i, bool same, boost::shared_ptr<Subtitle> s, double t) = 0;
+ };
  #endif
diff --combined src/lib/video_source.cc
index 1c4d6466cbd5c159bbcdd81de23745821b700080,af6f941fd1f3f00a2b6ea2055e879bd75ddbf337..ccb76f02018a45268ed74d22d1d3b344b53cfea5
  #include "video_sink.h"
  
  using boost::shared_ptr;
 +using boost::weak_ptr;
  using boost::bind;
  
 +static void
 +process_video_proxy (weak_ptr<VideoSink> sink, shared_ptr<Image> i, bool same, shared_ptr<Subtitle> s)
 +{
 +      shared_ptr<VideoSink> p = sink.lock ();
 +      if (p) {
 +              p->process_video (i, same, s);
 +      }
 +}
 +
  void
  VideoSource::connect_video (shared_ptr<VideoSink> s)
  {
 -      Video.connect (bind (&VideoSink::process_video, s, _1, _2, _3));
 +      /* If we bind, say, a Playlist (as the VideoSink) to a Decoder (which is owned
 +         by the Playlist) we create a cycle.  Use a weak_ptr to break it.
 +      */
 +      Video.connect (bind (process_video_proxy, boost::weak_ptr<VideoSink> (s), _1, _2, _3));
  }
+ void
+ TimedVideoSource::connect_video (shared_ptr<TimedVideoSink> s)
+ {
+       Video.connect (bind (&TimedVideoSink::process_video, s, _1, _2, _3, _4));
+ }
diff --combined src/lib/video_source.h
index e7c9805ef613d527d00f374d141b475ab36d9b4e,705b0023aec3037877a60e5e7af16db1a854d97e..d2aa045a715e858fa8854fc22a2ccda098aa7908
   *  @brief Parent class for classes which emit video data.
   */
  
 -#ifndef DVDOMATIC_VIDEO_SOURCE_H
 -#define DVDOMATIC_VIDEO_SOURCE_H
 +#ifndef DCPOMATIC_VIDEO_SOURCE_H
 +#define DCPOMATIC_VIDEO_SOURCE_H
  
  #include <boost/shared_ptr.hpp>
  #include <boost/signals2.hpp>
  #include "util.h"
  
  class VideoSink;
+ class TimedVideoSink;
  class Subtitle;
  class Image;
  
  /** @class VideoSource
-  *  @param A class that emits video data.
+  *  @param A class that emits video data without timestamps.
   */
  class VideoSource
  {
@@@ -49,4 -50,22 +50,22 @@@ public
        void connect_video (boost::shared_ptr<VideoSink>);
  };
  
+ /** @class TimedVideoSource
+  *  @param A class that emits video data with timestamps.
+  */
+ class TimedVideoSource
+ {
+ public:
+       /** Emitted when a video frame is ready.
+        *  First parameter is the video image.
+        *  Second parameter is true if the image is the same as the last one that was emitted.
+        *  Third parameter is either 0 or a subtitle that should be on this frame.
+        *  Fourth parameter is the source timestamp of this frame.
+        */
+       boost::signals2::signal<void (boost::shared_ptr<Image>, bool, boost::shared_ptr<Subtitle>, double)> Video;
+       void connect_video (boost::shared_ptr<TimedVideoSink>);
+ };
  #endif
diff --combined src/wx/film_viewer.cc
index cba19c07c2ba6d093d6e420d33f13f4803bc47e0,8508ec2a21beec19832255432e1cc118ef18550f..688be3bd06ab163158b785e65e7b0db61354d0a7
  #include "lib/format.h"
  #include "lib/util.h"
  #include "lib/job_manager.h"
 -#include "lib/options.h"
  #include "lib/subtitle.h"
  #include "lib/image.h"
  #include "lib/scaler.h"
  #include "lib/exceptions.h"
  #include "lib/examine_content_job.h"
  #include "lib/filter.h"
 +#include "lib/player.h"
 +#include "lib/video_content.h"
 +#include "lib/ffmpeg_content.h"
 +#include "lib/imagemagick_content.h"
  #include "film_viewer.h"
  #include "wx_util.h"
  #include "video_decoder.h"
@@@ -48,14 -45,16 +48,18 @@@ using std::max
  using std::cout;
  using std::list;
  using boost::shared_ptr;
 +using boost::dynamic_pointer_cast;
 +using boost::weak_ptr;
  using libdcp::Size;
  
  FilmViewer::FilmViewer (shared_ptr<Film> f, wxWindow* p)
        : wxPanel (p)
        , _panel (new wxPanel (this))
        , _slider (new wxSlider (this, wxID_ANY, 0, 0, 4096))
+       , _back_button (new wxButton (this, wxID_ANY, wxT("<")))
+       , _forward_button (new wxButton (this, wxID_ANY, wxT(">")))
+       , _frame (new wxStaticText (this, wxID_ANY, wxT("")))
+       , _timecode (new wxStaticText (this, wxID_ANY, wxT("")))
        , _play_button (new wxToggleButton (this, wxID_ANY, _("Play")))
        , _display_frame_x (0)
        , _got_frame (false)
        _v_sizer->Add (_panel, 1, wxEXPAND);
  
        wxBoxSizer* h_sizer = new wxBoxSizer (wxHORIZONTAL);
+       wxBoxSizer* time_sizer = new wxBoxSizer (wxVERTICAL);
+       time_sizer->Add (_frame, 0, wxEXPAND);
+       time_sizer->Add (_timecode, 0, wxEXPAND);
+       h_sizer->Add (_back_button, 0, wxALL, 2);
+       h_sizer->Add (time_sizer, 0, wxEXPAND);
+       h_sizer->Add (_forward_button, 0, wxALL, 2);
        h_sizer->Add (_play_button, 0, wxEXPAND);
        h_sizer->Add (_slider, 1, wxEXPAND);
  
        _v_sizer->Add (h_sizer, 0, wxEXPAND | wxALL, 6);
  
+       _frame->SetMinSize (wxSize (84, -1));
+       _back_button->SetMinSize (wxSize (32, -1));
+       _forward_button->SetMinSize (wxSize (32, -1));
        _panel->Connect (wxID_ANY, wxEVT_PAINT, wxPaintEventHandler (FilmViewer::paint_panel), 0, this);
        _panel->Connect (wxID_ANY, wxEVT_SIZE, wxSizeEventHandler (FilmViewer::panel_sized), 0, this);
        _slider->Connect (wxID_ANY, wxEVT_SCROLL_THUMBTRACK, wxScrollEventHandler (FilmViewer::slider_moved), 0, this);
@@@ -83,6 -94,8 +99,8 @@@
        _slider->Connect (wxID_ANY, wxEVT_SCROLL_PAGEDOWN, wxScrollEventHandler (FilmViewer::slider_moved), 0, this);
        _play_button->Connect (wxID_ANY, wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler (FilmViewer::play_clicked), 0, this);
        _timer.Connect (wxID_ANY, wxEVT_TIMER, wxTimerEventHandler (FilmViewer::timer), 0, this);
+       _back_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmViewer::back_clicked), 0, this);
+       _forward_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmViewer::forward_clicked), 0, this);
  
        set_film (f);
  
@@@ -100,24 -113,44 +118,22 @@@ FilmViewer::film_changed (Film::Propert
                update_from_raw ();
                break;
        case Film::CONTENT:
--      {
 -              DecodeOptions o;
 -              o.decode_audio = false;
 -              o.decode_subtitles = true;
 -              o.video_sync = false;
 -
 -              try {
 -                      _decoders = decoder_factory (_film, o);
 -              } catch (StringError& e) {
 -                      error_dialog (this, wxString::Format (_("Could not open content file (%s)"), std_to_wx(e.what()).data()));
 -                      return;
 -              }
 -              
 -              if (_decoders.video == 0) {
 -                      break;
 -              }
 -              _decoders.video->Video.connect (bind (&FilmViewer::process_video, this, _1, _2, _3, _4));
 -              _decoders.video->OutputChanged.connect (boost::bind (&FilmViewer::decoder_changed, this));
 -              _decoders.video->set_subtitle_stream (_film->subtitle_stream());
                calculate_sizes ();
                get_frame ();
                _panel->Refresh ();
 -              _slider->Show (_film->content_type() == VIDEO);
 -              _play_button->Show (_film->content_type() == VIDEO);
                _v_sizer->Layout ();
                break;
--      }
        case Film::WITH_SUBTITLES:
        case Film::SUBTITLE_OFFSET:
        case Film::SUBTITLE_SCALE:
 +              raw_to_display ();
 +              _panel->Refresh ();
 +              _panel->Update ();
 +              break;
        case Film::SCALER:
        case Film::FILTERS:
 -              update_from_raw ();
 -              break;
 -      case Film::SUBTITLE_STREAM:
 -              if (_decoders.video) {
 -                      _decoders.video->set_subtitle_stream (_film->subtitle_stream ());
 -              }
 +      case Film::CROP:
 +              update_from_decoder ();
                break;
        default:
                break;
@@@ -130,7 -163,7 +146,7 @@@ FilmViewer::set_film (shared_ptr<Film> 
        if (_film == f) {
                return;
        }
 -      
 +
        _film = f;
  
        _raw_frame.reset ();
                return;
        }
  
-       _player->disable_video_sync ();
 +      _player = f->player ();
 +      _player->disable_audio ();
-       _player->Video.connect (bind (&FilmViewer::process_video, this, _1, _2, _3));
 +      /* Don't disable subtitles here as we may need them, and it's nice to be able to turn them
 +         on and off without needing obtain a new Player.
 +      */
 +      
++      _player->Video.connect (bind (&FilmViewer::process_video, this, _1, _2, _3, _4));
 +      
        _film->Changed.connect (boost::bind (&FilmViewer::film_changed, this, _1));
 +      _film->ContentChanged.connect (boost::bind (&FilmViewer::film_content_changed, this, _1, _2));
  
        film_changed (Film::CONTENT);
        film_changed (Film::FORMAT);
        film_changed (Film::WITH_SUBTITLES);
        film_changed (Film::SUBTITLE_OFFSET);
        film_changed (Film::SUBTITLE_SCALE);
 -      film_changed (Film::SUBTITLE_STREAM);
  }
  
  void
 -FilmViewer::decoder_changed ()
 +FilmViewer::update_from_decoder ()
  {
 -      if (_decoders.video == 0 || _decoders.video->seek_to_last ()) {
 +      if (!_player || _player->seek (_player->last_video_time ())) {
                return;
        }
  
  void
  FilmViewer::timer (wxTimerEvent &)
  {
 -      if (!_film || !_decoders.video) {
 +      if (!_player) {
                return;
        }
        
  
        get_frame ();
  
 -      if (_film->length()) {
 -              int const new_slider_position = 4096 * _decoders.video->last_source_time() / (_film->length().get() / _film->source_frame_rate());
 +      if (_film->video_length()) {
 +              int const new_slider_position = 4096 * _player->last_video_time() / (_film->video_length() / _film->video_frame_rate());
                if (new_slider_position != _slider->GetValue()) {
                        _slider->SetValue (new_slider_position);
                }
@@@ -242,11 -266,11 +257,11 @@@ FilmViewer::paint_panel (wxPaintEvent &
  void
  FilmViewer::slider_moved (wxScrollEvent &)
  {
 -      if (!_film || !_film->length() || !_decoders.video) {
 +      if (!_film || !_player) {
                return;
        }
 -      
 -      if (_decoders.video->seek (_slider->GetValue() * _film->length().get() / (4096 * _film->source_frame_rate()))) {
 +
 +      if (_player->seek (_slider->GetValue() * _film->video_length() / (4096 * _film->video_frame_rate()))) {
                return;
        }
        
@@@ -284,7 -308,7 +299,7 @@@ FilmViewer::raw_to_display (
                return;
        }
  
 -      boost::shared_ptr<Image> input = _raw_frame;
 +      shared_ptr<Image> input = _raw_frame;
  
        pair<string, string> const s = Filter::ffmpeg_strings (_film->filters());
        if (!s.second.empty ()) {
                   when working out the scale that we are applying.
                */
  
 -              Size const cropped_size = _film->cropped_size (_film->size ());
 +              Size const cropped_size = _film->cropped_size (_film->video_size ());
  
                Rect tx = subtitle_transformed_area (
                        float (_film_size.width) / cropped_size.width,
  void
  FilmViewer::calculate_sizes ()
  {
 -      if (!_film) {
 +      if (!_film || !_player) {
                return;
        }
  
        Format const * format = _film->format ();
        
        float const panel_ratio = static_cast<float> (_panel_size.width) / _panel_size.height;
-       float const film_ratio = format ? format->container_ratio_as_float () : 1.78;
+       float const film_ratio = format ? format->container_ratio () : 1.78;
                        
        if (panel_ratio < film_ratio) {
                /* panel is less widscreen than the film; clamp width */
@@@ -369,14 -393,14 +384,14 @@@ FilmViewer::check_play_state (
        }
        
        if (_play_button->GetValue()) {
 -              _timer.Start (1000 / _film->source_frame_rate());
 +              _timer.Start (1000 / _film->video_frame_rate());
        } else {
                _timer.Stop ();
        }
  }
  
  void
- FilmViewer::process_video (shared_ptr<Image> image, bool, shared_ptr<Subtitle> sub)
+ FilmViewer::process_video (shared_ptr<Image> image, bool, shared_ptr<Subtitle> sub, double t)
  {
        _raw_frame = image;
        _raw_sub = sub;
        raw_to_display ();
  
        _got_frame = true;
 -      double const fps = _decoders.video->frames_per_second ();
++      double const fps = _film->video_frame_rate ();
+       _frame->SetLabel (wxString::Format (wxT("%d"), int (rint (t * fps))));
+       double w = t;
+       int const h = (w / 3600);
+       w -= h * 3600;
+       int const m = (w / 60);
+       w -= m * 60;
+       int const s = floor (w);
+       w -= s;
+       int const f = rint (w * fps);
+       _timecode->SetLabel (wxString::Format (wxT("%02d:%02d:%02d:%02d"), h, m, s, f));
  }
  
 +/** Get a new _raw_frame from the decoder and then do
 + *  raw_to_display ().
 + */
  void
  FilmViewer::get_frame ()
  {
        /* Clear our raw frame in case we don't get a new one */
        _raw_frame.reset ();
  
 -      if (_decoders.video == 0) {
 +      if (!_player) {
                _display_frame.reset ();
                return;
        }
 -      
 +
        try {
                _got_frame = false;
                while (!_got_frame) {
 -                      if (_decoders.video->pass ()) {
 +                      if (_player->pass ()) {
                                /* We didn't get a frame before the decoder gave up,
                                   so clear our display frame.
                                */
@@@ -438,12 -472,28 +466,38 @@@ FilmViewer::active_jobs_changed (bool a
        _play_button->Enable (!a);
  }
  
 -      if (!_decoders.video) {
 +void
 +FilmViewer::film_content_changed (weak_ptr<Content>, int p)
 +{
 +      if (p == VideoContentProperty::VIDEO_LENGTH) {
 +              /* Force an update to our frame */
 +              wxScrollEvent ev;
 +              slider_moved (ev);
 +      }
 +}
++
+ void
+ FilmViewer::back_clicked (wxCommandEvent &)
+ {
 -      _decoders.video->seek_back ();
++      if (!_player) {
+               return;
+       }
+       
 -      if (!_decoders.video) {
++      _player->seek_back ();
+       get_frame ();
+       _panel->Refresh ();
+       _panel->Update ();
+ }
+ void
+ FilmViewer::forward_clicked (wxCommandEvent &)
+ {
 -      _decoders.video->seek_forward ();
++      if (!_player) {
+               return;
+       }
++      _player->seek_forward ();
+       get_frame ();
+       _panel->Refresh ();
+       _panel->Update ();
+ }
diff --combined src/wx/film_viewer.h
index c81c65acd782d4deb26cb1e7c37c1c540e308955,a78c772a49858570f9dd0170e6cf8dd5a752460c..814a095af374753ac17a596651e9397211b0b5e0
@@@ -23,6 -23,7 +23,6 @@@
  
  #include <wx/wx.h>
  #include "lib/film.h"
 -#include "lib/decoder_factory.h"
  
  class wxToggleButton;
  class FFmpegPlayer;
@@@ -32,25 -33,6 +32,25 @@@ class Subtitle
  
  /** @class FilmViewer
   *  @brief A wx widget to view a preview of a Film.
 + *
 + *  The film takes the following path through the viewer:
 + *
 + *  1.  get_frame() asks our _player to decode some data.  If it does, process_video()
 + *      will be called.
 + *
 + *  2.  process_video() takes the image and subtitle from the decoder (_raw_frame and _raw_sub)
 + *      and calls raw_to_display().
 + * 
 + *  3.  raw_to_display() copies _raw_frame to _display_frame, processing it and scaling it.
 + *
 + *  4.  calling _panel->Refresh() and _panel->Update() results in paint_panel() being called;
 + *      this creates frame_bitmap from _display_frame and blits it to the display.  It also
 + *      blits the subtitle, if required.
 + *
 + * update_from_decoder() asks the player to re-emit its current frame on the next pass(), and then
 + * starts from step #1.
 + *
 + * update_from_raw() starts at step #3, then calls _panel->Refresh and _panel->Update.
   */
  class FilmViewer : public wxPanel
  {
@@@ -61,30 -43,35 +61,36 @@@ public
  
  private:
        void film_changed (Film::Property);
 +      void film_content_changed (boost::weak_ptr<Content>, int);
        void paint_panel (wxPaintEvent &);
        void panel_sized (wxSizeEvent &);
        void slider_moved (wxScrollEvent &);
        void play_clicked (wxCommandEvent &);
        void timer (wxTimerEvent &);
-       void process_video (boost::shared_ptr<Image>, bool, boost::shared_ptr<Subtitle>);
+       void process_video (boost::shared_ptr<Image>, bool, boost::shared_ptr<Subtitle>, double);
        void calculate_sizes ();
        void check_play_state ();
        void update_from_raw ();
 -      void decoder_changed ();
 +      void update_from_decoder ();
        void raw_to_display ();
        void get_frame ();
        void active_jobs_changed (bool);
+       void back_clicked (wxCommandEvent &);
+       void forward_clicked (wxCommandEvent &);
  
        boost::shared_ptr<Film> _film;
 +      boost::shared_ptr<Player> _player;
  
        wxSizer* _v_sizer;
        wxPanel* _panel;
        wxSlider* _slider;
+       wxButton* _back_button;
+       wxButton* _forward_button;
+       wxStaticText* _frame;
+       wxStaticText* _timecode;
        wxToggleButton* _play_button;
        wxTimer _timer;
  
 -      Decoders _decoders;
        boost::shared_ptr<Image> _raw_frame;
        boost::shared_ptr<Subtitle> _raw_sub;
        boost::shared_ptr<Image> _display_frame;
diff --combined test/test.cc
index a2c0de25048443675eb61dc55c4a6a80c8fdb953,b0b2cef7d057fe0e43167d1889c563f372c51f53..46fadd5703cb8eacb155b197337ba5ab41d4530a
@@@ -28,7 -28,6 +28,6 @@@
  #include "job_manager.h"
  #include "util.h"
  #include "exceptions.h"
- #include "delay_line.h"
  #include "image.h"
  #include "log.h"
  #include "dcp_video_frame.h"
  #include "scaler.h"
  #include "ffmpeg_decoder.h"
  #include "sndfile_decoder.h"
 +#include "dcp_content_type.h"
  #include "trimmer.h"
  #define BOOST_TEST_DYN_LINK
 -#define BOOST_TEST_MODULE dvdomatic_test
 +#define BOOST_TEST_MODULE dcpomatic_test
  #include <boost/test/unit_test.hpp>
  
  using std::string;
@@@ -91,7 -89,7 +90,7 @@@ new_test_film (string name
  BOOST_AUTO_TEST_CASE (make_black_test)
  {
        /* This needs to happen in the first test */
 -      dvdomatic_setup ();
 +      dcpomatic_setup ();
  
        libdcp::Size in_size (512, 512);
        libdcp::Size out_size (1024, 1024);
@@@ -193,7 -191,7 +192,7 @@@ BOOST_AUTO_TEST_CASE (film_metadata_tes
        BOOST_CHECK (f->filters ().empty());
  
        f->set_name ("fred");
 -      BOOST_CHECK_THROW (f->set_content ("jim"), OpenFileError);
 +//    BOOST_CHECK_THROW (f->set_content ("jim"), OpenFileError);
        f->set_dcp_content_type (DCPContentType::from_pretty_name ("Short"));
        f->set_format (Format::from_nickname ("Flat"));
        f->set_left_crop (1);
        f->set_filters (f_filters);
        f->set_trim_start (42);
        f->set_trim_end (99);
 -      f->set_dcp_ab (true);
 +      f->set_ab (true);
        f->write_metadata ();
  
        stringstream s;
        BOOST_CHECK_EQUAL (g_filters.back(), Filter::from_id ("unsharp"));
        BOOST_CHECK_EQUAL (g->trim_start(), 42);
        BOOST_CHECK_EQUAL (g->trim_end(), 99);
 -      BOOST_CHECK_EQUAL (g->dcp_ab(), true);
 +      BOOST_CHECK_EQUAL (g->ab(), true);
        
        g->write_metadata ();
        BOOST_CHECK_EQUAL (::system (s.str().c_str ()), 0);
  }
  
 -BOOST_AUTO_TEST_CASE (stream_test)
 -{
 -      FFmpegAudioStream a ("ffmpeg 4 44100 1 hello there world", boost::optional<int> (1));
 -      BOOST_CHECK_EQUAL (a.id(), 4);
 -      BOOST_CHECK_EQUAL (a.sample_rate(), 44100);
 -      BOOST_CHECK_EQUAL (a.channel_layout(), 1);
 -      BOOST_CHECK_EQUAL (a.name(), "hello there world");
 -      BOOST_CHECK_EQUAL (a.to_string(), "ffmpeg 4 44100 1 hello there world");
 -
 -      SndfileStream e ("external 44100 1", boost::optional<int> (1));
 -      BOOST_CHECK_EQUAL (e.sample_rate(), 44100);
 -      BOOST_CHECK_EQUAL (e.channel_layout(), 1);
 -      BOOST_CHECK_EQUAL (e.to_string(), "external 44100 1");
 -
 -      SubtitleStream s ("5 a b c", boost::optional<int> (1));
 -      BOOST_CHECK_EQUAL (s.id(), 5);
 -      BOOST_CHECK_EQUAL (s.name(), "a b c");
 -
 -      shared_ptr<AudioStream> ff = audio_stream_factory ("ffmpeg 4 44100 1 hello there world", boost::optional<int> (1));
 -      shared_ptr<FFmpegAudioStream> cff = dynamic_pointer_cast<FFmpegAudioStream> (ff);
 -      BOOST_CHECK (cff);
 -      BOOST_CHECK_EQUAL (cff->id(), 4);
 -      BOOST_CHECK_EQUAL (cff->sample_rate(), 44100);
 -      BOOST_CHECK_EQUAL (cff->channel_layout(), 1);
 -      BOOST_CHECK_EQUAL (cff->name(), "hello there world");
 -      BOOST_CHECK_EQUAL (cff->to_string(), "ffmpeg 4 44100 1 hello there world");
 -
 -      shared_ptr<AudioStream> fe = audio_stream_factory ("external 44100 1", boost::optional<int> (1));
 -      BOOST_CHECK_EQUAL (fe->sample_rate(), 44100);
 -      BOOST_CHECK_EQUAL (fe->channel_layout(), 1);
 -      BOOST_CHECK_EQUAL (fe->to_string(), "external 44100 1");
 -}
 -
  BOOST_AUTO_TEST_CASE (format_test)
  {
        Format::setup_formats ();
        
        Format const * f = Format::from_nickname ("Flat");
        BOOST_CHECK (f);
- //    BOOST_CHECK_EQUAL (f->ratio_as_integer(shared_ptr<const Film> ()), 185);
+       BOOST_CHECK_EQUAL (f->dcp_size().width, 1998);
+       BOOST_CHECK_EQUAL (f->dcp_size().height, 1080);
        
        f = Format::from_nickname ("Scope");
        BOOST_CHECK (f);
- //    BOOST_CHECK_EQUAL (f->ratio_as_integer(shared_ptr<const Film> ()), 239);
+       BOOST_CHECK_EQUAL (f->dcp_size().width, 2048);
+       BOOST_CHECK_EQUAL (f->dcp_size().height, 858);
  }
  
 -/* Test VariableFormat-based scaling of content */
 -BOOST_AUTO_TEST_CASE (scaling_test)
 -{
 -      shared_ptr<Film> film (new Film (test_film_dir ("scaling_test").string(), false));
 -
 -      /* 4:3 ratio */
 -      film->set_size (libdcp::Size (320, 240));
 -
 -      /* This format should preserve aspect ratio of the source */
 -      Format const * format = Format::from_id ("var-185");
 -
 -      /* We should have enough padding that the result is 4:3,
 -         which would be 1440 pixels.
 -      */
 -      BOOST_CHECK_EQUAL (format->dcp_padding (film), (1998 - 1440) / 2);
 -      
 -      /* This crops it to 1.291666667 */
 -      film->set_left_crop (5);
 -      film->set_right_crop (5);
 -
 -      /* We should now have enough padding that the result is 1.29166667,
 -         which would be 1395 pixels.
 -      */
 -      BOOST_CHECK_EQUAL (format->dcp_padding (film), rint ((1998 - 1395) / 2.0));
 -}
 -
  BOOST_AUTO_TEST_CASE (util_test)
  {
        string t = "Hello this is a string \"with quotes\" and indeed without them";
@@@ -270,101 -329,6 +271,6 @@@ public
        void do_log (string) {}
  };
  
- void
- do_positive_delay_line_test (int delay_length, int data_length)
- {
-       shared_ptr<NullLog> log (new NullLog);
-       
-       DelayLine d (log, 6, delay_length);
-       shared_ptr<AudioBuffers> data (new AudioBuffers (6, data_length));
-       int in = 0;
-       int out = 0;
-       int returned = 0;
-       int zeros = 0;
-       
-       for (int i = 0; i < 64; ++i) {
-               for (int j = 0; j < data_length; ++j) {
-                       for (int c = 0; c < 6; ++c ) {
-                               data->data(c)[j] = in;
-                               ++in;
-                       }
-               }
-               /* This only works because the delay line modifies the parameter */
-               d.process_audio (data);
-               returned += data->frames ();
-               for (int j = 0; j < data->frames(); ++j) {
-                       if (zeros < delay_length) {
-                               for (int c = 0; c < 6; ++c) {
-                                       BOOST_CHECK_EQUAL (data->data(c)[j], 0);
-                               }
-                               ++zeros;
-                       } else {
-                               for (int c = 0; c < 6; ++c) {
-                                       BOOST_CHECK_EQUAL (data->data(c)[j], out);
-                                       ++out;
-                               }
-                       }
-               }
-       }
-       BOOST_CHECK_EQUAL (returned, 64 * data_length);
- }
- void
- do_negative_delay_line_test (int delay_length, int data_length)
- {
-       shared_ptr<NullLog> log (new NullLog);
-       DelayLine d (log, 6, delay_length);
-       shared_ptr<AudioBuffers> data (new AudioBuffers (6, data_length));
-       int in = 0;
-       int out = -delay_length * 6;
-       int returned = 0;
-       
-       for (int i = 0; i < 256; ++i) {
-               data->set_frames (data_length);
-               for (int j = 0; j < data_length; ++j) {
-                       for (int c = 0; c < 6; ++c) {
-                               data->data(c)[j] = in;
-                               ++in;
-                       }
-               }
-               /* This only works because the delay line modifies the parameter */
-               d.process_audio (data);
-               returned += data->frames ();
-               for (int j = 0; j < data->frames(); ++j) {
-                       for (int c = 0; c < 6; ++c) {
-                               BOOST_CHECK_EQUAL (data->data(c)[j], out);
-                               ++out;
-                       }
-               }
-       }
-       returned += -delay_length;
-       BOOST_CHECK_EQUAL (returned, 256 * data_length);
- }
- BOOST_AUTO_TEST_CASE (delay_line_test)
- {
-       do_positive_delay_line_test (64, 128);
-       do_positive_delay_line_test (128, 64);
-       do_positive_delay_line_test (3, 512);
-       do_positive_delay_line_test (512, 3);
-       do_positive_delay_line_test (0, 64);
-       do_negative_delay_line_test (-64, 128);
-       do_negative_delay_line_test (-128, 64);
-       do_negative_delay_line_test (-3, 512);
-       do_negative_delay_line_test (-512, 3);
- }
  BOOST_AUTO_TEST_CASE (md5_digest_test)
  {
        string const t = md5_digest ("test/md5.test");
        BOOST_CHECK_THROW (md5_digest ("foobar"), OpenFileError);
  }
  
 -BOOST_AUTO_TEST_CASE (paths_test)
 -{
 -      shared_ptr<Film> f = new_test_film ("paths_test");
 -      f->set_directory ("build/test/a/b/c/d/e");
 -
 -      f->_content = "/foo/bar/baz";
 -      BOOST_CHECK_EQUAL (f->content_path(), "/foo/bar/baz");
 -      f->_content = "foo/bar/baz";
 -      BOOST_CHECK_EQUAL (f->content_path(), "build/test/a/b/c/d/e/foo/bar/baz");
 -}
 -
  void
  do_remote_encode (shared_ptr<DCPVideoFrame> frame, ServerDescription* description, shared_ptr<EncodedData> locally_encoded)
  {
@@@ -442,7 -417,7 +348,7 @@@ BOOST_AUTO_TEST_CASE (client_server_tes
        new thread (boost::bind (&Server::run, server, 2));
  
        /* Let the server get itself ready */
 -      dvdomatic_sleep (1);
 +      dcpomatic_sleep (1);
  
        ServerDescription description ("localhost", 2);
  
@@@ -464,14 -439,14 +370,14 @@@ BOOST_AUTO_TEST_CASE (make_dcp_test
  {
        shared_ptr<Film> film = new_test_film ("make_dcp_test");
        film->set_name ("test_film2");
 -      film->set_content ("../../../test/test.mp4");
 +//    film->set_content ("../../../test/test.mp4");
        film->set_format (Format::from_nickname ("Flat"));
        film->set_dcp_content_type (DCPContentType::from_pretty_name ("Test"));
        film->make_dcp ();
        film->write_metadata ();
  
        while (JobManager::instance()->work_to_do ()) {
 -              dvdomatic_sleep (1);
 +              dcpomatic_sleep (1);
        }
        
        BOOST_CHECK_EQUAL (JobManager::instance()->errors(), false);
@@@ -494,15 -469,15 +400,15 @@@ BOOST_AUTO_TEST_CASE (make_dcp_with_ran
  {
        shared_ptr<Film> film = new_test_film ("make_dcp_with_range_test");
        film->set_name ("test_film3");
 -      film->set_content ("../../../test/test.mp4");
 -      film->examine_content ();
 +//    film->set_content ("../../../test/test.mp4");
 +//    film->examine_content ();
        film->set_format (Format::from_nickname ("Flat"));
        film->set_dcp_content_type (DCPContentType::from_pretty_name ("Test"));
        film->set_trim_end (42);
        film->make_dcp ();
  
        while (JobManager::instance()->work_to_do() && !JobManager::instance()->errors()) {
 -              dvdomatic_sleep (1);
 +              dcpomatic_sleep (1);
        }
  
        BOOST_CHECK_EQUAL (JobManager::instance()->errors(), false);
@@@ -656,44 -631,44 +562,44 @@@ BOOST_AUTO_TEST_CASE (audio_sampling_ra
        Config::instance()->set_allowed_dcp_frame_rates (afr);
  
        shared_ptr<Film> f = new_test_film ("audio_sampling_rate_test");
 -      f->set_source_frame_rate (24);
 +//    f->set_source_frame_rate (24);
        f->set_dcp_frame_rate (24);
  
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
        BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 48000);
  
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
        BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 48000);
  
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 80000, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 80000, 0)));
        BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 96000);
  
 -      f->set_source_frame_rate (23.976);
 +//    f->set_source_frame_rate (23.976);
        f->set_dcp_frame_rate (best_dcp_frame_rate (23.976));
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
        BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 47952);
  
 -      f->set_source_frame_rate (29.97);
 +//    f->set_source_frame_rate (29.97);
        f->set_dcp_frame_rate (best_dcp_frame_rate (29.97));
        BOOST_CHECK_EQUAL (f->dcp_frame_rate (), 30);
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
        BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 47952);
  
 -      f->set_source_frame_rate (25);
 +//    f->set_source_frame_rate (25);
        f->set_dcp_frame_rate (24);
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
        BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 50000);
  
 -      f->set_source_frame_rate (25);
 +//    f->set_source_frame_rate (25);
        f->set_dcp_frame_rate (24);
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
        BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 50000);
  
        /* Check some out-there conversions (not the best) */
        
 -      f->set_source_frame_rate (14.99);
 +//    f->set_source_frame_rate (14.99);
        f->set_dcp_frame_rate (25);
 -      f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 16000, 0)));
 +//    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 16000, 0)));
        /* The FrameRateConversion within target_audio_sample_rate should choose to double-up
           the 14.99 fps video to 30 and then run it slow at 25.
        */
@@@ -739,10 -714,10 +645,10 @@@ BOOST_AUTO_TEST_CASE (job_manager_test
        shared_ptr<TestJob> a (new TestJob (f));
  
        JobManager::instance()->add (a);
 -      dvdomatic_sleep (1);
 +      dcpomatic_sleep (1);
        BOOST_CHECK_EQUAL (a->running (), true);
        a->set_finished_ok ();
 -      dvdomatic_sleep (2);
 +      dcpomatic_sleep (2);
        BOOST_CHECK_EQUAL (a->finished_ok(), true);
  }
  
diff --combined wscript
index f784d760dd40222821f89b78da17cf4c5a5bf26f,a9a0573670e3ac7b0e95fdac531f11429f01e096..5288cab14141bce30f51ff60e979ddcf5dd7f3dc
+++ b/wscript
@@@ -2,8 -2,8 +2,8 @@@ import subproces
  import os
  import sys
  
 -APPNAME = 'dvdomatic'
 +APPNAME = 'dcpomatic'
- VERSION = '0.84pre'
+ VERSION = '0.85pre'
  
  def options(opt):
      opt.load('compiler_cxx')
@@@ -25,7 -25,7 +25,7 @@@ def configure(conf)
                                         '-Wall', '-Wno-attributes', '-Wextra'])
  
      if conf.options.target_windows:
 -        conf.env.append_value('CXXFLAGS', ['-DDVDOMATIC_WINDOWS', '-DWIN32_LEAN_AND_MEAN', '-DBOOST_USE_WINDOWS_H', '-DUNICODE'])
 +        conf.env.append_value('CXXFLAGS', ['-DDCPOMATIC_WINDOWS', '-DWIN32_LEAN_AND_MEAN', '-DBOOST_USE_WINDOWS_H', '-DUNICODE'])
          wxrc = os.popen('wx-config --rescomp').read().split()[1:]
          conf.env.append_value('WINRCFLAGS', wxrc)
          if conf.options.enable_debug:
@@@ -35,9 -35,9 +35,9 @@@
          boost_lib_suffix = '-mt'
          boost_thread = 'boost_thread_win32-mt'
      else:
 -        conf.env.append_value('CXXFLAGS', '-DDVDOMATIC_POSIX')
 +        conf.env.append_value('CXXFLAGS', '-DDCPOMATIC_POSIX')
          conf.env.append_value('CXXFLAGS', '-DPOSIX_LOCALE_PREFIX="%s/share/locale"' % conf.env['PREFIX'])
 -        conf.env.append_value('CXXFLAGS', '-DPOSIX_ICON_PREFIX="%s/share/dvdomatic"' % conf.env['PREFIX'])
 +        conf.env.append_value('CXXFLAGS', '-DPOSIX_ICON_PREFIX="%s/share/dcpomatic"' % conf.env['PREFIX'])
          boost_lib_suffix = ''
          boost_thread = 'boost_thread'
          conf.env.append_value('LINKFLAGS', '-pthread')
      conf.env.VERSION = VERSION
  
      if conf.options.enable_debug:
 -        conf.env.append_value('CXXFLAGS', ['-g', '-DDVDOMATIC_DEBUG'])
 +        conf.env.append_value('CXXFLAGS', ['-g', '-DDCPOMATIC_DEBUG'])
      else:
          conf.env.append_value('CXXFLAGS', '-O2')
  
      if not conf.options.static:
-         conf.check_cfg(package = 'libdcp', atleast_version = '0.41', args = '--cflags --libs', uselib_store = 'DCP', mandatory = True)
+         conf.check_cfg(package = 'libdcp', atleast_version = '0.45', args = '--cflags --libs', uselib_store = 'DCP', mandatory = True)
 +        conf.check_cfg(package = 'libcxml', atleast_version = '0.01', args = '--cflags --libs', uselib_store = 'CXML', mandatory = True)
          conf.check_cfg(package = 'libavformat', args = '--cflags --libs', uselib_store = 'AVFORMAT', mandatory = True)
          conf.check_cfg(package = 'libavfilter', args = '--cflags --libs', uselib_store = 'AVFILTER', mandatory = True)
          conf.check_cfg(package = 'libavcodec', args = '--cflags --libs', uselib_store = 'AVCODEC', mandatory = True)
@@@ -73,9 -72,6 +73,9 @@@
          conf.env.HAVE_DCP = 1
          conf.env.STLIB_DCP = ['dcp', 'asdcp-libdcp', 'kumu-libdcp']
          conf.env.LIB_DCP = ['glibmm-2.4', 'xml++-2.6', 'ssl', 'crypto', 'bz2']
 +        conf.env.HAVE_CXML = 1
 +        conf.env.STLIB_CXML = ['cxml']
 +        conf.check_cfg(package = 'libxml++-2.6', args = '--cflags --libs', uselib_store = 'XML++', mandatory = True)
          conf.env.HAVE_AVFORMAT = 1
          conf.env.STLIB_AVFORMAT = ['avformat']
          conf.env.HAVE_AVFILTER = 1
@@@ -228,16 -224,16 +228,16 @@@ def build(bld)
      d = { 'PREFIX' : '${PREFIX' }
  
      obj = bld(features = 'subst')
 -    obj.source = 'dvdomatic.desktop.in'
 -    obj.target = 'dvdomatic.desktop'
 +    obj.source = 'dcpomatic.desktop.in'
 +    obj.target = 'dcpomatic.desktop'
      obj.dict = d
  
 -    bld.install_files('${PREFIX}/share/applications', 'dvdomatic.desktop')
 +    bld.install_files('${PREFIX}/share/applications', 'dcpomatic.desktop')
      for r in ['22x22', '32x32', '48x48', '64x64', '128x128']:
 -        bld.install_files('${PREFIX}/share/icons/hicolor/%s/apps' % r, 'icons/%s/dvdomatic.png' % r)
 +        bld.install_files('${PREFIX}/share/icons/hicolor/%s/apps' % r, 'icons/%s/dcpomatic.png' % r)
  
      if not bld.env.TARGET_WINDOWS:
 -        bld.install_files('${PREFIX}/share/dvdomatic', 'icons/taskbar_icon.png')
 +        bld.install_files('${PREFIX}/share/dcpomatic', 'icons/taskbar_icon.png')
  
      bld.add_post_fun(post)
  
@@@ -259,8 -255,8 +259,8 @@@ def create_version_cc(version)
  
      try:
          text =  '#include "version.h"\n'
 -        text += 'char const * dvdomatic_git_commit = \"%s\";\n' % commit
 -        text += 'char const * dvdomatic_version = \"%s\";\n' % version
 +        text += 'char const * dcpomatic_git_commit = \"%s\";\n' % commit
 +        text += 'char const * dcpomatic_version = \"%s\";\n' % version
          print('Writing version information to src/lib/version.cc')
          o = open('src/lib/version.cc', 'w')
          o.write(text)