Merge master.
authorCarl Hetherington <cth@carlh.net>
Mon, 11 Aug 2014 21:39:20 +0000 (22:39 +0100)
committerCarl Hetherington <cth@carlh.net>
Mon, 11 Aug 2014 21:39:20 +0000 (22:39 +0100)
27 files changed:
1  2 
ChangeLog
debian/changelog
platform/osx/make_dmg.sh
src/lib/analyse_audio_job.cc
src/lib/analyse_audio_job.h
src/lib/audio_decoder.cc
src/lib/colour_conversion.cc
src/lib/encoder.cc
src/lib/ffmpeg.cc
src/lib/ffmpeg_decoder.cc
src/lib/image_proxy.cc
src/lib/job.cc
src/lib/playlist.cc
src/lib/ratio.cc
src/lib/send_kdm_email_job.cc
src/lib/send_kdm_email_job.h
src/lib/transcode_job.cc
src/lib/writer.cc
src/lib/wscript
src/tools/dcpomatic.cc
src/tools/dcpomatic_cli.cc
src/wx/timecode.cc
src/wx/timecode.h
src/wx/timing_panel.cc
src/wx/video_panel.cc
test/film_metadata_test.cc
test/job_test.cc

diff --combined ChangeLog
index fe281428425795f7c86cf586e95d28215e6eab0a,04778b35f72b1a98fb7118b4c5538be69e591920..23ccae1902d6f8189f305fa33f221f08f27353ae
+++ b/ChangeLog
@@@ -1,21 -1,20 +1,38 @@@
 +2014-08-06  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.1 released.
 +
 +2014-07-15  Carl Hetherington  <cth@carlh.net>
 +
 +      * A variety of changes were made on the 2.0 branch
 +      but not documented in the ChangeLog.  Most sigificantly:
 +
 +      - DCP import
 +      - Creation of DCPs with proper XML subtitles
 +      - Import of .srt and .xml subtitles
 +      - Audio processing framework (with some basic processors).
 +
 +2014-03-07  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add subtitle view.
 +
+ 2014-08-09  Carl Hetherington  <cth@carlh.net>
+       * Version 1.72.10 released.
+ 2014-08-09  Carl Hetherington  <cth@carlh.net>
+       * Version 1.72.8 released.
+ 2014-08-08  Carl Hetherington  <cth@carlh.net>
+       * Approximate support for changing timing details of multiple
+       bits of content at the same time.
+       * Allow removal of multiple bits of content at the same time.
+       * Version 1.72.7 released.
  2014-08-04  Carl Hetherington  <cth@carlh.net>
  
        * Add BCC option for KDM emails.
@@@ -62,7 -61,6 +79,7 @@@
  2014-07-10  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.72.2 released.
 +>>>>>>> origin/master
  
  2014-07-10  Carl Hetherington  <cth@carlh.net>
  
diff --combined debian/changelog
index 09d40178298a9256211e2b1504703a01fb37445c,0cf0cc1d82e01c5ac011c7851d3d37242365f070..1da8a1f91707c709a48beaa08630d3c38924a409
@@@ -1,4 -1,4 +1,4 @@@
 -dcpomatic (1.72.10-1) UNRELEASED; urgency=low
 +dcpomatic (2.0.1-1) UNRELEASED; urgency=low
  
    * New upstream release.
    * New upstream release.
    * New upstream release.
    * New upstream release.
    * New upstream release.
++<<<<<<< HEAD
 +
 + -- Carl Hetherington <carl@dalglish>  Wed, 06 Aug 2014 18:59:35 +0100
++=======
+   * New upstream release.
+   * New upstream release.
+  -- Carl Hetherington <carl@d1stkfactory>  Sat, 09 Aug 2014 12:38:18 +0100
++>>>>>>> origin/master
  
  dcpomatic (0.87-1) UNRELEASED; urgency=low
  
diff --combined platform/osx/make_dmg.sh
index 5500c42678c048599783376aaac640eefe1070ed,01e9b0aefb8dc131970dd3e4b5923aef163f96db..1174f966a07e0c34071d61c1ad4d240e3baf21f1
@@@ -15,7 -15,7 +15,7 @@@ WORK=build/platform/os
  ENV=/Users/carl/Environments/osx/10.6
  ROOT=$1
  
 -appdir="DCP-o-matic.app"
 +appdir="DCP-o-matic 2.app"
  approot="$appdir/Contents"
  libs="$approot/lib"
  macos="$approot/MacOS"
@@@ -53,16 -53,16 +53,29 @@@ function universal_copy_lib 
      relink="$relink|$2"
  }
  
++<<<<<<< HEAD
 +universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2 "$WORK/$macos"
 +universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_cli "$WORK/$macos"
 +universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_server_cli "$WORK/$macos"
 +universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic2_batch "$WORK/$macos"
 +universal_copy $ROOT src/dcpomatic/build/src/lib/libdcpomatic2.dylib "$WORK/$libs"
 +universal_copy $ROOT src/dcpomatic/build/src/wx/libdcpomatic2-wx.dylib "$WORK/$libs"
 +universal_copy_lib $ROOT libcxml "$WORK/$libs"
 +universal_copy_lib $ROOT libdcp-1.0 "$WORK/$libs"
 +universal_copy_lib $ROOT libasdcp-libdcp-1.0 "$WORK/$libs"
 +universal_copy_lib $ROOT libkumu-libdcp-1.0 "$WORK/$libs"
++=======
+ universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic "$WORK/$macos"
+ universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic_cli "$WORK/$macos"
+ universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic_server_cli "$WORK/$macos"
+ universal_copy $ROOT src/dcpomatic/build/src/tools/dcpomatic_batch "$WORK/$macos"
+ universal_copy $ROOT src/dcpomatic/build/src/lib/libdcpomatic.dylib "$WORK/$libs"
+ universal_copy $ROOT src/dcpomatic/build/src/wx/libdcpomatic-wx.dylib "$WORK/$libs"
+ universal_copy_lib $ROOT libcxml "$WORK/$libs"
+ universal_copy_lib $ROOT libdcp "$WORK/$libs"
+ universal_copy_lib $ROOT libasdcp-libdcp "$WORK/$libs"
+ universal_copy_lib $ROOT libkumu-libdcp "$WORK/$libs"
++>>>>>>> origin/master
  universal_copy_lib $ROOT libopenjpeg "$WORK/$libs"
  universal_copy_lib $ROOT libavdevice "$WORK/$libs"
  universal_copy_lib $ROOT libavformat "$WORK/$libs"
@@@ -108,11 -108,10 +121,11 @@@ universal_copy_lib $ENV libcairo "$WORK
  
  relink=`echo $relink | sed -e "s/\+//g"`
  
 -for obj in "$WORK/$macos/dcpomatic" "$WORK/$macos/dcpomatic_batch" "$WORK/$macos/dcpomatic_cli" "$WORK/$macos/dcpomatic_server_cli" "$WORK/$macos/ffprobe" "$WORK/$libs/"*.dylib; do
 +for obj in "$WORK/$macos/dcpomatic2" "$WORK/$macos/dcpomatic2_batch" "$WORK/$macos/dcpomatic2_cli" "$WORK/$macos/dcpomatic2_server_cli" "$WORK/$macos/ffprobe" "$WORK/$libs/"*.dylib; do
    deps=`otool -L "$obj" | awk '{print $1}' | egrep "($relink)" | egrep "($ENV|$ROOT|boost)"`
    changes=""
    for dep in $deps; do
 +      echo "Relinking $dep into $obj"
        base=`basename $dep`
        # $dep will be a path within 64/; make a 32/ path too
        dep32=`echo $dep | sed -e "s/\/64\//\/32\//g"`
@@@ -130,7 -129,6 +143,7 @@@ cp icons/defaults.png "$WORK/$resources
  cp icons/kdm_email.png "$WORK/$resources"
  cp icons/servers.png "$WORK/$resources"
  cp icons/tms.png "$WORK/$resources"
 +cp icons/keys.png "$WORK/$resources"
  
  # i18n: DCP-o-matic .mo files
  for lang in de_DE es_ES fr_FR it_IT sv_SE nl_NL; do
@@@ -171,7 -169,7 +184,7 @@@ echo 
             set theViewOptions to the icon view options of container window
             set arrangement of theViewOptions to not arranged
             set icon size of theViewOptions to 64
 -           set position of item "DCP-o-matic.app" of container window to {90, 80}
 +           set position of item "DCP-o-matic 2.app" of container window to {90, 80}
             set position of item "Applications" of container window to {310, 80}
             close
             open
index af58e77ac59058097be12614c8c8499656ccff93,8186f9de49b9d89baa81c04d7f454f876b8de963..347cc0a0fb362f95b55686ad3fb76744e746ff8f
@@@ -18,7 -18,6 +18,7 @@@
  */
  
  #include "audio_analysis.h"
 +#include "audio_buffers.h"
  #include "analyse_audio_job.h"
  #include "compose.hpp"
  #include "film.h"
@@@ -49,12 -48,6 +49,6 @@@ AnalyseAudioJob::name () cons
        return _("Analyse audio");
  }
  
- string
- AnalyseAudioJob::json_name () const
- {
-       return N_("analyse_audio");
- }
  void
  AnalyseAudioJob::run ()
  {
        shared_ptr<Playlist> playlist (new Playlist);
        playlist->add (content);
        shared_ptr<Player> player (new Player (_film, playlist));
 -      player->disable_video ();
        
 -      player->Audio.connect (bind (&AnalyseAudioJob::audio, this, _1, _2));
 -
 -      _samples_per_point = max (int64_t (1), _film->time_to_audio_frames (_film->length()) / _num_points);
 +      int64_t const len = _film->length().frames (_film->audio_frame_rate());
 +      _samples_per_point = max (int64_t (1), len / _num_points);
  
        _current.resize (_film->audio_channels ());
        _analysis.reset (new AudioAnalysis (_film->audio_channels ()));
  
        _done = 0;
 -      OutputAudioFrame const len = _film->time_to_audio_frames (_film->length ());
 -      while (!player->pass ()) {
 -              set_progress (double (_done) / len);
 +      DCPTime const block = DCPTime::from_seconds (1.0 / 8);
 +      for (DCPTime t; t < _film->length(); t += block) {
 +              analyse (player->get_audio (t, block, false));
 +              set_progress (t.seconds() / _film->length().seconds());
        }
  
        _analysis->write (content->audio_analysis_path ());
@@@ -87,7 -81,7 +81,7 @@@
  }
  
  void
 -AnalyseAudioJob::audio (shared_ptr<const AudioBuffers> b, Time)
 +AnalyseAudioJob::analyse (shared_ptr<const AudioBuffers> b)
  {
        for (int i = 0; i < b->frames(); ++i) {
                for (int j = 0; j < b->channels(); ++j) {
index 4d657951b7123d57d21438c50ee273f9c807d178,3d4881983b5234572ce40a47455c5b849a620328..a218cb3400d563354b751101eeb349bf87f81b59
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  
  */
  
 +/** @file  src/lib/analyse_audio_job.h
 + *  @brief AnalyseAudioJob class.
 + */
 +
  #include "job.h"
  #include "audio_analysis.h"
  #include "types.h"
 +#include "dcpomatic_time.h"
  
  class AudioBuffers;
  class AudioContent;
  
 +/** @class AnalyseAudioJob
 + *  @brief A job to analyse the audio of a piece of AudioContent and make a note of its
 + *  broad peak and RMS levels.
 + *
 + *  After computing the peak and RMS levels over the length of the content, the job
 + *  will write a file to Content::audio_analysis_path.
 + */
  class AnalyseAudioJob : public Job
  {
  public:
        AnalyseAudioJob (boost::shared_ptr<const Film>, boost::shared_ptr<AudioContent>);
  
        std::string name () const;
-       std::string json_name () const;
        void run ();
  
  private:
 -      void audio (boost::shared_ptr<const AudioBuffers>, Time);
 +      void analyse (boost::shared_ptr<const AudioBuffers>);
  
        boost::weak_ptr<AudioContent> _content;
 -      OutputAudioFrame _done;
 +      int64_t _done;
        int64_t _samples_per_point;
        std::vector<AudioPoint> _current;
  
diff --combined src/lib/audio_decoder.cc
index e4c82f64c1a7c05d33b7f02fbf043ef5aa93b80a,18f4b890d6b8878cc58eabc6a2aba5930eec6838..f425cf2808cfbf70d53f3ff2bf097a24ab0ba035
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  
  #include "audio_decoder.h"
  #include "audio_buffers.h"
 -#include "exceptions.h"
 -#include "log.h"
 +#include "audio_processor.h"
  #include "resampler.h"
 +#include "util.h"
  
  #include "i18n.h"
  
- using std::stringstream;
  using std::list;
  using std::pair;
  using std::cout;
 +using std::min;
 +using std::max;
  using boost::optional;
  using boost::shared_ptr;
  
 -AudioDecoder::AudioDecoder (shared_ptr<const Film> film, shared_ptr<const AudioContent> content)
 -      : Decoder (film)
 -      , _audio_content (content)
 -      , _audio_position (0)
 +AudioDecoder::AudioDecoder (shared_ptr<const AudioContent> content)
 +      : _audio_content (content)
  {
 +      if (content->resampled_audio_frame_rate() != content->audio_frame_rate() && content->audio_channels ()) {
 +              _resampler.reset (new Resampler (content->audio_frame_rate(), content->resampled_audio_frame_rate(), content->audio_channels ()));
 +      }
  
 +      if (content->audio_processor ()) {
 +              _processor = content->audio_processor()->clone (content->resampled_audio_frame_rate ());
 +      }
 +
 +      reset_decoded_audio ();
  }
  
  void
 -AudioDecoder::audio (shared_ptr<const AudioBuffers> data, AudioContent::Frame frame)
 +AudioDecoder::reset_decoded_audio ()
  {
 -      Audio (data, frame);
 -      _audio_position = frame + data->frames ();
 +      _decoded_audio = ContentAudio (shared_ptr<AudioBuffers> (new AudioBuffers (_audio_content->processed_audio_channels(), 0)), 0);
  }
  
 -/** This is a bit odd, but necessary when we have (e.g.) FFmpegDecoders with no audio.
 - *  The player needs to know that there is no audio otherwise it will keep trying to
 - *  pass() the decoder to get it to emit audio.
 +shared_ptr<ContentAudio>
 +AudioDecoder::get_audio (AudioFrame frame, AudioFrame length, bool accurate)
 +{
 +      shared_ptr<ContentAudio> dec;
 +
 +      AudioFrame const end = frame + length - 1;
 +              
 +      if (frame < _decoded_audio.frame || end > (_decoded_audio.frame + length * 4)) {
 +              /* Either we have no decoded data, or what we do have is a long way from what we want: seek */
 +              seek (ContentTime::from_frames (frame, _audio_content->audio_frame_rate()), accurate);
 +      }
 +
 +      /* Offset of the data that we want from the start of _decoded_audio.audio
 +         (to be set up shortly)
 +      */
 +      AudioFrame decoded_offset = 0;
 +      
 +      /* Now enough pass() calls will either:
 +       *  (a) give us what we want, or
 +       *  (b) hit the end of the decoder.
 +       *
 +       * If we are being accurate, we want the right frames,
 +       * otherwise any frames will do.
 +       */
 +      if (accurate) {
 +              /* Keep stuffing data into _decoded_audio until we have enough data, or the subclass does not want to give us any more */
 +              while (!pass() && (_decoded_audio.frame > frame || (_decoded_audio.frame + _decoded_audio.audio->frames()) < end)) {}
 +              decoded_offset = frame - _decoded_audio.frame;
 +      } else {
 +              while (!pass() && _decoded_audio.audio->frames() < length) {}
 +              /* Use decoded_offset of 0, as we don't really care what frames we return */
 +      }
 +
 +      /* The amount of data available in _decoded_audio.audio starting from `frame'.  This could be -ve
 +         if pass() returned true before we got enough data.
 +      */
 +      AudioFrame const available = _decoded_audio.audio->frames() - decoded_offset;
 +
 +      /* We will return either that, or the requested amount, whichever is smaller */
 +      AudioFrame const to_return = max ((AudioFrame) 0, min (available, length));
 +
 +      /* Copy our data to the output */
 +      shared_ptr<AudioBuffers> out (new AudioBuffers (_decoded_audio.audio->channels(), to_return));
 +      out->copy_from (_decoded_audio.audio.get(), to_return, decoded_offset, 0);
 +
 +      AudioFrame const remaining = max ((AudioFrame) 0, available - to_return);
 +
 +      /* Clean up decoded; first, move the data after what we just returned to the start of the buffer */
 +      _decoded_audio.audio->move (decoded_offset + to_return, 0, remaining);
 +      /* And set up the number of frames we have left */
 +      _decoded_audio.audio->set_frames (remaining);
 +      /* Also bump where those frames are in terms of the content */
 +      _decoded_audio.frame += decoded_offset + to_return;
 +
 +      return shared_ptr<ContentAudio> (new ContentAudio (out, frame));
 +}
 +
 +/** Called by subclasses when audio data is ready.
 + *
 + *  Audio timestamping is made hard by many factors, but perhaps the most entertaining is resampling.
 + *  We have to assume that we are feeding continuous data into the resampler, and so we get continuous
 + *  data out.  Hence we do the timestamping here, post-resampler, just by counting samples.
 + *
 + *  The time is passed in here so that after a seek we can set up our _audio_position.  The
 + *  time is ignored once this has been done.
   */
 -bool
 -AudioDecoder::has_audio () const
 +void
 +AudioDecoder::audio (shared_ptr<const AudioBuffers> data, ContentTime time)
 +{
 +      if (_resampler) {
 +              data = _resampler->run (data);
 +      }
 +
 +      if (_processor) {
 +              data = _processor->run (data);
 +      }
 +
 +      AudioFrame const frame_rate = _audio_content->resampled_audio_frame_rate ();
 +
 +      if (_seek_reference) {
 +              /* We've had an accurate seek and now we're seeing some data */
 +              ContentTime const delta = time - _seek_reference.get ();
 +              AudioFrame const delta_frames = delta.frames (frame_rate);
 +              if (delta_frames > 0) {
 +                      /* This data comes after the seek time.  Pad the data with some silence. */
 +                      shared_ptr<AudioBuffers> padded (new AudioBuffers (data->channels(), data->frames() + delta_frames));
 +                      padded->make_silent ();
 +                      padded->copy_from (data.get(), data->frames(), 0, delta_frames);
 +                      data = padded;
 +                      time -= delta;
 +              } else if (delta_frames < 0) {
 +                      /* This data comes before the seek time.  Throw some data away */
 +                      AudioFrame const to_discard = min (-delta_frames, static_cast<AudioFrame> (data->frames()));
 +                      AudioFrame const to_keep = data->frames() - to_discard;
 +                      if (to_keep == 0) {
 +                              /* We have to throw all this data away, so keep _seek_reference and
 +                                 try again next time some data arrives.
 +                              */
 +                              return;
 +                      }
 +                      shared_ptr<AudioBuffers> trimmed (new AudioBuffers (data->channels(), to_keep));
 +                      trimmed->copy_from (data.get(), to_keep, to_discard, 0);
 +                      data = trimmed;
 +                      time += ContentTime::from_frames (to_discard, frame_rate);
 +              }
 +              _seek_reference = optional<ContentTime> ();
 +      }
 +
 +      if (!_audio_position) {
 +              _audio_position = time.frames (frame_rate);
 +      }
 +
 +      assert (_audio_position.get() >= (_decoded_audio.frame + _decoded_audio.audio->frames()));
 +
 +      add (data);
 +}
 +
 +void
 +AudioDecoder::add (shared_ptr<const AudioBuffers> data)
 +{
 +      /* Resize _decoded_audio to fit the new data */
 +      int new_size = 0;
 +      if (_decoded_audio.audio->frames() == 0) {
 +              /* There's nothing in there, so just store the new data */
 +              new_size = data->frames ();
 +              _decoded_audio.frame = _audio_position.get ();
 +      } else {
 +              /* Otherwise we need to extend _decoded_audio to include the new stuff */
 +              new_size = _audio_position.get() + data->frames() - _decoded_audio.frame;
 +      }
 +      
 +      _decoded_audio.audio->ensure_size (new_size);
 +      _decoded_audio.audio->set_frames (new_size);
 +
 +      /* Copy new data in */
 +      _decoded_audio.audio->copy_from (data.get(), data->frames(), 0, _audio_position.get() - _decoded_audio.frame);
 +      _audio_position = _audio_position.get() + data->frames ();
 +
 +      /* Limit the amount of data we keep in case nobody is asking for it */
 +      int const max_frames = _audio_content->resampled_audio_frame_rate () * 10;
 +      if (_decoded_audio.audio->frames() > max_frames) {
 +              int const to_remove = _decoded_audio.audio->frames() - max_frames;
 +              _decoded_audio.frame += to_remove;
 +              _decoded_audio.audio->move (to_remove, 0, max_frames);
 +              _decoded_audio.audio->set_frames (max_frames);
 +      }
 +}
 +
 +void
 +AudioDecoder::flush ()
 +{
 +      if (!_resampler) {
 +              return;
 +      }
 +
 +      shared_ptr<const AudioBuffers> b = _resampler->flush ();
 +      if (b) {
 +              add (b);
 +      }
 +}
 +
 +void
 +AudioDecoder::seek (ContentTime t, bool accurate)
  {
 -      return _audio_content->audio_channels () > 0;
 +      _audio_position.reset ();
 +      reset_decoded_audio ();
 +      if (accurate) {
 +              _seek_reference = t;
 +      }
 +      if (_processor) {
 +              _processor->flush ();
 +      }
  }
index aacefaa05502c9202178b2637dfa7dd5dbd50df6,e5b1104ff9427525dcd374c76d146eedccfb1775..c836cc2715728e8c75f09eb5271d4b003748dbc3
@@@ -18,8 -18,8 +18,8 @@@
  */
  
  #include <libxml++/libxml++.h>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
  #include <libcxml/cxml.h>
  #include "config.h"
  #include "colour_conversion.h"
  
  using std::list;
  using std::string;
- using std::stringstream;
  using std::cout;
  using std::vector;
  using boost::shared_ptr;
  using boost::optional;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  ColourConversion::ColourConversion ()
        : input_gamma (2.4)
@@@ -45,7 -44,7 +44,7 @@@
  {
        for (int i = 0; i < 3; ++i) {
                for (int j = 0; j < 3; ++j) {
 -                      matrix (i, j) = libdcp::colour_matrix::srgb_to_xyz[i][j];
 +                      matrix (i, j) = dcp::colour_matrix::srgb_to_xyz[i][j];
                }
        }
  }
diff --combined src/lib/encoder.cc
index e8ab5452bd404ec8a5c10cdb49ee761a8ad29f20,693fd587e8bab1c8a6b2ba94253cfacc5caf6f96..8caa0190c38726121d182b1f81e27ebf99b85295
  #include "film.h"
  #include "log.h"
  #include "config.h"
 -#include "dcp_video_frame.h"
 +#include "dcp_video.h"
  #include "server.h"
  #include "cross.h"
  #include "writer.h"
  #include "server_finder.h"
  #include "player.h"
 -#include "player_video_frame.h"
 +#include "player_video.h"
  
  #include "i18n.h"
  
@@@ -45,7 -45,6 +45,6 @@@
  
  using std::pair;
  using std::string;
- using std::stringstream;
  using std::vector;
  using std::list;
  using std::cout;
@@@ -59,14 -58,15 +58,14 @@@ using boost::scoped_array
  int const Encoder::_history_size = 25;
  
  /** @param f Film that we are encoding */
 -Encoder::Encoder (shared_ptr<const Film> f, weak_ptr<Job> j)
 +Encoder::Encoder (shared_ptr<const Film> f, weak_ptr<Job> j, shared_ptr<Writer> writer)
        : _film (f)
        , _job (j)
        , _video_frames_out (0)
        , _terminate (false)
 +      , _writer (writer)
  {
 -      _have_a_real_frame[EYES_BOTH] = false;
 -      _have_a_real_frame[EYES_LEFT] = false;
 -      _have_a_real_frame[EYES_RIGHT] = false;
 +
  }
  
  Encoder::~Encoder ()
@@@ -88,17 -88,18 +87,17 @@@ Encoder::add_worker_threads (ServerDesc
  }
  
  void
 -Encoder::process_begin ()
 +Encoder::begin ()
  {
        for (int i = 0; i < Config::instance()->num_local_encoding_threads (); ++i) {
                _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, optional<ServerDescription> ())));
        }
  
 -      _writer.reset (new Writer (_film, _job));
        ServerFinder::instance()->connect (boost::bind (&Encoder::server_found, this, _1));
  }
  
  void
 -Encoder::process_end ()
 +Encoder::end ()
  {
        boost::mutex::scoped_lock lock (_mutex);
  
             So just mop up anything left in the queue here.
        */
  
 -      for (list<shared_ptr<DCPVideoFrame> >::iterator i = _queue.begin(); i != _queue.end(); ++i) {
 +      for (list<shared_ptr<DCPVideo> >::iterator i = _queue.begin(); i != _queue.end(); ++i) {
                LOG_GENERAL (N_("Encode left-over frame %1"), (*i)->index ());
                try {
                        _writer->write ((*i)->encode_locally(), (*i)->index (), (*i)->eyes ());
                        LOG_ERROR (N_("Local encode failed (%1)"), e.what ());
                }
        }
 -              
 -      _writer->finish ();
 -      _writer.reset ();
  }     
  
  /** @return an estimate of the current number of frames we are encoding per second,
@@@ -178,7 -182,7 +177,7 @@@ Encoder::frame_done (
  }
  
  void
 -Encoder::process_video (shared_ptr<PlayerVideoFrame> pvf, bool same)
 +Encoder::enqueue (shared_ptr<PlayerVideo> pvf)
  {
        _waker.nudge ();
        
  
        if (_writer->can_fake_write (_video_frames_out)) {
                _writer->fake_write (_video_frames_out, pvf->eyes ());
 -              _have_a_real_frame[pvf->eyes()] = false;
 -              frame_done ();
 -      } else if (same && _have_a_real_frame[pvf->eyes()]) {
 -              /* Use the last frame that we encoded. */
 -              _writer->repeat (_video_frames_out, pvf->eyes());
                frame_done ();
 +      } else if (pvf->has_j2k ()) {
 +              _writer->write (pvf->j2k(), _video_frames_out, pvf->eyes ());
        } else {
                /* Queue this new frame for encoding */
                LOG_TIMING ("adding to queue of %1", _queue.size ());
 -              _queue.push_back (shared_ptr<DCPVideoFrame> (
 -                                        new DCPVideoFrame (
 -                                                pvf, _video_frames_out, _film->video_frame_rate(),
 -                                                _film->j2k_bandwidth(), _film->resolution(), _film->log()
 +              _queue.push_back (shared_ptr<DCPVideo> (
 +                                        new DCPVideo (
 +                                                pvf,
 +                                                _video_frames_out,
 +                                                _film->video_frame_rate(),
 +                                                _film->j2k_bandwidth(),
 +                                                _film->resolution(),
 +                                                _film->burn_subtitles(),
 +                                                _film->log()
                                                  )
                                          ));
  
                   waiting on that.
                */
                _empty_condition.notify_all ();
 -              _have_a_real_frame[pvf->eyes()] = true;
        }
  
        if (pvf->eyes() != EYES_LEFT) {
        }
  }
  
 -void
 -Encoder::process_audio (shared_ptr<const AudioBuffers> data)
 -{
 -      _writer->write (data);
 -}
 -
  void
  Encoder::terminate_threads ()
  {
@@@ -278,7 -287,7 +277,7 @@@ tr
                }
  
                LOG_TIMING ("[%1] encoder thread wakes with queue of %2", boost::this_thread::get_id(), _queue.size());
 -              shared_ptr<DCPVideoFrame> vf = _queue.front ();
 +              shared_ptr<DCPVideo> vf = _queue.front ();
                LOG_TIMING ("[%1] encoder thread pops frame %2 (%3) from queue", boost::this_thread::get_id(), vf->index(), vf->eyes ());
                _queue.pop_front ();
                
diff --combined src/lib/ffmpeg.cc
index f5af239b0a0eeba9445d85a98fbe97c767ae9658,ebe62b51fbd412cb73956b2ae86db2c6058257ab..fa369dda429c9342c2b08eed7a4b74ee50a38c35
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -22,11 -22,9 +22,11 @@@ extern "C" 
  #include <libavformat/avformat.h>
  #include <libswscale/swscale.h>
  }
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "ffmpeg.h"
  #include "ffmpeg_content.h"
 +#include "ffmpeg_audio_stream.h"
 +#include "ffmpeg_subtitle_stream.h"
  #include "exceptions.h"
  #include "util.h"
  
@@@ -34,9 -32,8 +34,8 @@@
  
  using std::string;
  using std::cout;
- using std::stringstream;
  using boost::shared_ptr;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  boost::mutex FFmpeg::_mutex;
  
@@@ -50,7 -47,8 +49,7 @@@ FFmpeg::FFmpeg (boost::shared_ptr<cons
        , _video_stream (-1)
  {
        setup_general ();
 -      setup_video ();
 -      setup_audio ();
 +      setup_decoders ();
  }
  
  FFmpeg::~FFmpeg ()
        boost::mutex::scoped_lock lm (_mutex);
  
        for (uint32_t i = 0; i < _format_context->nb_streams; ++i) {
 -              AVCodecContext* context = _format_context->streams[i]->codec;
 -              if (context->codec_type == AVMEDIA_TYPE_VIDEO || context->codec_type == AVMEDIA_TYPE_AUDIO) {
 -                      avcodec_close (context);
 -              }
 +              avcodec_close (_format_context->streams[i]->codec);
        }
  
        av_frame_free (&_frame);
 -      
        avformat_close_input (&_format_context);
  }
  
@@@ -141,24 -143,46 +140,24 @@@ FFmpeg::setup_general (
  }
  
  void
 -FFmpeg::setup_video ()
 -{
 -      boost::mutex::scoped_lock lm (_mutex);
 -
 -      assert (_video_stream >= 0);
 -      AVCodecContext* context = _format_context->streams[_video_stream]->codec;
 -      AVCodec* codec = avcodec_find_decoder (context->codec_id);
 -
 -      if (codec == 0) {
 -              throw DecodeError (_("could not find video decoder"));
 -      }
 -
 -      if (avcodec_open2 (context, codec, 0) < 0) {
 -              throw DecodeError (N_("could not open video decoder"));
 -      }
 -}
 -
 -void
 -FFmpeg::setup_audio ()
 +FFmpeg::setup_decoders ()
  {
        boost::mutex::scoped_lock lm (_mutex);
  
        for (uint32_t i = 0; i < _format_context->nb_streams; ++i) {
                AVCodecContext* context = _format_context->streams[i]->codec;
 -              if (context->codec_type != AVMEDIA_TYPE_AUDIO) {
 -                      continue;
 -              }
                
                AVCodec* codec = avcodec_find_decoder (context->codec_id);
 -              if (codec == 0) {
 -                      throw DecodeError (_("could not find audio decoder"));
 -              }
 -              
 -              if (avcodec_open2 (context, codec, 0) < 0) {
 -                      throw DecodeError (N_("could not open audio decoder"));
 +              if (codec) {
 +                      if (avcodec_open2 (context, codec, 0) < 0) {
 +                              throw DecodeError (N_("could not open decoder"));
 +                      }
                }
 +
 +              /* We are silently ignoring any failures to find suitable decoders here */
        }
  }
  
 -
  AVCodecContext *
  FFmpeg::video_codec_context () const
  {
  AVCodecContext *
  FFmpeg::audio_codec_context () const
  {
 +      if (!_ffmpeg_content->audio_stream ()) {
 +              return 0;
 +      }
 +      
        return _ffmpeg_content->audio_stream()->stream(_format_context)->codec;
  }
  
 +AVCodecContext *
 +FFmpeg::subtitle_codec_context () const
 +{
 +      if (!_ffmpeg_content->subtitle_stream ()) {
 +              return 0;
 +      }
 +      
 +      return _ffmpeg_content->subtitle_stream()->stream(_format_context)->codec;
 +}
 +
  int
  FFmpeg::avio_read (uint8_t* buffer, int const amount)
  {
index 0f2c88fc9f8ce36f06f957b8c4172f6248c2c44c,9ecc503dc6fb6d305375ff6729a1667b49ecdaf5..4e5d9bb1ed271760e858b4a37d62fcde110fa1d4
@@@ -32,47 -32,50 +32,46 @@@ extern "C" 
  #include <libavcodec/avcodec.h>
  #include <libavformat/avformat.h>
  }
 -#include "film.h"
  #include "filter.h"
  #include "exceptions.h"
  #include "image.h"
  #include "util.h"
  #include "log.h"
  #include "ffmpeg_decoder.h"
 +#include "ffmpeg_audio_stream.h"
 +#include "ffmpeg_subtitle_stream.h"
  #include "filter_graph.h"
  #include "audio_buffers.h"
  #include "ffmpeg_content.h"
 -#include "image_proxy.h"
 +#include "raw_image_proxy.h"
 +#include "film.h"
 +#include "timer.h"
  
  #include "i18n.h"
  
 -#define LOG_GENERAL(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
 -#define LOG_ERROR(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR);
 -#define LOG_WARNING(...) film->log()->log (__VA_ARGS__, Log::TYPE_WARNING);
 +#define LOG_GENERAL(...) _video_content->film()->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
 +#define LOG_ERROR(...) _video_content->film()->log()->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR);
 +#define LOG_WARNING(...) _video_content->film()->log()->log (__VA_ARGS__, Log::TYPE_WARNING);
  
  using std::cout;
  using std::string;
  using std::vector;
- using std::stringstream;
  using std::list;
  using std::min;
  using std::pair;
 +using std::make_pair;
  using boost::shared_ptr;
  using boost::optional;
  using boost::dynamic_pointer_cast;
 -using libdcp::Size;
 +using dcp::Size;
  
 -FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegContent> c, bool video, bool audio)
 -      : Decoder (f)
 -      , VideoDecoder (f, c)
 -      , AudioDecoder (f, c)
 -      , SubtitleDecoder (f)
 +FFmpegDecoder::FFmpegDecoder (shared_ptr<const FFmpegContent> c, shared_ptr<Log> log)
 +      : VideoDecoder (c)
 +      , AudioDecoder (c)
 +      , SubtitleDecoder (c)
        , FFmpeg (c)
 -      , _subtitle_codec_context (0)
 -      , _subtitle_codec (0)
 -      , _decode_video (video)
 -      , _decode_audio (audio)
 -      , _pts_offset (0)
 -      , _just_sought (false)
 +      , _log (log)
  {
 -      setup_subtitle ();
 -
        /* Audio and video frame PTS values may not start with 0.  We want
           to fiddle them so that:
  
           Then we remove big initial gaps in PTS and we allow our
           insertion of black frames to work.
  
 -         We will do:
 -           audio_pts_to_use = audio_pts_from_ffmpeg + pts_offset;
 -           video_pts_to_use = video_pts_from_ffmpeg + pts_offset;
 +         We will do pts_to_use = pts_from_ffmpeg + pts_offset;
        */
  
 -      bool const have_video = video && c->first_video();
 -      bool const have_audio = audio && c->audio_stream() && c->audio_stream()->first_audio;
 +      bool const have_video = c->first_video();
 +      bool const have_audio = c->audio_stream () && c->audio_stream()->first_audio;
  
        /* First, make one of them start at 0 */
  
  
        /* Now adjust both so that the video pts starts on a frame */
        if (have_video && have_audio) {
 -              double first_video = c->first_video().get() + _pts_offset;
 -              double const old_first_video = first_video;
 -              
 -              /* Round the first video up to a frame boundary */
 -              if (fabs (rint (first_video * c->video_frame_rate()) - first_video * c->video_frame_rate()) > 1e-6) {
 -                      first_video = ceil (first_video * c->video_frame_rate()) / c->video_frame_rate ();
 -              }
 -
 -              _pts_offset += first_video - old_first_video;
 -      }
 -}
 -
 -FFmpegDecoder::~FFmpegDecoder ()
 -{
 -      boost::mutex::scoped_lock lm (_mutex);
 -
 -      if (_subtitle_codec_context) {
 -              avcodec_close (_subtitle_codec_context);
 +              ContentTime first_video = c->first_video().get() + _pts_offset;
 +              ContentTime const old_first_video = first_video;
 +              _pts_offset += first_video.round_up (c->video_frame_rate ()) - old_first_video;
        }
  }
  
@@@ -116,15 -136,20 +115,15 @@@ FFmpegDecoder::flush (
        
        /* XXX: should we reset _packet.data and size after each *_decode_* call? */
        
 -      if (_decode_video) {
 -              while (decode_video_packet ()) {}
 -      }
 +      while (decode_video_packet ()) {}
        
 -      if (_ffmpeg_content->audio_stream() && _decode_audio) {
 +      if (_ffmpeg_content->audio_stream()) {
                decode_audio_packet ();
 +              AudioDecoder::flush ();
        }
 -
 -      /* Stop us being asked for any more data */
 -      _video_position = _ffmpeg_content->video_length_after_3d_combine ();
 -      _audio_position = _ffmpeg_content->audio_length ();
  }
  
 -void
 +bool
  FFmpegDecoder::pass ()
  {
        int r = av_read_frame (_format_context, &_packet);
                        /* Maybe we should fail here, but for now we'll just finish off instead */
                        char buf[256];
                        av_strerror (r, buf, sizeof(buf));
 -                      shared_ptr<const Film> film = _film.lock ();
 -                      assert (film);
                        LOG_ERROR (N_("error on av_read_frame (%1) (%2)"), buf, r);
                }
  
                flush ();
 -              return;
 +              return true;
        }
  
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
        int const si = _packet.stream_index;
 -      
 -      if (si == _video_stream && _decode_video) {
 +
 +      if (si == _video_stream) {
                decode_video_packet ();
 -      } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si) && _decode_audio) {
 +      } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si)) {
                decode_audio_packet ();
 -      } else if (_ffmpeg_content->subtitle_stream() && _ffmpeg_content->subtitle_stream()->uses_index (_format_context, si) && film->with_subtitles ()) {
 +      } else if (_ffmpeg_content->subtitle_stream() && _ffmpeg_content->subtitle_stream()->uses_index (_format_context, si)) {
                decode_subtitle_packet ();
        }
  
        av_free_packet (&_packet);
 +      return false;
  }
  
  /** @param data pointer to array of pointers to buffers.
@@@ -285,40 -314,82 +284,40 @@@ FFmpegDecoder::bytes_per_audio_sample (
  }
  
  void
 -FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate)
 +FFmpegDecoder::seek (ContentTime time, bool accurate)
  {
 -      double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base);
 -
 -      /* If we are doing an accurate seek, our initial shot will be 5 frames (5 being
 -         a number plucked from the air) earlier than we want to end up.  The loop below
 -         will hopefully then step through to where we want to be.
 +      VideoDecoder::seek (time, accurate);
 +      AudioDecoder::seek (time, accurate);
 +      
 +      /* If we are doing an `accurate' seek, we need to use pre-roll, as
 +         we don't really know what the seek will give us.
        */
 -      int initial = frame;
  
 -      if (accurate) {
 -              initial -= 5;
 -      }
 +      ContentTime pre_roll = accurate ? ContentTime::from_seconds (2) : ContentTime (0);
 +      time -= pre_roll;
  
 -      if (initial < 0) {
 -              initial = 0;
 -      }
 -
 -      /* Initial seek time in the stream's timebase */
 -      int64_t const initial_vt = ((initial / _ffmpeg_content->original_video_frame_rate()) - _pts_offset) / time_base;
 -
 -      av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD);
 -
 -      avcodec_flush_buffers (video_codec_context());
 -      if (_subtitle_codec_context) {
 -              avcodec_flush_buffers (_subtitle_codec_context);
 -      }
 -
 -      /* This !accurate is piling hack upon hack; setting _just_sought to true
 -         even with accurate == true defeats our attempt to align the start
 -         of the video and audio.  Here we disable that defeat when accurate == true
 -         i.e. when we are making a DCP rather than just previewing one.
 -         Ewww.  This should be gone in 2.0.
 +      /* XXX: it seems debatable whether PTS should be used here...
 +         http://www.mjbshaw.com/2012/04/seeking-in-ffmpeg-know-your-timestamp.html
        */
 -      if (!accurate) {
 -              _just_sought = true;
 -      }
 -      
 -      _video_position = frame;
        
 -      if (frame == 0 || !accurate) {
 -              /* We're already there, or we're as close as we need to be */
 -              return;
 -      }
 +      ContentTime const u = time - _pts_offset;
 +      int64_t s = u.seconds() / av_q2d (_format_context->streams[_video_stream]->time_base);
  
 -      while (true) {
 -              int r = av_read_frame (_format_context, &_packet);
 -              if (r < 0) {
 -                      return;
 -              }
 +      if (_ffmpeg_content->audio_stream ()) {
 +              s = min (
 +                      s, int64_t (u.seconds() / av_q2d (_ffmpeg_content->audio_stream()->stream(_format_context)->time_base))
 +                      );
 +      }
  
 -              if (_packet.stream_index != _video_stream) {
 -                      av_free_packet (&_packet);
 -                      continue;
 -              }
 -              
 -              int finished = 0;
 -              r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
 -              if (r >= 0 && finished) {
 -                      _video_position = rint (
 -                              (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * _ffmpeg_content->original_video_frame_rate()
 -                              );
 +      av_seek_frame (_format_context, _video_stream, s, 0);
  
 -                      if (_video_position >= (frame - 1)) {
 -                              av_free_packet (&_packet);
 -                              break;
 -                      }
 -              }
 -              
 -              av_free_packet (&_packet);
 +      avcodec_flush_buffers (video_codec_context());
 +      if (audio_codec_context ()) {
 +              avcodec_flush_buffers (audio_codec_context ());
 +      }
 +      if (subtitle_codec_context ()) {
 +              avcodec_flush_buffers (subtitle_codec_context ());
        }
 -
 -      /* _video_position should be the next thing to be emitted, which will the one after the thing
 -         we just saw.
 -      */
 -      _video_position++;
  }
  
  void
@@@ -334,23 -405,42 +333,23 @@@ FFmpegDecoder::decode_audio_packet (
  
                int frame_finished;
                int const decode_result = avcodec_decode_audio4 (audio_codec_context(), _frame, &frame_finished, &copy_packet);
 +
                if (decode_result < 0) {
 -                      shared_ptr<const Film> film = _film.lock ();
 -                      assert (film);
                        LOG_ERROR ("avcodec_decode_audio4 failed (%1)", decode_result);
                        return;
                }
  
                if (frame_finished) {
 -                      
 -                      if (_audio_position == 0) {
 -                              /* Where we are in the source, in seconds */
 -                              double const pts = av_q2d (_format_context->streams[copy_packet.stream_index]->time_base)
 -                                      * av_frame_get_best_effort_timestamp(_frame) + _pts_offset;
 -
 -                              if (pts > 0) {
 -                                      /* Emit some silence */
 -                                      int64_t frames = pts * _ffmpeg_content->content_audio_frame_rate ();
 -                                      while (frames > 0) {
 -                                              int64_t const this_time = min (frames, (int64_t) _ffmpeg_content->content_audio_frame_rate() / 2);
 -                                              
 -                                              shared_ptr<AudioBuffers> silence (
 -                                                      new AudioBuffers (_ffmpeg_content->audio_channels(), this_time)
 -                                                      );
 -                                      
 -                                              silence->make_silent ();
 -                                              audio (silence, _audio_position);
 -                                              frames -= this_time;
 -                                      }
 -                              }
 -                      }
 +                      ContentTime const ct = ContentTime::from_seconds (
 +                              av_frame_get_best_effort_timestamp (_frame) *
 +                              av_q2d (_ffmpeg_content->audio_stream()->stream (_format_context)->time_base))
 +                              + _pts_offset;
                        
                        int const data_size = av_samples_get_buffer_size (
                                0, audio_codec_context()->channels, _frame->nb_samples, audio_sample_format (), 1
                                );
 -                      
 -                      audio (deinterleave_audio (_frame->data, data_size), _audio_position);
 +
 +                      audio (deinterleave_audio (_frame->data, data_size), ct);
                }
                        
                copy_packet.data += decode_result;
@@@ -371,13 -461,17 +370,13 @@@ FFmpegDecoder::decode_video_packet (
        shared_ptr<FilterGraph> graph;
        
        list<shared_ptr<FilterGraph> >::iterator i = _filter_graphs.begin();
 -      while (i != _filter_graphs.end() && !(*i)->can_process (libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)) {
 +      while (i != _filter_graphs.end() && !(*i)->can_process (dcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)) {
                ++i;
        }
  
        if (i == _filter_graphs.end ()) {
 -              shared_ptr<const Film> film = _film.lock ();
 -              assert (film);
 -
 -              graph.reset (new FilterGraph (_ffmpeg_content, libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
 +              graph.reset (new FilterGraph (_ffmpeg_content, dcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
                _filter_graphs.push_back (graph);
 -
                LOG_GENERAL (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format);
        } else {
                graph = *i;
  
        list<pair<shared_ptr<Image>, int64_t> > images = graph->process (_frame);
  
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
        for (list<pair<shared_ptr<Image>, int64_t> >::iterator i = images.begin(); i != images.end(); ++i) {
  
                shared_ptr<Image> image = i->first;
                
                if (i->second != AV_NOPTS_VALUE) {
 -
 -                      double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset;
 -
 -                      if (_just_sought) {
 -                              /* We just did a seek, so disable any attempts to correct for where we
 -                                 are / should be.
 -                              */
 -                              _video_position = rint (pts * _ffmpeg_content->original_video_frame_rate ());
 -                              _just_sought = false;
 -                      }
 -
 -                      double const next = _video_position / _ffmpeg_content->original_video_frame_rate();
 -                      double const one_frame = 1 / _ffmpeg_content->original_video_frame_rate ();
 -                      double delta = pts - next;
 -
 -                      while (delta > one_frame) {
 -                              /* This PTS is more than one frame forward in time of where we think we should be; emit
 -                                 a black frame.
 -                              */
 -
 -                              /* XXX: I think this should be a copy of the last frame... */
 -                              boost::shared_ptr<Image> black (
 -                                      new Image (
 -                                              static_cast<AVPixelFormat> (_frame->format),
 -                                              libdcp::Size (video_codec_context()->width, video_codec_context()->height),
 -                                              true
 -                                              )
 -                                      );
 -                              
 -                              shared_ptr<const Film> film = _film.lock ();
 -                              assert (film);
 -
 -                              black->make_black ();
 -                              video (shared_ptr<ImageProxy> (new RawImageProxy (image, film->log())), false, _video_position);
 -                              delta -= one_frame;
 -                      }
 -
 -                      if (delta > -one_frame) {
 -                              /* This PTS is within a frame of being right; emit this (otherwise it will be dropped) */
 -                              video (shared_ptr<ImageProxy> (new RawImageProxy (image, film->log())), false, _video_position);
 -                      }
 -                              
 +                      double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset.seconds ();
 +                      video (
 +                              shared_ptr<ImageProxy> (new RawImageProxy (image, _video_content->film()->log())),
 +                              rint (pts * _ffmpeg_content->video_frame_rate ())
 +                              );
                } else {
                        LOG_WARNING ("Dropping frame without PTS");
                }
  
        return true;
  }
 -
 -      
 -void
 -FFmpegDecoder::setup_subtitle ()
 -{
 -      boost::mutex::scoped_lock lm (_mutex);
 -      
 -      if (!_ffmpeg_content->subtitle_stream()) {
 -              return;
 -      }
 -
 -      _subtitle_codec_context = _ffmpeg_content->subtitle_stream()->stream(_format_context)->codec;
 -      if (_subtitle_codec_context == 0) {
 -              throw DecodeError (N_("could not find subtitle stream"));
 -      }
 -
 -      _subtitle_codec = avcodec_find_decoder (_subtitle_codec_context->codec_id);
 -
 -      if (_subtitle_codec == 0) {
 -              throw DecodeError (N_("could not find subtitle decoder"));
 -      }
 -      
 -      if (avcodec_open2 (_subtitle_codec_context, _subtitle_codec, 0) < 0) {
 -              throw DecodeError (N_("could not open subtitle decoder"));
 -      }
 -}
 -
 -bool
 -FFmpegDecoder::done () const
 -{
 -      bool const vd = !_decode_video || (_video_position >= _ffmpeg_content->video_length());
 -      bool const ad = !_decode_audio || !_ffmpeg_content->audio_stream() || (_audio_position >= _ffmpeg_content->audio_length());
 -      return vd && ad;
 -}
        
  void
  FFmpegDecoder::decode_subtitle_packet ()
  {
        int got_subtitle;
        AVSubtitle sub;
 -      if (avcodec_decode_subtitle2 (_subtitle_codec_context, &sub, &got_subtitle, &_packet) < 0 || !got_subtitle) {
 +      if (avcodec_decode_subtitle2 (subtitle_codec_context(), &sub, &got_subtitle, &_packet) < 0 || !got_subtitle) {
                return;
        }
  
           indicate that the previous subtitle should stop.
        */
        if (sub.num_rects <= 0) {
 -              subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0);
 +              image_subtitle (ContentTimePeriod (), shared_ptr<Image> (), dcpomatic::Rect<double> ());
                return;
        } else if (sub.num_rects > 1) {
                throw DecodeError (_("multi-part subtitles not yet supported"));
        }
                
 -      /* Subtitle PTS in seconds (within the source, not taking into account any of the
 +      /* Subtitle PTS (within the source, not taking into account any of the
           source that we may have chopped off for the DCP)
        */
 -      double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _pts_offset;
 -
 -      /* hence start time for this sub */
 -      Time const from = (packet_time + (double (sub.start_display_time) / 1e3)) * TIME_HZ;
 -      Time const to = (packet_time + (double (sub.end_display_time) / 1e3)) * TIME_HZ;
 +      ContentTimePeriod period = subtitle_period (sub) + _pts_offset;
  
        AVSubtitleRect const * rect = sub.rects[0];
  
        if (rect->type != SUBTITLE_BITMAP) {
 -              throw DecodeError (_("non-bitmap subtitles not yet supported"));
 +              /* XXX */
 +              // throw DecodeError (_("non-bitmap subtitles not yet supported"));
 +              return;
        }
  
        /* Note RGBA is expressed little-endian, so the first byte in the word is R, second
           G, third B, fourth A.
        */
 -      shared_ptr<Image> image (new Image (PIX_FMT_RGBA, libdcp::Size (rect->w, rect->h), true));
 +      shared_ptr<Image> image (new Image (PIX_FMT_RGBA, dcp::Size (rect->w, rect->h), true));
  
        /* Start of the first line in the subtitle */
        uint8_t* sub_p = rect->pict.data[0];
                out_p += image->stride()[0] / sizeof (uint32_t);
        }
  
 -      libdcp::Size const vs = _ffmpeg_content->video_size ();
 +      dcp::Size const vs = _ffmpeg_content->video_size ();
  
 -      subtitle (
 +      image_subtitle (
 +              period,
                image,
                dcpomatic::Rect<double> (
                        static_cast<double> (rect->x) / vs.width,
                        static_cast<double> (rect->y) / vs.height,
                        static_cast<double> (rect->w) / vs.width,
                        static_cast<double> (rect->h) / vs.height
 -                      ),
 -              from,
 -              to
 +                      )
                );
 -                        
        
        avsubtitle_free (&sub);
  }
 +
 +list<ContentTimePeriod>
 +FFmpegDecoder::subtitles_during (ContentTimePeriod p, bool starting) const
 +{
 +      return _ffmpeg_content->subtitles_during (p, starting);
 +}
diff --combined src/lib/image_proxy.cc
index 233f477453416ee44dac4c0659442a751e48a701,7c212be045a166704852e78f4453f1af06175a74..b6b387b76081947a554290c0846cf81333acd42c
  
  */
  
 -#include <Magick++.h>
 -#include <libdcp/util.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/util.h>
 +#include <dcp/raw_convert.h>
  #include "image_proxy.h"
 +#include "raw_image_proxy.h"
 +#include "magick_image_proxy.h"
 +#include "j2k_image_proxy.h"
  #include "image.h"
  #include "exceptions.h"
  #include "cross.h"
@@@ -34,7 -32,6 +34,6 @@@
  
  using std::cout;
  using std::string;
- using std::stringstream;
  using boost::shared_ptr;
  
  ImageProxy::ImageProxy (shared_ptr<Log> log)
  
  }
  
 -RawImageProxy::RawImageProxy (shared_ptr<Image> image, shared_ptr<Log> log)
 -      : ImageProxy (log)
 -      , _image (image)
 -{
 -
 -}
 -
 -RawImageProxy::RawImageProxy (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log)
 -      : ImageProxy (log)
 -{
 -      libdcp::Size size (
 -              xml->number_child<int> ("Width"), xml->number_child<int> ("Height")
 -              );
 -
 -      _image.reset (new Image (static_cast<AVPixelFormat> (xml->number_child<int> ("PixelFormat")), size, true));
 -      _image->read_from_socket (socket);
 -}
 -
 -shared_ptr<Image>
 -RawImageProxy::image () const
 -{
 -      return _image;
 -}
 -
 -void
 -RawImageProxy::add_metadata (xmlpp::Node* node) const
 -{
 -      node->add_child("Type")->add_child_text (N_("Raw"));
 -      node->add_child("Width")->add_child_text (libdcp::raw_convert<string> (_image->size().width));
 -      node->add_child("Height")->add_child_text (libdcp::raw_convert<string> (_image->size().height));
 -      node->add_child("PixelFormat")->add_child_text (libdcp::raw_convert<string> (_image->pixel_format ()));
 -}
 -
 -void
 -RawImageProxy::send_binary (shared_ptr<Socket> socket) const
 -{
 -      _image->write_to_socket (socket);
 -}
 -
 -MagickImageProxy::MagickImageProxy (boost::filesystem::path path, shared_ptr<Log> log)
 -      : ImageProxy (log)
 -{
 -      /* Read the file into a Blob */
 -      
 -      boost::uintmax_t const size = boost::filesystem::file_size (path);
 -      FILE* f = fopen_boost (path, "rb");
 -      if (!f) {
 -              throw OpenFileError (path);
 -      }
 -              
 -      uint8_t* data = new uint8_t[size];
 -      if (fread (data, 1, size, f) != size) {
 -              delete[] data;
 -              throw ReadFileError (path);
 -      }
 -      
 -      fclose (f);
 -      _blob.update (data, size);
 -      delete[] data;
 -}
 -
 -MagickImageProxy::MagickImageProxy (shared_ptr<cxml::Node>, shared_ptr<Socket> socket, shared_ptr<Log> log)
 -      : ImageProxy (log)
 -{
 -      uint32_t const size = socket->read_uint32 ();
 -      uint8_t* data = new uint8_t[size];
 -      socket->read (data, size);
 -      _blob.update (data, size);
 -      delete[] data;
 -}
 -
 -shared_ptr<Image>
 -MagickImageProxy::image () const
 -{
 -      if (_image) {
 -              return _image;
 -      }
 -
 -      LOG_TIMING ("[%1] MagickImageProxy begins decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length());
 -
 -      Magick::Image* magick_image = 0;
 -      try {
 -              magick_image = new Magick::Image (_blob);
 -      } catch (...) {
 -              throw DecodeError (_("Could not decode image file"));
 -      }
 -
 -      LOG_TIMING ("[%1] MagickImageProxy decode finished", boost::this_thread::get_id ());
 -
 -      libdcp::Size size (magick_image->columns(), magick_image->rows());
 -
 -      _image.reset (new Image (PIX_FMT_RGB24, size, true));
 -
 -      /* Write line-by-line here as _image must be aligned, and write() cannot be told about strides */
 -      uint8_t* p = _image->data()[0];
 -      for (int i = 0; i < size.height; ++i) {
 -              using namespace MagickCore;
 -              magick_image->write (0, i, size.width, 1, "RGB", CharPixel, p);
 -              p += _image->stride()[0];
 -      }
 -
 -      delete magick_image;
 -
 -      LOG_TIMING ("[%1] MagickImageProxy completes decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length());
 -
 -      return _image;
 -}
 -
 -void
 -MagickImageProxy::add_metadata (xmlpp::Node* node) const
 -{
 -      node->add_child("Type")->add_child_text (N_("Magick"));
 -}
 -
 -void
 -MagickImageProxy::send_binary (shared_ptr<Socket> socket) const
 -{
 -      socket->write (_blob.length ());
 -      socket->write ((uint8_t *) _blob.data (), _blob.length ());
 -}
 -
  shared_ptr<ImageProxy>
  image_proxy_factory (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log)
  {
                return shared_ptr<ImageProxy> (new RawImageProxy (xml, socket, log));
        } else if (xml->string_child("Type") == N_("Magick")) {
                return shared_ptr<MagickImageProxy> (new MagickImageProxy (xml, socket, log));
 +      } else if (xml->string_child("Type") == N_("J2K")) {
 +              return shared_ptr<J2KImageProxy> (new J2KImageProxy (xml, socket, log));
        }
  
        throw NetworkError (_("Unexpected image type received by server"));
diff --combined src/lib/job.cc
index 594c0da34f1b5cdc7a55177e61bee698b5ac21d5,530ad979830495cbe9a43ecbb90c3bc6082457f7..31a10a44bd6af2343ca4ddfa525ed3b4e35226ad
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  
  #include <boost/thread.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/exceptions.h>
 +#include <dcp/exceptions.h>
  #include "job.h"
  #include "util.h"
  #include "cross.h"
  #include "ui_signaller.h"
  #include "exceptions.h"
 +#include "film.h"
 +#include "log.h"
  
  #include "i18n.h"
  
@@@ -68,8 -66,8 +68,8 @@@ Job::run_wrapper (
  
                run ();
  
 -      } catch (libdcp::FileError& e) {
 -              
 +      } catch (dcp::FileError& e) {
 +
                string m = String::compose (_("An error occurred whilst handling the file %1."), boost::filesystem::path (e.filename()).leaf());
  
                try {
@@@ -206,7 -204,7 +206,7 @@@ Job::set_state (State s
        }       
  }
  
 -/** @return Time (in seconds) that this sub-job has been running */
 +/** @return DCPTime (in seconds) that this sub-job has been running */
  int
  Job::elapsed_time () const
  {
@@@ -281,7 -279,6 +281,7 @@@ Job::error_summary () cons
  void
  Job::set_error (string s, string d)
  {
 +      _film->log()->log (String::compose ("Error in job: %1 (%2)", s, d), Log::TYPE_ERROR);
        boost::mutex::scoped_lock lm (_state_mutex);
        _error_summary = s;
        _error_details = d;
@@@ -328,29 -325,6 +328,6 @@@ Job::status () cons
        return s.str ();
  }
  
- string
- Job::json_status () const
- {
-       boost::mutex::scoped_lock lm (_state_mutex);
-       switch (_state) {
-       case NEW:
-               return N_("new");
-       case RUNNING:
-               return N_("running");
-       case PAUSED:
-               return N_("paused");
-       case FINISHED_OK:
-               return N_("finished_ok");
-       case FINISHED_ERROR:
-               return N_("finished_error");
-       case FINISHED_CANCELLED:
-               return N_("finished_cancelled");
-       }
-       return "";
- }
  /** @return An estimate of the remaining time for this sub-job, in seconds */
  int
  Job::remaining_time () const
diff --combined src/lib/playlist.cc
index 16c740943ca89f382f27082ba70a935d987c484b,c3e430082ecacd2a51db12edb1e5ee536b577fc6..22412da4a3640b5313cd6d75cdb25422a4ee08f6
@@@ -40,7 -40,6 +40,6 @@@ using std::vector
  using std::min;
  using std::max;
  using std::string;
- using std::stringstream;
  using std::pair;
  using boost::optional;
  using boost::shared_ptr;
@@@ -80,20 -79,20 +79,20 @@@ Playlist::maybe_sequence_video (
        _sequencing_video = true;
        
        ContentList cl = _content;
 -      Time next_left = 0;
 -      Time next_right = 0;
 +      DCPTime next_left;
 +      DCPTime next_right;
        for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) {
                shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i);
                if (!vc) {
                        continue;
                }
 -      
 +              
                if (vc->video_frame_type() == VIDEO_FRAME_TYPE_3D_RIGHT) {
                        vc->set_position (next_right);
 -                      next_right = vc->end() + 1;
 +                      next_right = vc->end() + DCPTime::delta ();
                } else {
                        vc->set_position (next_left);
 -                      next_left = vc->end() + 1;
 +                      next_left = vc->end() + DCPTime::delta ();
                }
        }
  
@@@ -121,7 -120,7 +120,7 @@@ Playlist::video_identifier () cons
  
  /** @param node <Playlist> node */
  void
 -Playlist::set_from_xml (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node, int version, list<string>& notes)
 +Playlist::set_from_xml (shared_ptr<const Film> film, cxml::ConstNodePtr node, int version, list<string>& notes)
  {
        list<cxml::NodePtr> c = node->node_children ("Content");
        for (list<cxml::NodePtr>::iterator i = c.begin(); i != c.end(); ++i) {
@@@ -186,6 -185,19 +185,6 @@@ Playlist::remove (ContentList c
        Changed ();
  }
  
 -bool
 -Playlist::has_subtitles () const
 -{
 -      for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 -              shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (*i);
 -              if (fc && !fc->subtitle_streams().empty()) {
 -                      return true;
 -              }
 -      }
 -
 -      return false;
 -}
 -
  class FrameRateCandidate
  {
  public:
@@@ -249,12 -261,12 +248,12 @@@ Playlist::best_dcp_frame_rate () cons
        return best->dcp;
  }
  
 -Time
 +DCPTime
  Playlist::length () const
  {
 -      Time len = 0;
 +      DCPTime len;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 -              len = max (len, (*i)->end() + 1);
 +              len = max (len, (*i)->end() + DCPTime::delta ());
        }
  
        return len;
@@@ -274,10 -286,10 +273,10 @@@ Playlist::reconnect (
        }
  }
  
 -Time
 +DCPTime
  Playlist::video_end () const
  {
 -      Time end = 0;
 +      DCPTime end;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
                if (dynamic_pointer_cast<const VideoContent> (*i)) {
                        end = max (end, (*i)->end ());
        return end;
  }
  
 +FrameRateChange
 +Playlist::active_frame_rate_change (DCPTime t, int dcp_video_frame_rate) const
 +{
 +      for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 +              shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i);
 +              if (!vc) {
 +                      continue;
 +              }
 +
 +              if (vc->position() >= t && t < vc->end()) {
 +                      return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate);
 +              }
 +      }
 +
 +      return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate);
 +}
 +
  void
  Playlist::set_sequence_video (bool s)
  {
@@@ -326,7 -321,7 +325,7 @@@ Playlist::content () cons
  void
  Playlist::repeat (ContentList c, int n)
  {
 -      pair<Time, Time> range (TIME_MAX, 0);
 +      pair<DCPTime, DCPTime> range (DCPTime::max (), DCPTime ());
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                range.first = min (range.first, (*i)->position ());
                range.second = max (range.second, (*i)->position ());
                range.second = max (range.second, (*i)->end ());
        }
  
 -      Time pos = range.second;
 +      DCPTime pos = range.second;
        for (int i = 0; i < n; ++i) {
                for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                        shared_ptr<Content> copy = (*i)->clone ();
@@@ -368,7 -363,7 +367,7 @@@ Playlist::move_earlier (shared_ptr<Cont
        }
  
        
 -      Time const p = (*previous)->position ();
 +      DCPTime const p = (*previous)->position ();
        (*previous)->set_position (p + c->length_after_trim ());
        c->set_position (p);
        sort (_content.begin(), _content.end(), ContentSorter ());
@@@ -397,3 -392,20 +396,3 @@@ Playlist::move_later (shared_ptr<Conten
        c->set_position (c->position() + c->length_after_trim ());
        sort (_content.begin(), _content.end(), ContentSorter ());
  }
 -
 -FrameRateChange
 -Playlist::active_frame_rate_change (Time t, int dcp_video_frame_rate) const
 -{
 -      for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 -              shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i);
 -              if (!vc) {
 -                      continue;
 -              }
 -
 -              if (vc->position() >= t && t < vc->end()) {
 -                      return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate);
 -              }
 -      }
 -
 -      return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate);
 -}
diff --combined src/lib/ratio.cc
index fbd7022322c5b0e9e02d8e94785baaa54e23c4c5,4a5b39a22f28b060ebf5ede3e7b27d197e7667d4..bb69636584167d12b19d358e99d5a25193394d76
  
  */
  
 -#include <libdcp/types.h>
 +#include <dcp/types.h>
  #include "ratio.h"
  #include "util.h"
  
  #include "i18n.h"
  
  using std::string;
- using std::stringstream;
  using std::vector;
  
  vector<Ratio const *> Ratio::_ratios;
index de03222725074b9a6a9fdfa670a463de0f88f396,164dfe9875e4ec35a5578e8ff41f9132fe7eea7c..541307f5acc8ace90d682b5b396046ddcdcbab2e
@@@ -34,7 -34,7 +34,7 @@@ SendKDMEmailJob::SendKDMEmailJob 
        boost::filesystem::path dcp,
        boost::posix_time::ptime from,
        boost::posix_time::ptime to,
 -      libdcp::KDM::Formulation formulation
 +      dcp::Formulation formulation
        )
        : Job (f)
        , _screens (screens)
@@@ -52,12 -52,6 +52,6 @@@ SendKDMEmailJob::name () cons
        return String::compose (_("Email KDMs for %1"), _film->name());
  }
  
- string
- SendKDMEmailJob::json_name () const
- {
-       return N_("send_kdm_email");
- }
  void
  SendKDMEmailJob::run ()
  {
index 084836715f865fe6a62b52eae1e7ac1536bd202f,778d3927ac01d7a741c88804065dab4f5df00c21..af84a13af2b959c034e342c3dd9d2b56448c8a12
@@@ -18,7 -18,7 +18,7 @@@
  */
  
  #include <boost/filesystem.hpp>
 -#include <libdcp/kdm.h>
 +#include <dcp/types.h>
  #include "job.h"
  
  class Screen;
@@@ -32,11 -32,10 +32,10 @@@ public
                boost::filesystem::path,
                boost::posix_time::ptime,
                boost::posix_time::ptime,
 -              libdcp::KDM::Formulation
 +              dcp::Formulation
                );
  
        std::string name () const;
-       std::string json_name () const;
        void run ();
  
  private:
@@@ -44,5 -43,5 +43,5 @@@
        boost::filesystem::path _dcp;
        boost::posix_time::ptime _from;
        boost::posix_time::ptime _to;
 -      libdcp::KDM::Formulation _formulation;
 +      dcp::Formulation _formulation;
  };
diff --combined src/lib/transcode_job.cc
index 4a85fa18aa8935d0a2c0ff5670f504de012b439e,675a951160cd31f2027da12d6f2c3ac89d146c03..1a162b6544ad1ba6442acbfbdcaab87d52b4bd95
@@@ -37,7 -37,6 +37,7 @@@ using std::string
  using std::stringstream;
  using std::fixed;
  using std::setprecision;
 +using std::cout;
  using boost::shared_ptr;
  
  /** @param s Film to use.
@@@ -54,12 -53,6 +54,6 @@@ TranscodeJob::name () cons
        return String::compose (_("Transcode %1"), _film->name());
  }
  
- string
- TranscodeJob::json_name () const
- {
-       return N_("transcode");
- }
  void
  TranscodeJob::run ()
  {
@@@ -107,7 -100,6 +101,7 @@@ TranscodeJob::status () cons
        return s.str ();
  }
  
 +/** @return Approximate remaining time in seconds */
  int
  TranscodeJob::remaining_time () const
  {
        }
  
        /* Compute approximate proposed length here, as it's only here that we need it */
 -      OutputVideoFrame const left = _film->time_to_video_frames (_film->length ()) - t->video_frames_out();
 -      return left / fps;
 +      return (_film->length().frames (_film->video_frame_rate ()) - t->video_frames_out()) / fps;
  }
diff --combined src/lib/writer.cc
index c594a64464729e4ae33287deb06c64f457f234f4,e8d4f90a6cbc181bb7c162cf5bf7efc1179db29c..eda82f27729c6b810b6850679c0562eeadb24ba2
  
  #include <fstream>
  #include <cerrno>
 -#include <libdcp/mono_picture_asset.h>
 -#include <libdcp/stereo_picture_asset.h>
 -#include <libdcp/sound_asset.h>
 -#include <libdcp/reel.h>
 -#include <libdcp/dcp.h>
 -#include <libdcp/cpl.h>
 +#include <dcp/mono_picture_mxf.h>
 +#include <dcp/stereo_picture_mxf.h>
 +#include <dcp/sound_mxf.h>
 +#include <dcp/sound_mxf_writer.h>
 +#include <dcp/reel.h>
 +#include <dcp/reel_mono_picture_asset.h>
 +#include <dcp/reel_stereo_picture_asset.h>
 +#include <dcp/reel_sound_asset.h>
 +#include <dcp/reel_subtitle_asset.h>
 +#include <dcp/dcp.h>
 +#include <dcp/cpl.h>
 +#include <dcp/signer.h>
  #include "writer.h"
  #include "compose.hpp"
  #include "film.h"
  #include "ratio.h"
  #include "log.h"
 -#include "dcp_video_frame.h"
 +#include "dcp_video.h"
  #include "dcp_content_type.h"
 -#include "player.h"
  #include "audio_mapping.h"
  #include "config.h"
  #include "job.h"
  #include "cross.h"
 +#include "audio_buffers.h"
  #include "md5_digester.h"
 +#include "encoded_data.h"
  
  #include "i18n.h"
  
@@@ -60,10 -53,8 +60,9 @@@ using std::pair
  using std::string;
  using std::list;
  using std::cout;
- using std::stringstream;
  using boost::shared_ptr;
  using boost::weak_ptr;
 +using boost::dynamic_pointer_cast;
  
  int const Writer::_maximum_frames_in_memory = Config::instance()->num_local_encoding_threads() + 4;
  
@@@ -78,6 -69,7 +77,6 @@@ Writer::Writer (shared_ptr<const Film> 
        , _last_written_eyes (EYES_RIGHT)
        , _full_written (0)
        , _fake_written (0)
 -      , _repeat_written (0)
        , _pushed_to_disk (0)
  {
        /* Remove any old DCP */
        */
  
        if (_film->three_d ()) {
 -              _picture_asset.reset (new libdcp::StereoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::StereoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        } else {
 -              _picture_asset.reset (new libdcp::MonoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::MonoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        }
  
 -      _picture_asset->set_edit_rate (_film->video_frame_rate ());
 -      _picture_asset->set_size (_film->frame_size ());
 -      _picture_asset->set_interop (_film->interop ());
 +      _picture_mxf->set_size (_film->frame_size ());
  
        if (_film->encrypted ()) {
 -              _picture_asset->set_key (_film->key ());
 +              _picture_mxf->set_key (_film->key ());
        }
        
 -      _picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0);
 +      _picture_mxf_writer = _picture_mxf->start_write (
 +              _film->internal_video_mxf_dir() / _film->internal_video_mxf_filename(),
 +              _film->interop() ? dcp::INTEROP : dcp::SMPTE,
 +              _first_nonexistant_frame > 0
 +              );
  
        if (_film->audio_channels ()) {
 -              _sound_asset.reset (new libdcp::SoundAsset (_film->directory (), _film->audio_mxf_filename ()));
 -              _sound_asset->set_edit_rate (_film->video_frame_rate ());
 -              _sound_asset->set_channels (_film->audio_channels ());
 -              _sound_asset->set_sampling_rate (_film->audio_frame_rate ());
 -              _sound_asset->set_interop (_film->interop ());
 +              _sound_mxf.reset (new dcp::SoundMXF (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels ()));
  
                if (_film->encrypted ()) {
 -                      _sound_asset->set_key (_film->key ());
 +                      _sound_mxf->set_key (_film->key ());
                }
 -              
 -              /* Write the sound asset into the film directory so that we leave the creation
 +      
 +              /* Write the sound MXF into the film directory so that we leave the creation
                   of the DCP directory until the last minute.
                */
 -              _sound_asset_writer = _sound_asset->start_write ();
 +              _sound_mxf_writer = _sound_mxf->start_write (_film->directory() / _film->audio_mxf_filename(), _film->interop() ? dcp::INTEROP : dcp::SMPTE);
 +      }
 +
 +      /* Check that the signer is OK if we need one */
 +      if (_film->is_signed() && !Config::instance()->signer()->valid ()) {
 +              throw InvalidSignerError ();
        }
  
        _thread = new boost::thread (boost::bind (&Writer::thread, this));
@@@ -184,7 -173,7 +183,7 @@@ Writer::fake_write (int frame, Eyes eye
        }
        
        FILE* ifi = fopen_boost (_film->info_path (frame, eyes), "r");
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        
        QueueItem qi;
  void
  Writer::write (shared_ptr<const AudioBuffers> audio)
  {
 -      if (_sound_asset) {
 -              _sound_asset_writer->write (audio->data(), audio->frames());
 +      if (_sound_mxf_writer) {
 +              _sound_mxf_writer->write (audio->data(), audio->frames());
        }
  }
  
@@@ -286,7 -275,7 +285,7 @@@ tr
                                        qi.encoded.reset (new EncodedData (_film->j2c_path (qi.frame, qi.eyes, false)));
                                }
  
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (qi.encoded->data(), qi.encoded->size());
 +                              dcp::FrameInfo fin = _picture_mxf_writer->write (qi.encoded->data(), qi.encoded->size());
                                qi.encoded->write_info (_film, qi.frame, qi.eyes, fin);
                                _last_written[qi.eyes] = qi.encoded;
                                ++_full_written;
                        }
                        case QueueItem::FAKE:
                                LOG_GENERAL (N_("Writer FAKE-writes %1 to MXF"), qi.frame);
 -                              _picture_asset_writer->fake_write (qi.size);
 +                              _picture_mxf_writer->fake_write (qi.size);
                                _last_written[qi.eyes].reset ();
                                ++_fake_written;
                                break;
 -                      case QueueItem::REPEAT:
 -                      {
 -                              LOG_GENERAL (N_("Writer REPEAT-writes %1 to MXF"), qi.frame);
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (
 -                                      _last_written[qi.eyes]->data(),
 -                                      _last_written[qi.eyes]->size()
 -                                      );
 -                              
 -                              _last_written[qi.eyes]->write_info (_film, qi.frame, qi.eyes, fin);
 -                              ++_repeat_written;
 -                              break;
 -                      }
                        }
                        lock.lock ();
  
                        _last_written_frame = qi.frame;
                        _last_written_eyes = qi.eyes;
                        
 -                      if (_film->length()) {
 -                              shared_ptr<Job> job = _job.lock ();
 -                              assert (job);
 -                              int total = _film->time_to_video_frames (_film->length ());
 -                              if (_film->three_d ()) {
 -                                      /* _full_written and so on are incremented for each eye, so we need to double the total
 -                                         frames to get the correct progress.
 -                                      */
 -                                      total *= 2;
 -                              }
 -                              job->set_progress (float (_full_written + _fake_written + _repeat_written) / total);
 +                      shared_ptr<Job> job = _job.lock ();
 +                      assert (job);
 +                      int64_t total = _film->length().frames (_film->video_frame_rate ());
 +                      if (_film->three_d ()) {
 +                              /* _full_written and so on are incremented for each eye, so we need to double the total
 +                                 frames to get the correct progress.
 +                              */
 +                              total *= 2;
 +                      }
 +                      if (total) {
 +                              job->set_progress (float (_full_written + _fake_written) / total);
                        }
                }
  
@@@ -389,11 -390,15 +388,11 @@@ Writer::finish (
        
        terminate_thread (true);
  
 -      _picture_asset_writer->finalize ();
 -      if (_sound_asset_writer) {
 -              _sound_asset_writer->finalize ();
 +      _picture_mxf_writer->finalize ();
 +      if (_sound_mxf_writer) {
 +              _sound_mxf_writer->finalize ();
        }
        
 -      int const frames = _last_written_frame + 1;
 -
 -      _picture_asset->set_duration (frames);
 -
        /* Hard-link the video MXF into the DCP */
        boost::filesystem::path video_from;
        video_from /= _film->internal_video_mxf_dir();
                LOG_WARNING_NC ("Hard-link failed; fell back to copying");
        }
  
 -      /* And update the asset */
 -
 -      _picture_asset->set_directory (_film->dir (_film->dcp_name ()));
 -      _picture_asset->set_file_name (_film->video_mxf_filename ());
 +      _picture_mxf->set_file (video_to);
  
        /* Move the audio MXF into the DCP */
  
 -      if (_sound_asset) {
 +      if (_sound_mxf) {
                boost::filesystem::path audio_to;
                audio_to /= _film->dir (_film->dcp_name ());
                audio_to /= _film->audio_mxf_filename ();
                                String::compose (_("could not move audio MXF into the DCP (%1)"), ec.value ()), _film->file (_film->audio_mxf_filename ())
                                );
                }
 -              
 -              _sound_asset->set_directory (_film->dir (_film->dcp_name ()));
 -              _sound_asset->set_duration (frames);
 +
 +              _sound_mxf->set_file (audio_to);
        }
 -      
 -      libdcp::DCP dcp (_film->dir (_film->dcp_name()));
  
 -      shared_ptr<libdcp::CPL> cpl (
 -              new libdcp::CPL (
 -                      _film->dir (_film->dcp_name()),
 +      dcp::DCP dcp (_film->dir (_film->dcp_name()));
 +
 +      shared_ptr<dcp::CPL> cpl (
 +              new dcp::CPL (
                        _film->dcp_name(),
 -                      _film->dcp_content_type()->libdcp_kind (),
 -                      frames,
 -                      _film->video_frame_rate ()
 +                      _film->dcp_content_type()->libdcp_kind ()
                        )
                );
        
 -      dcp.add_cpl (cpl);
 +      dcp.add (cpl);
 +
 +      shared_ptr<dcp::Reel> reel (new dcp::Reel ());
 +
 +      shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (_picture_mxf);
 +      if (mono) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelMonoPictureAsset (mono, 0)));
 +              dcp.add (mono);
 +      }
 +
 +      shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (_picture_mxf);
 +      if (stereo) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelStereoPictureAsset (stereo, 0)));
 +              dcp.add (stereo);
 +      }
 +
 +      if (_sound_mxf) {
 +              reel->add (shared_ptr<dcp::ReelSoundAsset> (new dcp::ReelSoundAsset (_sound_mxf, 0)));
 +              dcp.add (_sound_mxf);
 +      }
  
 -      cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel (
 -                                                       _picture_asset,
 -                                                       _sound_asset,
 -                                                       shared_ptr<libdcp::SubtitleAsset> ()
 -                                                       )
 -                             ));
 +      if (_subtitle_content) {
 +              _subtitle_content->write_xml (_film->dir (_film->dcp_name ()) / _film->subtitle_xml_filename ());
 +              reel->add (shared_ptr<dcp::ReelSubtitleAsset> (
 +                                 new dcp::ReelSubtitleAsset (
 +                                         _subtitle_content,
 +                                         dcp::Fraction (_film->video_frame_rate(), 1),
 +                                         _picture_mxf->intrinsic_duration (),
 +                                         0
 +                                         )
 +                                 ));
 +              
 +              dcp.add (_subtitle_content);
 +      }
 +      
 +      cpl->add (reel);
  
        shared_ptr<Job> job = _job.lock ();
        assert (job);
  
        job->sub (_("Computing image digest"));
 -      _picture_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +      _picture_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
  
 -      if (_sound_asset) {
 +      if (_sound_mxf) {
                job->sub (_("Computing audio digest"));
 -              _sound_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +              _sound_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
        }
  
 -      libdcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
 +      dcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
        meta.set_issue_date_now ();
 -      dcp.write_xml (_film->interop (), meta, _film->is_signed() ? make_signer () : shared_ptr<const libdcp::Signer> ());
 -
 -      LOG_GENERAL (
 -              N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT; %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
 -              );
 -}
  
 -/** Tell the writer that frame `f' should be a repeat of the frame before it */
 -void
 -Writer::repeat (int f, Eyes e)
 -{
 -      boost::mutex::scoped_lock lock (_mutex);
 -
 -      while (_queued_full_in_memory > _maximum_frames_in_memory) {
 -              /* The queue is too big; wait until that is sorted out */
 -              _full_condition.wait (lock);
 -      }
 -      
 -      QueueItem qi;
 -      qi.type = QueueItem::REPEAT;
 -      qi.frame = f;
 -      if (_film->three_d() && e == EYES_BOTH) {
 -              qi.eyes = EYES_LEFT;
 -              _queue.push_back (qi);
 -              qi.eyes = EYES_RIGHT;
 -              _queue.push_back (qi);
 -      } else {
 -              qi.eyes = e;
 -              _queue.push_back (qi);
 +      shared_ptr<const dcp::Signer> signer;
 +      if (_film->is_signed ()) {
 +              signer = Config::instance()->signer ();
 +              /* We did check earlier, but check again here to be on the safe side */
 +              if (!signer->valid ()) {
 +                      throw InvalidSignerError ();
 +              }
        }
  
 -      /* Now there's something to do: wake anything wait()ing on _empty_condition */
 -      _empty_condition.notify_all ();
 +      dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer);
 +
 +      LOG_GENERAL (
 +              N_("Wrote %1 FULL, %2 FAKE, %3 pushed to disk"), _full_written, _fake_written, _pushed_to_disk
 +              );
  }
  
  bool
@@@ -516,7 -518,7 +515,7 @@@ Writer::check_existing_picture_mxf_fram
                return false;
        }
        
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        if (info.size == 0) {
                LOG_GENERAL ("Existing frame %1 has no info file", f);
@@@ -601,24 -603,6 +600,24 @@@ Writer::can_fake_write (int frame) cons
        return (frame != 0 && frame < _first_nonexistant_frame);
  }
  
 +void
 +Writer::write (PlayerSubtitles subs)
 +{
 +      if (subs.text.empty ()) {
 +              return;
 +      }
 +      
 +      if (!_subtitle_content) {
 +              _subtitle_content.reset (
 +                      new dcp::SubtitleContent (_film->name(), _film->isdcf_metadata().subtitle_language)
 +                      );
 +      }
 +      
 +      for (list<dcp::SubtitleString>::const_iterator i = subs.text.begin(); i != subs.text.end(); ++i) {
 +              _subtitle_content->add (*i);
 +      }
 +}
 +
  bool
  operator< (QueueItem const & a, QueueItem const & b)
  {
diff --combined src/lib/wscript
index f26529b8423b7763a6ab237c2feafe6e07dadf51,f53ae7b74db8dbb4923b97ca13ea4228e3a1bd0a..15f26c34f2dcc91cfeef95d19a142301eebd5e38
@@@ -7,39 -7,26 +7,39 @@@ sources = ""
            audio_buffers.cc
            audio_content.cc
            audio_decoder.cc
 +          audio_filter.cc
            audio_mapping.cc
 +          audio_processor.cc
            cinema.cc
 +          cinema_sound_processor.cc
            colour_conversion.cc
            config.cc
            content.cc
            content_factory.cc
 +          content_subtitle.cc
            cross.cc
 +          dcp_content.cc
            dcp_content_type.cc
 -          dcp_video_frame.cc
 -          decoder.cc
 +          dcp_decoder.cc
 +          dcp_examiner.cc
 +          dcp_subtitle_content.cc
 +          dcp_subtitle_decoder.cc
 +          dcp_video.cc
 +          dcpomatic_time.cc
            dolby_cp750.cc
            encoder.cc
 +          encoded_data.cc
            examine_content_job.cc
            exceptions.cc
            file_group.cc
            filter_graph.cc
            ffmpeg.cc
 +          ffmpeg_audio_stream.cc
            ffmpeg_content.cc
            ffmpeg_decoder.cc
            ffmpeg_examiner.cc
 +          ffmpeg_stream.cc
 +          ffmpeg_subtitle_stream.cc
            film.cc
            filter.cc
            frame_rate_change.cc
            image_examiner.cc
            image_proxy.cc
            isdcf_metadata.cc
 +          j2k_image_proxy.cc
            job.cc
            job_manager.cc
            kdm.cc
-           json_server.cc
            log.cc
 +          magick_image_proxy.cc
            md5_digester.cc
 -          piece.cc
 +          mid_side_decoder.cc
            player.cc
 -          player_video_frame.cc
 +          player_video.cc
            playlist.cc
            ratio.cc
 +          raw_image_proxy.cc
 +          render_subtitles.cc
            resampler.cc
            scp_dcp_job.cc
            scaler.cc
            send_kdm_email_job.cc
            server.cc
            server_finder.cc
 +          single_stream_audio_content.cc
            sndfile_content.cc
            sndfile_decoder.cc
 -          sound_processor.cc
 -          subtitle.cc
 +          subrip.cc
 +          subrip_content.cc
 +          subrip_decoder.cc
            subtitle_content.cc
            subtitle_decoder.cc
            timer.cc
@@@ -85,7 -65,6 +84,7 @@@
            types.cc
            ui_signaller.cc
            update.cc
 +          upmixer_a.cc
            util.cc
            video_content.cc
            video_decoder.cc
@@@ -98,13 -77,13 +97,13 @@@ def build(bld)
      else:
          obj = bld(features = 'cxx cxxshlib')
  
 -    obj.name = 'libdcpomatic'
 +    obj.name = 'libdcpomatic2'
      obj.export_includes = ['..']
      obj.uselib = """
                   AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE 
                   BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2
 -                 SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XMLPP
 -                 CURL ZIP QUICKMAIL XMLSEC
 +                 SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++
 +                 CURL ZIP QUICKMAIL PANGOMM CAIROMM XMLSEC
                   """
  
      if bld.env.TARGET_OSX:
      if bld.env.BUILD_STATIC:
          obj.uselib += ' XMLPP'
  
 -    obj.target = 'dcpomatic'
 +    obj.target = 'dcpomatic2'
  
 -    i18n.po_to_mo(os.path.join('src', 'lib'), 'libdcpomatic', bld)
 +    i18n.po_to_mo(os.path.join('src', 'lib'), 'libdcpomatic2', bld)
  
  def pot(bld):
      i18n.pot(os.path.join('src', 'lib'), sources, 'libdcpomatic')
diff --combined src/tools/dcpomatic.cc
index e67703b22fe34a69b544a34b660c74f693e39ca8,cd2978052add1f55409eda32784413bd5f0e84f2..8c7f09ae77de6fd4e8344ca12f3a528617991a6a
@@@ -30,7 -30,7 +30,7 @@@
  #include <wx/stdpaths.h>
  #include <wx/cmdline.h>
  #include <wx/preferences.h>
 -#include <libdcp/exceptions.h>
 +#include <dcp/exceptions.h>
  #include "wx/film_viewer.h"
  #include "wx/film_editor.h"
  #include "wx/job_manager_view.h"
@@@ -45,7 -45,6 +45,7 @@@
  #include "wx/servers_list_dialog.h"
  #include "wx/hints_dialog.h"
  #include "wx/update_dialog.h"
 +#include "wx/content_panel.h"
  #include "lib/film.h"
  #include "lib/config.h"
  #include "lib/util.h"
@@@ -338,13 -337,12 +338,12 @@@ private
  
        void file_changed (boost::filesystem::path f)
        {
-               stringstream s;
-               s << wx_to_std (_("DCP-o-matic"));
+               string s = wx_to_std (_("DCP-o-matic"));
                if (!f.empty ()) {
-                       s << " - " << f.string ();
+                       s += " - " + f.string ();
                }
                
-               SetTitle (std_to_wx (s.str()));
+               SetTitle (std_to_wx (s));
        }
        
        void file_new ()
                                        shared_ptr<Job> (new SendKDMEmailJob (film, d->screens (), d->cpl (), d->from (), d->until (), d->formulation ()))
                                        );
                        }
 -              } catch (libdcp::NotEncryptedError& e) {
 +              } catch (dcp::NotEncryptedError& e) {
                        error_dialog (this, _("CPL's content is not encrypted."));
                } catch (exception& e) {
                        error_dialog (this, e.what ());
  
        void content_scale_to_fit_width ()
        {
 -              VideoContentList vc = _film_editor->selected_video_content ();
 +              VideoContentList vc = _film_editor->content_panel()->selected_video ();
                for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
                        (*i)->scale_and_crop_to_fit_width ();
                }
  
        void content_scale_to_fit_height ()
        {
 -              VideoContentList vc = _film_editor->selected_video_content ();
 +              VideoContentList vc = _film_editor->content_panel()->selected_video ();
                for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) {
                        (*i)->scale_and_crop_to_fit_height ();
                }
                }
                bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished ();
                bool const have_cpl = film && !film->cpls().empty ();
 -              bool const have_selected_video_content = !_film_editor->selected_video_content().empty();
 +              bool const have_selected_video_content = !_film_editor->content_panel()->selected_video().empty();
                
                for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) {
                        
@@@ -644,9 -642,6 +643,9 @@@ static const wxCmdLineEntryDesc command
        { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
  };
  
 +/** @class App
 + *  @brief The magic App class for wxWidgets.
 + */
  class App : public wxApp
  {
        bool OnInit ()
index 7dd820b9571bc9becf931b176aabcd975e75d996,5cb05e11d5ce23721d5acff9c1ab530550022325..8c33b7d83a983f4917bf98ee413bf0cf7186d635
@@@ -20,7 -20,7 +20,7 @@@
  #include <iostream>
  #include <iomanip>
  #include <getopt.h>
 -#include <libdcp/version.h>
 +#include <dcp/version.h>
  #include "lib/film.h"
  #include "lib/filter.h"
  #include "lib/transcode_job.h"
@@@ -33,7 -33,6 +33,6 @@@
  #include "lib/log.h"
  #include "lib/ui_signaller.h"
  #include "lib/server_finder.h"
- #include "lib/json_server.h"
  
  using std::string;
  using std::cerr;
@@@ -53,7 -52,6 +52,6 @@@ help (string n
             << "  -f, --flags        show flags passed to C++ compiler on build\n"
             << "  -n, --no-progress  do not print progress to stdout\n"
             << "  -r, --no-remote    do not use any remote servers\n"
-            << "  -j, --json <port>  run a JSON server on the specified port\n"
             << "  -k, --keep-going   keep running even when the job is complete\n"
             << "\n"
             << "<FILM> is the film directory.\n";
@@@ -65,7 -63,6 +63,6 @@@ main (int argc, char* argv[]
        string film_dir;
        bool progress = true;
        bool no_remote = false;
-       int json_port = 0;
        bool keep_going = false;
  
        int option_index = 0;
                        { "flags", no_argument, 0, 'f'},
                        { "no-progress", no_argument, 0, 'n'},
                        { "no-remote", no_argument, 0, 'r'},
-                       { "json", required_argument, 0, 'j' },
                        { "keep-going", no_argument, 0, 'k' },
                        { 0, 0, 0, 0 }
                };
  
-               int c = getopt_long (argc, argv, "vhdfnrj:k", long_options, &option_index);
+               int c = getopt_long (argc, argv, "vhdfnrk", long_options, &option_index);
  
                if (c == -1) {
                        break;
                case 'r':
                        no_remote = true;
                        break;
-               case 'j':
-                       json_port = atoi (optarg);
-                       break;
                case 'k':
                        keep_going = true;
                        break;
                ServerFinder::instance()->disable ();
        }
  
-       if (json_port) {
-               new JSONServer (json_port);
-       }
        cout << "DCP-o-matic " << dcpomatic_version << " git " << dcpomatic_git_commit;
        char buf[256];
        if (gethostname (buf, 256) == 0) {
        }
  
        cout << "\nMaking DCP for " << film->name() << "\n";
- //    cout << "Content: " << film->content() << "\n";
- //    pair<string, string> const f = Filter::ffmpeg_strings (film->filters ());
- //    cout << "Filters: " << f.first << " " << f.second << "\n";
  
        film->make_dcp ();
  
diff --combined src/wx/timecode.cc
index 86e1997e961ba31ad080b70bfe6fc6fbb7ac2de5,166446d8c76140eb1e5fa01c27731abdb003111a..07cb0be6586b6485defff73b314ad2702fac87f5
@@@ -83,38 -83,54 +83,48 @@@ Timecode::Timecode (wxWindow* parent
  }
  
  void
 -Timecode::set (Time t, int fps)
 +Timecode::set (DCPTime t, int fps)
  {
 -      /* Do this calculation with frames so that we can round
 -         to a frame boundary at the start rather than the end.
 -      */
 -      int64_t f = divide_with_round (t * fps, TIME_HZ);
 -      
 -      int const h = f / (3600 * fps);
 -      f -= h * 3600 * fps;
 -      int const m = f / (60 * fps);
 -      f -= m * 60 * fps;
 -      int const s = f / fps;
 -      f -= s * fps;
 +      int h;
 +      int m;
 +      int s;
 +      int f;
 +      t.split (fps, h, m, s, f);
  
        checked_set (_hours, lexical_cast<string> (h));
        checked_set (_minutes, lexical_cast<string> (m));
        checked_set (_seconds, lexical_cast<string> (s));
        checked_set (_frames, lexical_cast<string> (f));
  
 -      _fixed->SetLabel (wxString::Format ("%02d:%02d:%02d.%02" wxLongLongFmtSpec "d", h, m, s, f));
 +      _fixed->SetLabel (std_to_wx (t.timecode (fps)));
  }
  
 -Time
 +DCPTime
  Timecode::get (int fps) const
  {
 -      Time t = 0;
 +      DCPTime t;
        string const h = wx_to_std (_hours->GetValue ());
 -      t += lexical_cast<int> (h.empty() ? "0" : h) * 3600 * TIME_HZ;
 +      t += DCPTime::from_seconds (lexical_cast<int> (h.empty() ? "0" : h) * 3600);
        string const m = wx_to_std (_minutes->GetValue());
 -      t += lexical_cast<int> (m.empty() ? "0" : m) * 60 * TIME_HZ;
 +      t += DCPTime::from_seconds (lexical_cast<int> (m.empty() ? "0" : m) * 60);
        string const s = wx_to_std (_seconds->GetValue());
 -      t += lexical_cast<int> (s.empty() ? "0" : s) * TIME_HZ;
 +      t += DCPTime::from_seconds (lexical_cast<int> (s.empty() ? "0" : s));
        string const f = wx_to_std (_frames->GetValue());
 -      t += lexical_cast<int> (f.empty() ? "0" : f) * TIME_HZ / fps;
 +      t += DCPTime::from_seconds (lexical_cast<double> (f.empty() ? "0" : f) / fps);
  
        return t;
  }
  
+ void
+ Timecode::clear ()
+ {
+       checked_set (_hours, "");
+       checked_set (_minutes, "");
+       checked_set (_seconds, "");
+       checked_set (_frames, "");
+       _fixed->SetLabel ("");
+ }
  void
  Timecode::changed ()
  {
diff --combined src/wx/timecode.h
index b13e8c3c084b9c35514e805f59e752927fe46ae1,d0e8176f2d12a5e1cd51fce3675850c7a661c666..72b00ddeb4867065943d41b7ca41ad1295d632a8
@@@ -26,8 -26,9 +26,9 @@@ class Timecode : public wxPane
  public:
        Timecode (wxWindow *);
  
 -      void set (Time, int);
 -      Time get (int) const;
 +      void set (DCPTime, int);
 +      DCPTime get (int) const;
+       void clear ();
  
        void set_editable (bool);
  
diff --combined src/wx/timing_panel.cc
index 021ab6ab08405ecd646286937cd948ac82096d97,38891fb0e841af1b65c038d130298079e5edc2a5..a0e1f8f8a4845b8ebc9c852202d89818b55f8f01
  
  */
  
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "lib/content.h"
  #include "lib/image_content.h"
  #include "timing_panel.h"
  #include "wx_util.h"
  #include "timecode.h"
 -#include "film_editor.h"
 +#include "content_panel.h"
  
  using std::cout;
  using std::string;
+ using std::set;
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
 -TimingPanel::TimingPanel (FilmEditor* e)
 +TimingPanel::TimingPanel (ContentPanel* p)
        /* horrid hack for apparent lack of context support with wxWidgets i18n code */
 -      : FilmEditorPanel (e, S_("Timing|Timing"))
 +      : ContentSubPanel (p, S_("Timing|Timing"))
  {
        wxFlexGridSizer* grid = new wxFlexGridSizer (2, 4, 4);
        _sizer->Add (grid, 0, wxALL, 8);
  void
  TimingPanel::film_content_changed (int property)
  {
 -      ContentList cl = _editor->selected_content ();
 -      int const film_video_frame_rate = _editor->film()->video_frame_rate ();
 +      ContentList cl = _parent->selected ();
-       shared_ptr<Content> content;
-       if (cl.size() == 1) {
-               content = cl.front ();
-       }
 +      int const film_video_frame_rate = _parent->film()->video_frame_rate ();
+       /* Here we check to see if we have exactly one different value of various
+          properties, and fill the controls with that value if so.
+       */
        
        if (property == ContentProperty::POSITION) {
-               if (content) {
-                       _position->set (content->position (), film_video_frame_rate);
 -              set<Time> check;
++              set<DCPTime> check;
+               for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
+                       check.insert ((*i)->position ());
+               }
+               if (check.size() == 1) {
+                       _position->set (cl.front()->position(), film_video_frame_rate);
                } else {
-                       _position->set (DCPTime () , 24);
+                       _position->clear ();
                }
+               
        } else if (
                property == ContentProperty::LENGTH ||
                property == VideoContentProperty::VIDEO_FRAME_RATE ||
                property == VideoContentProperty::VIDEO_FRAME_TYPE
                ) {
-               if (content) {
-                       _full_length->set (content->full_length (), film_video_frame_rate);
-                       _play_length->set (content->length_after_trim (), film_video_frame_rate);
 -              set<Time> check;
++              set<DCPTime> check;
+               for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
+                       check.insert ((*i)->full_length ());
+               }
+               
+               if (check.size() == 1) {
+                       _full_length->set (cl.front()->full_length (), film_video_frame_rate);
                } else {
-                       _full_length->set (DCPTime (), 24);
-                       _play_length->set (DCPTime (), 24);
+                       _full_length->clear ();
                }
        } else if (property == ContentProperty::TRIM_START) {
-               if (content) {
-                       _trim_start->set (content->trim_start (), film_video_frame_rate);
-                       _play_length->set (content->length_after_trim (), film_video_frame_rate);
 -              set<Time> check;
++              set<DCPTime> check;
+               for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
+                       check.insert ((*i)->trim_start ());
+               }
+               
+               if (check.size() == 1) {
+                       _trim_start->set (cl.front()->trim_start (), film_video_frame_rate);
                } else {
-                       _trim_start->set (DCPTime (), 24);
-                       _play_length->set (DCPTime (), 24);
+                       _trim_start->clear ();
                }
+               
        } else if (property == ContentProperty::TRIM_END) {
-               if (content) {
-                       _trim_end->set (content->trim_end (), film_video_frame_rate);
-                       _play_length->set (content->length_after_trim (), film_video_frame_rate);
 -              set<Time> check;
++              set<DCPTime> check;
+               for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
+                       check.insert ((*i)->trim_end ());
+               }
+               
+               if (check.size() == 1) {
+                       _trim_end->set (cl.front()->trim_end (), film_video_frame_rate);
                } else {
-                       _trim_end->set (DCPTime (), 24);
-                       _play_length->set (DCPTime (), 24);
 -                      _trim_end->set (0, 24);
++                      _trim_end->clear ();
+               }
+       }
+       if (
+               property == ContentProperty::LENGTH ||
+               property == ContentProperty::TRIM_START ||
+               property == ContentProperty::TRIM_END ||
+               property == VideoContentProperty::VIDEO_FRAME_RATE ||
+               property == VideoContentProperty::VIDEO_FRAME_TYPE
+               ) {
 -              set<Time> check;
++              set<DCPTime> check;
+               for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
+                       check.insert ((*i)->length_after_trim ());
+               }
+               
+               if (check.size() == 1) {
+                       _play_length->set (cl.front()->length_after_trim (), film_video_frame_rate);
+               } else {
+                       _play_length->clear ();
                }
        }
  
        if (property == VideoContentProperty::VIDEO_FRAME_RATE) {
-               if (content) {
-                       shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (content);
+               set<float> check;
+               shared_ptr<VideoContent> vc;
+               for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
+                       vc = dynamic_pointer_cast<VideoContent> (*i);
                        if (vc) {
-                               _video_frame_rate->SetValue (std_to_wx (raw_convert<string> (vc->video_frame_rate (), 5)));
-                       } else {
-                               _video_frame_rate->SetValue ("24");
+                               check.insert (vc->video_frame_rate ());
                        }
+               }
+               if (check.size() == 1) {
+                       _video_frame_rate->SetValue (std_to_wx (raw_convert<string> (vc->video_frame_rate (), 5)));
+                       _video_frame_rate->Enable (true);
                } else {
-                       _video_frame_rate->SetValue ("24");
+                       _video_frame_rate->SetValue ("");
+                       _video_frame_rate->Enable (false);
                }
        }
  
-       shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (content);
-       shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (content);
-       _full_length->set_editable (ic && ic->still ());
-       _play_length->set_editable (!ic || !ic->still ());
-       _video_frame_rate->Enable (vc);
+       bool have_still = false;
+       for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) {
+               shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (*i);
+               if (ic && ic->still ()) {
+                       have_still = true;
+               }
+       }
+       _full_length->set_editable (have_still);
+       _play_length->set_editable (!have_still);
        _set_video_frame_rate->Enable (false);
  }
  
  void
  TimingPanel::position_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
-       if (c.size() == 1) {
-               c.front()->set_position (_position->get (_parent->film()->video_frame_rate ()));
+       for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_position (_position->get (_editor->film()->video_frame_rate ()));
++              (*i)->set_position (_position->get (_parent->film()->video_frame_rate ()));
        }
  }
  
  void
  TimingPanel::full_length_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
-       if (c.size() == 1) {
-               shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (c.front ());
+       for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
+               shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (*i);
                if (ic && ic->still ()) {
 -                      ic->set_video_length (rint (_full_length->get (_editor->film()->video_frame_rate()) * ic->video_frame_rate() / TIME_HZ));
 +                      /* XXX: No effective FRC here... is this right? */
 +                      ic->set_video_length (ContentTime (_full_length->get (_parent->film()->video_frame_rate()), FrameRateChange (1, 1)));
                }
        }
  }
  void
  TimingPanel::trim_start_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
-       if (c.size() == 1) {
-               c.front()->set_trim_start (_trim_start->get (_parent->film()->video_frame_rate ()));
+       for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_trim_start (_trim_start->get (_editor->film()->video_frame_rate ()));
++              (*i)->set_trim_start (_trim_start->get (_parent->film()->video_frame_rate ()));
        }
  }
  
  void
  TimingPanel::trim_end_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
-       if (c.size() == 1) {
-               c.front()->set_trim_end (_trim_end->get (_parent->film()->video_frame_rate ()));
+       for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_trim_end (_trim_end->get (_editor->film()->video_frame_rate ()));
++              (*i)->set_trim_end (_trim_end->get (_parent->film()->video_frame_rate ()));
        }
  }
  
  void
  TimingPanel::play_length_changed ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
-       if (c.size() == 1) {
-               c.front()->set_trim_end (c.front()->full_length() - _play_length->get (_parent->film()->video_frame_rate()) - c.front()->trim_start());
+       for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
 -              (*i)->set_trim_end ((*i)->full_length() - _play_length->get (_editor->film()->video_frame_rate()) - (*i)->trim_start());
++              (*i)->set_trim_end ((*i)->full_length() - _play_length->get (_parent->film()->video_frame_rate()) - (*i)->trim_start());
        }
  }
  
@@@ -201,9 -251,9 +252,9 @@@ TimingPanel::video_frame_rate_changed (
  void
  TimingPanel::set_video_frame_rate ()
  {
 -      ContentList c = _editor->selected_content ();
 +      ContentList c = _parent->selected ();
-       if (c.size() == 1) {
-               shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (c.front ());
+       for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
+               shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i);
                if (vc) {
                        vc->set_video_frame_rate (raw_convert<float> (wx_to_std (_video_frame_rate->GetValue ())));
                }
  void
  TimingPanel::content_selection_changed ()
  {
-       ContentList sel = _parent->selected ();
-       bool const single = sel.size() == 1;
-       /* Things that are only allowed with single selections */
-       _position->Enable (single);
-       _full_length->Enable (single);
-       _trim_start->Enable (single);
-       _trim_end->Enable (single);
-       _play_length->Enable (single);
-       _video_frame_rate->Enable (single);
 -      bool const e = !_editor->selected_content().empty ();
++      bool const e = !_parent->selected().empty ();
+       _position->Enable (e);
+       _full_length->Enable (e);
+       _trim_start->Enable (e);
+       _trim_end->Enable (e);
+       _play_length->Enable (e);
+       _video_frame_rate->Enable (e);
        
        film_content_changed (ContentProperty::POSITION);
        film_content_changed (ContentProperty::LENGTH);
diff --combined src/wx/video_panel.cc
index f00edd8fff0f69c3168911694a22b906581fd567,fcb1b9f806069babf0f2032ae01a8ba1adf035b6..cd831baed7cbc5b97324d97a1a603773d0ee0614
@@@ -28,9 -28,9 +28,9 @@@
  #include "filter_dialog.h"
  #include "video_panel.h"
  #include "wx_util.h"
 -#include "film_editor.h"
  #include "content_colour_conversion_dialog.h"
  #include "content_widget.h"
 +#include "content_panel.h"
  
  using std::vector;
  using std::string;
@@@ -64,8 -64,8 +64,8 @@@ scale_to_index (VideoContentScale scale
        assert (false);
  }
  
 -VideoPanel::VideoPanel (FilmEditor* e)
 -      : FilmEditorPanel (e, _("Video"))
 +VideoPanel::VideoPanel (ContentPanel* p)
 +      : ContentSubPanel (p, _("Video"))
  {
        wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
        _sizer->Add (grid, 0, wxALL, 8);
@@@ -222,7 -222,7 +222,7 @@@ VideoPanel::film_changed (Film::Propert
  void
  VideoPanel::film_content_changed (int property)
  {
 -      VideoContentList vc = _editor->selected_video_content ();
 +      VideoContentList vc = _parent->selected_video ();
        shared_ptr<VideoContent> vcs;
        shared_ptr<FFmpegContent> fcs;
        if (!vc.empty ()) {
  void
  VideoPanel::edit_filters_clicked ()
  {
 -      FFmpegContentList c = _editor->selected_ffmpeg_content ();
 +      FFmpegContentList c = _parent->selected_ffmpeg ();
        if (c.size() != 1) {
                return;
        }
  void
  VideoPanel::setup_description ()
  {
 -      VideoContentList vc = _editor->selected_video_content ();
 +      VideoContentList vc = _parent->selected_video ();
        if (vc.empty ()) {
                _description->SetLabel ("");
                return;
        }
  
        Crop const crop = vcs->crop ();
 -      if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != libdcp::Size (0, 0)) {
 -              libdcp::Size cropped = vcs->video_size_after_crop ();
 +      if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != dcp::Size (0, 0)) {
 +              dcp::Size cropped = vcs->video_size_after_crop ();
                d << wxString::Format (
                        _("Cropped to %dx%d (%.2f:1)\n"),
                        cropped.width, cropped.height,
                ++lines;
        }
  
 -      libdcp::Size const container_size = _editor->film()->frame_size ();
 -      libdcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size);
 +      dcp::Size const container_size = _parent->film()->frame_size ();
 +      dcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size, 1);
  
        if (scaled != vcs->video_size_after_crop ()) {
                d << wxString::Format (
  
        d << wxString::Format (_("Content frame rate %.4f\n"), vcs->video_frame_rate ());
        ++lines;
 -      FrameRateChange frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ());
 +      FrameRateChange frc (vcs->video_frame_rate(), _parent->film()->video_frame_rate ());
-       d << std_to_wx (frc.description) << "\n";
+       d << std_to_wx (frc.description ()) << "\n";
        ++lines;
  
        for (int i = lines; i < 6; ++i) {
  void
  VideoPanel::edit_colour_conversion_clicked ()
  {
 -      VideoContentList vc = _editor->selected_video_content ();
 +      VideoContentList vc = _parent->selected_video ();
        if (vc.size() != 1) {
                return;
        }
  void
  VideoPanel::content_selection_changed ()
  {
 -      VideoContentList sel = _editor->selected_video_content ();
 +      VideoContentList sel = _parent->selected_video ();
        bool const single = sel.size() == 1;
  
        _left_crop->set_content (sel);
index 70f29b998933d355df2b793797ad36d38c9a0205,c41e8618900af948c77cd85e1f8e4fabb7bd410b..f0a544e50a9f57ed178fb4705696fb14ca810d9f
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  
  */
  
 +/** @file  test/film_metadata_test.cc
 + *  @brief Test some basic reading/writing of film metadata.
 + */
 +
  #include <sstream>
  #include <boost/test/unit_test.hpp>
  #include <boost/filesystem.hpp>
  #include "test.h"
  
  using std::string;
- using std::stringstream;
  using std::list;
  using boost::shared_ptr;
  
  BOOST_AUTO_TEST_CASE (film_metadata_test)
  {
 -      string const test_film = "build/test/film_metadata_test";
 -      
 -      if (boost::filesystem::exists (test_film)) {
 -              boost::filesystem::remove_all (test_film);
 -      }
 +      shared_ptr<Film> f = new_test_film ("film_metadata_test");
 +      boost::filesystem::path dir = test_film_dir ("film_metadata_test");
  
 -      shared_ptr<Film> f (new Film (test_film));
        f->_isdcf_date = boost::gregorian::from_undelimited_string ("20130211");
        BOOST_CHECK (f->container() == 0);
        BOOST_CHECK (f->dcp_content_type() == 0);
@@@ -52,9 -51,9 +51,9 @@@
  
        list<string> ignore;
        ignore.push_back ("Key");
 -      check_xml ("test/data/metadata.xml.ref", test_film + "/metadata.xml", ignore);
 +      check_xml ("test/data/metadata.xml.ref", dir.string() + "/metadata.xml", ignore);
  
 -      shared_ptr<Film> g (new Film (test_film));
 +      shared_ptr<Film> g (new Film (dir));
        g->read_metadata ();
  
        BOOST_CHECK_EQUAL (g->name(), "fred");
@@@ -62,5 -61,5 +61,5 @@@
        BOOST_CHECK_EQUAL (g->container(), Ratio::from_id ("185"));
        
        g->write_metadata ();
 -      check_xml ("test/data/metadata.xml.ref", test_film + "/metadata.xml", ignore);
 +      check_xml ("test/data/metadata.xml.ref", dir.string() + "/metadata.xml", ignore);
  }
diff --combined test/job_test.cc
index c1b66d4e01f38f36127c49087c79473cbdb20739,7d2911c4e756259f917a2fcf1e6f368c0fca67ce..97a23b946a765eb7c758c41678960d3f0d69ea81
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
 +    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  
  */
  
 +/** @file  test/job_test.cc
 + *  @brief Basic tests of Job and JobManager.
 + */
 +
  #include <boost/test/unit_test.hpp>
  #include "lib/job.h"
  #include "lib/job_manager.h"
@@@ -58,10 -54,6 +58,6 @@@ public
        string name () const {
                return "";
        }
-       string json_name () const {
-               return "";
-       }
  };
  
  BOOST_AUTO_TEST_CASE (job_manager_test)