Merge master.
authorCarl Hetherington <cth@carlh.net>
Fri, 12 Sep 2014 22:16:31 +0000 (23:16 +0100)
committerCarl Hetherington <cth@carlh.net>
Fri, 12 Sep 2014 22:16:31 +0000 (23:16 +0100)
1  2 
ChangeLog
src/lib/film.cc
src/lib/player.cc
src/lib/subtitle_content.cc
src/lib/subtitle_content.h
src/wx/subtitle_panel.cc
src/wx/subtitle_panel.h

diff --combined ChangeLog
index 183339f61c1b83ecbc57ae5ffb8d42dc0fa66502,6803d355c81ec0c61565ce70307403746de54eb0..222a0eea15acdcf95e8a3407f9fbac80f21af7b8
+++ b/ChangeLog
@@@ -1,18 -1,7 +1,20 @@@
  2014-09-12  Carl Hetherington  <cth@carlh.net>
  
 +      * Version 2.0.9 released.
 +
 +2014-09-12  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add "re-examine" option to content context menu (#339).
 +
 +2014-09-11  Carl Hetherington  <cth@carlh.net>
 +
 +      * Restore encoding optimisations for still-image sources.
 +
 +      * Add option to re-make signing chain with specified organisation,
 +      common names etc. (#354)
 +
+       * Allow separate X and Y scale for subtitles (#337).
  2014-09-10  Carl Hetherington  <cth@carlh.net>
  
        * Allow DCP names to be created using the ISDCF template and then
  
        * Fix hidden advanced preferences button in some locales.
  
 -2014-09-08  Carl Hetherington  <cth@carlh.net>
 +      * Version 2.0.8 released.
 +
 +2014-09-10  Carl Hetherington  <cth@carlh.net>
  
 -      * Version 1.73.4 released.
 +      * Fix loading of 1.x films.
 +
 +      * Fix crash on audio analysis in some cases.
 +
 +2014-09-09  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.7 released.
 +
 +2014-09-09  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.6 released.
 +
 +2014-09-09  Carl Hetherington  <cth@carlh.net>
 +
 +      * Fix missing OS X dependencies.
 +
 +      * Use a different directory for DCP-o-matic 2
 +      configuration (not the same as 1.x).
  
  2014-09-08  Carl Hetherington  <cth@carlh.net>
  
 -      * Fix failure to load Targa files.
 +      * Version 2.0.5 released.
  
 -2014-09-07  Carl Hetherington  <cth@carlh.net>
 +      * Fix hidden advanced preferences button in some locales.
  
 -      * Version 1.73.3 released.
 +2014-09-08  Carl Hetherington  <cth@carlh.net>
 +
 +      * Fix failure to load Targa files.
  
  2014-09-07  Carl Hetherington  <cth@carlh.net>
  
  
        * Fix a few bad fuzzy translations from the preferences dialog.
  
 -2014-09-03  Carl Hetherington  <cth@carlh.net>
 -
 -      * Version 1.73.2 released.
 -
  2014-09-03  Carl Hetherington  <cth@carlh.net>
  
        * Fix server certificate downloads on OS X (#376).
  
  2014-08-29  Carl Hetherington  <cth@carlh.net>
  
 +      * Version 2.0.4 released.
 +
 +2014-08-24  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.3 released.
 +
 +2014-08-24  Carl Hetherington  <cth@carlh.net>
 +
 +      * Version 2.0.2 released.
 +
 +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.
        * Some improvements to the manual.
  
  2014-08-26  Carl Hetherington  <cth@carlh.net>
        * Attempt to fix random crashes on OS X (especially during encodes)
        thought to be caused by multiple threads using (different) stringstreams
        at the same time; see src/lib/safe_stringstream.
 +>>>>>>> origin/master
  
  2014-08-09  Carl Hetherington  <cth@carlh.net>
  
  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 src/lib/film.cc
index 475dd68448628afba155ac59981b133f1b1e84f7,54503ef72c6c7bf5b8c21184fd736d5ba7d949cf..26810992175d8da91065dfc4052d7da424afa0a2
  #include <unistd.h>
  #include <boost/filesystem.hpp>
  #include <boost/algorithm/string.hpp>
 -#include <boost/date_time.hpp>
 +#include <boost/lexical_cast.hpp>
  #include <libxml++/libxml++.h>
  #include <libcxml/cxml.h>
 -#include <libdcp/signer_chain.h>
 -#include <libdcp/cpl.h>
 -#include <libdcp/signer.h>
 -#include <libdcp/util.h>
 -#include <libdcp/kdm.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/cpl.h>
 +#include <dcp/signer.h>
 +#include <dcp/util.h>
 +#include <dcp/local_time.h>
 +#include <dcp/raw_convert.h>
  #include "film.h"
  #include "job.h"
  #include "util.h"
@@@ -76,10 -77,9 +76,10 @@@ using boost::ends_with
  using boost::starts_with;
  using boost::optional;
  using boost::is_any_of;
 -using libdcp::Size;
 -using libdcp::Signer;
 -using libdcp::raw_convert;
 +using dcp::Size;
 +using dcp::Signer;
 +using dcp::raw_convert;
 +using dcp::raw_convert;
  
  #define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
  #define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, Log::TYPE_GENERAL);
   * 7 -> 8
   * Use <Scale> tag in <VideoContent> rather than <Ratio>.
   * 8 -> 9
 - * DCI -> ISDCF.
 + * DCI -> ISDCF
+  * 9 -> 10
+  * Subtitle X and Y scale.
 + *
 + * Bumped to 32 for 2.0 branch; some times are expressed in Times rather
 + * than frames now.
   */
 -int const Film::current_state_version = 10;
 +int const Film::current_state_version = 32;
  
  /** Construct a Film object in a given directory.
   *
@@@ -110,6 -109,7 +112,6 @@@ Film::Film (boost::filesystem::path dir
        , _container (Config::instance()->default_container ())
        , _resolution (RESOLUTION_2K)
        , _scaler (Scaler::from_id ("bicubic"))
 -      , _with_subtitles (false)
        , _signed (true)
        , _encrypted (false)
        , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
        , _three_d (false)
        , _sequence_video (true)
        , _interop (false)
 +      , _burn_subtitles (false)
        , _state_version (current_state_version)
        , _dirty (false)
  {
@@@ -183,12 -182,12 +185,12 @@@ Film::video_identifier () cons
                s << "_S";
        }
  
 -      if (_three_d) {
 -              s << "_3D";
 +      if (_burn_subtitles) {
 +              s << "_B";
        }
  
 -      if (_with_subtitles) {
 -              s << "_WS";
 +      if (_three_d) {
 +              s << "_3D";
        }
  
        return s.str ();
@@@ -228,12 -227,6 +230,12 @@@ Film::audio_mxf_filename () cons
        return filename_safe_name() + "_audio.mxf";
  }
  
 +boost::filesystem::path
 +Film::subtitle_xml_filename () const
 +{
 +      return filename_safe_name() + "_subtitle.xml";
 +}
 +
  string
  Film::filename_safe_name () const
  {
@@@ -376,6 -369,7 +378,6 @@@ Film::metadata () cons
  
        root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution));
        root->add_child("Scaler")->add_child_text (_scaler->id ());
 -      root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0");
        root->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth));
        _isdcf_metadata.as_xml (root->add_child ("ISDCFMetadata"));
        root->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate));
        root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0");
        root->add_child("SequenceVideo")->add_child_text (_sequence_video ? "1" : "0");
        root->add_child("Interop")->add_child_text (_interop ? "1" : "0");
 +      root->add_child("BurnSubtitles")->add_child_text (_burn_subtitles ? "1" : "0");
        root->add_child("Signed")->add_child_text (_signed ? "1" : "0");
        root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
        root->add_child("Key")->add_child_text (_key.hex ());
@@@ -448,6 -441,7 +450,6 @@@ Film::read_metadata (
  
        _resolution = string_to_resolution (f.string_child ("Resolution"));
        _scaler = Scaler::from_id (f.string_child ("Scaler"));
 -      _with_subtitles = f.bool_child ("WithSubtitles");
        _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
        _video_frame_rate = f.number_child<int> ("VideoFrameRate");
        _signed = f.optional_bool_child("Signed").get_value_or (true);
        _sequence_video = f.bool_child ("SequenceVideo");
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
 -      _key = libdcp::Key (f.string_child ("Key"));
 +      if (_state_version >= 32) {
 +              _burn_subtitles = f.bool_child ("BurnSubtitles");
 +      }
 +      _key = dcp::Key (f.string_child ("Key"));
  
        list<string> notes;
        /* This method is the only one that can return notes (so far) */
@@@ -594,9 -585,9 +596,9 @@@ Film::isdcf_name (bool if_created_now) 
        /* XXX: this uses the first bit of content only */
  
        /* The standard says we don't do this for trailers, for some strange reason */
 -      if (dcp_content_type() && dcp_content_type()->libdcp_kind() != libdcp::TRAILER) {
 -              Ratio const * content_ratio = 0;
 +      if (dcp_content_type() && dcp_content_type()->libdcp_kind() != dcp::TRAILER) {
                ContentList cl = content ();
 +              Ratio const * content_ratio = 0;
                for (ContentList::iterator i = cl.begin(); i != cl.end(); ++i) {
                        shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i);
                        if (vc) {
@@@ -698,6 -689,7 +700,6 @@@ Film::dcp_name (bool if_created_now) co
        return name();
  }
  
 -
  void
  Film::set_directory (boost::filesystem::path d)
  {
@@@ -747,6 -739,13 +749,6 @@@ Film::set_scaler (Scaler const * s
        signal_changed (SCALER);
  }
  
 -void
 -Film::set_with_subtitles (bool w)
 -{
 -      _with_subtitles = w;
 -      signal_changed (WITH_SUBTITLES);
 -}
 -
  void
  Film::set_j2k_bandwidth (int b)
  {
@@@ -789,13 -788,6 +791,13 @@@ Film::set_interop (bool i
        signal_changed (INTEROP);
  }
  
 +void
 +Film::set_burn_subtitles (bool b)
 +{
 +      _burn_subtitles = b;
 +      signal_changed (BURN_SUBTITLES);
 +}
 +
  void
  Film::signal_changed (Property p)
  {
@@@ -877,7 -869,7 +879,7 @@@ Film::j2c_path (int f, Eyes e, bool t) 
        return file (p);
  }
  
 -/** Find all the DCPs in our directory that can be libdcp::DCP::read() and return details of their CPLs */
 +/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs */
  vector<CPLSummary>
  Film::cpls () const
  {
                        ) {
  
                        try {
 -                              libdcp::DCP dcp (*i);
 +                              dcp::DCP dcp (*i);
                                dcp.read ();
                                out.push_back (
                                        CPLSummary (
 -                                              i->path().leaf().string(), dcp.cpls().front()->id(), dcp.cpls().front()->name(), dcp.cpls().front()->filename()
 +                                              i->path().leaf().string(),
 +                                              dcp.cpls().front()->id(),
 +                                              dcp.cpls().front()->annotation_text(),
 +                                              dcp.cpls().front()->file()
                                                )
                                        );
                        } catch (...) {
@@@ -942,13 -931,6 +944,13 @@@ Film::content () cons
        return _playlist->content ();
  }
  
 +void
 +Film::examine_content (shared_ptr<Content> c)
 +{
 +      shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
 +      JobManager::instance()->add (j);
 +}
 +
  void
  Film::examine_and_add_content (shared_ptr<Content> c)
  {
@@@ -1004,22 -986,22 +1006,22 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
  }
  
 -bool
 -Film::has_subtitles () const
 +int
 +Film::best_video_frame_rate () const
  {
 -      return _playlist->has_subtitles ();
 +      return _playlist->best_dcp_frame_rate ();
  }
  
 -OutputVideoFrame
 -Film::best_video_frame_rate () const
 +FrameRateChange
 +Film::active_frame_rate_change (DCPTime t) const
  {
 -      return _playlist->best_dcp_frame_rate ();
 +      return _playlist->active_frame_rate_change (t, video_frame_rate ());
  }
  
  void
@@@ -1040,7 -1022,31 +1042,7 @@@ Film::playlist_changed (
        signal_changed (CONTENT);
  }     
  
 -OutputAudioFrame
 -Film::time_to_audio_frames (Time t) const
 -{
 -      return divide_with_round (t * audio_frame_rate (), TIME_HZ);
 -}
 -
 -OutputVideoFrame
 -Film::time_to_video_frames (Time t) const
 -{
 -      return divide_with_round (t * video_frame_rate (), TIME_HZ);
 -}
 -
 -Time
 -Film::audio_frames_to_time (OutputAudioFrame f) const
 -{
 -      return divide_with_round (f * TIME_HZ, audio_frame_rate ());
 -}
 -
 -Time
 -Film::video_frames_to_time (OutputVideoFrame f) const
 -{
 -      return divide_with_round (f * TIME_HZ, video_frame_rate ());
 -}
 -
 -OutputAudioFrame
 +int
  Film::audio_frame_rate () const
  {
        /* XXX */
@@@ -1056,62 -1062,61 +1058,62 @@@ Film::set_sequence_video (bool s
  }
  
  /** @return Size of the largest possible image in whatever resolution we are using */
 -libdcp::Size
 +dcp::Size
  Film::full_frame () const
  {
        switch (_resolution) {
        case RESOLUTION_2K:
 -              return libdcp::Size (2048, 1080);
 +              return dcp::Size (2048, 1080);
        case RESOLUTION_4K:
 -              return libdcp::Size (4096, 2160);
 +              return dcp::Size (4096, 2160);
        }
  
        assert (false);
 -      return libdcp::Size ();
 +      return dcp::Size ();
  }
  
  /** @return Size of the frame */
 -libdcp::Size
 +dcp::Size
  Film::frame_size () const
  {
 -      return fit_ratio_within (container()->ratio(), full_frame ());
 +      return fit_ratio_within (container()->ratio(), full_frame (), 1);
  }
  
 -/** @param from KDM from time in local time.
 - *  @param to KDM to time in local time.
 - */
 -libdcp::KDM
 +dcp::EncryptedKDM
  Film::make_kdm (
 -      shared_ptr<libdcp::Certificate> target,
 +      dcp::Certificate target,
        boost::filesystem::path cpl_file,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime until,
 -      libdcp::KDM::Formulation formulation
 +      dcp::LocalTime from,
 +      dcp::LocalTime until,
 +      dcp::Formulation formulation
        ) const
  {
 -      shared_ptr<const Signer> signer = make_signer ();
 -
 -      time_t now = time (0);
 -      struct tm* tm = localtime (&now);
 -      string const issue_date = libdcp::tm_to_string (tm);
 +      shared_ptr<const dcp::CPL> cpl (new dcp::CPL (cpl_file));
 +      shared_ptr<const dcp::Signer> signer = Config::instance()->signer();
 +      if (!signer->valid ()) {
 +              throw InvalidSignerError ();
 +      }
        
 -      return libdcp::KDM (cpl_file, signer, target, key (), from, until, "DCP-o-matic", issue_date, formulation);
 +      return dcp::DecryptedKDM (
 +              cpl, key(), from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string()
 +              ).encrypt (signer, target, formulation);
  }
  
 -list<libdcp::KDM>
 +list<dcp::EncryptedKDM>
  Film::make_kdms (
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path dcp,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime until,
 -      libdcp::KDM::Formulation formulation
 +      dcp::LocalTime from,
 +      dcp::LocalTime until,
 +      dcp::Formulation formulation
        ) const
  {
 -      list<libdcp::KDM> kdms;
 +      list<dcp::EncryptedKDM> kdms;
  
        for (list<shared_ptr<Screen> >::iterator i = screens.begin(); i != screens.end(); ++i) {
 -              kdms.push_back (make_kdm ((*i)->certificate, dcp, from, until, formulation));
 +              if ((*i)->certificate) {
 +                      kdms.push_back (make_kdm ((*i)->certificate.get(), dcp, from, until, formulation));
 +              }
        }
  
        return kdms;
  uint64_t
  Film::required_disk_space () const
  {
 -      return uint64_t (j2k_bandwidth() / 8) * length() / TIME_HZ;
 +      return uint64_t (j2k_bandwidth() / 8) * length().seconds();
  }
  
  /** This method checks the disk that the Film is on and tries to decide whether or not
@@@ -1141,3 -1146,10 +1143,3 @@@ Film::should_be_enough_disk_space (doub
        available = double (s.available) / 1073741824.0f;
        return (available - required) > 1;
  }
 -
 -FrameRateChange
 -Film::active_frame_rate_change (Time t) const
 -{
 -      return _playlist->active_frame_rate_change (t, video_frame_rate ());
 -}
 -
diff --combined src/lib/player.cc
index e46d539f872c90fb1e130cdcd45049f77ddc169b,8063d1212971afe2e9bf682a84d253361b902bac..f83c9563b29ce80cab262ad02c22c913219cf835
  */
  
  #include <stdint.h>
 +#include <algorithm>
  #include "player.h"
  #include "film.h"
  #include "ffmpeg_decoder.h"
 +#include "audio_buffers.h"
  #include "ffmpeg_content.h"
  #include "image_decoder.h"
  #include "image_content.h"
  #include "sndfile_decoder.h"
  #include "sndfile_content.h"
  #include "subtitle_content.h"
 +#include "subrip_decoder.h"
 +#include "subrip_content.h"
 +#include "dcp_content.h"
  #include "playlist.h"
  #include "job.h"
  #include "image.h"
 -#include "image_proxy.h"
 +#include "raw_image_proxy.h"
  #include "ratio.h"
 -#include "resampler.h"
  #include "log.h"
  #include "scaler.h"
 -#include "player_video_frame.h"
 +#include "render_subtitles.h"
 +#include "config.h"
 +#include "content_video.h"
 +#include "player_video.h"
  #include "frame_rate_change.h"
 +#include "dcp_content.h"
 +#include "dcp_decoder.h"
 +#include "dcp_subtitle_content.h"
 +#include "dcp_subtitle_decoder.h"
  
  #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
  
@@@ -55,21 -44,23 +55,21 @@@ using std::list
  using std::cout;
  using std::min;
  using std::max;
 +using std::min;
  using std::vector;
  using std::pair;
  using std::map;
 +using std::make_pair;
  using boost::shared_ptr;
  using boost::weak_ptr;
  using boost::dynamic_pointer_cast;
 +using boost::optional;
  
  Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
        : _film (f)
        , _playlist (p)
 -      , _video (true)
 -      , _audio (true)
        , _have_valid_pieces (false)
 -      , _video_position (0)
 -      , _audio_position (0)
 -      , _audio_merger (f->audio_channels(), bind (&Film::time_to_audio_frames, f.get(), _1), bind (&Film::audio_frames_to_time, f.get(), _1))
 -      , _last_emit_was_black (false)
 +      , _approximate_size (false)
  {
        _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
        _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3));
  }
  
  void
 -Player::disable_video ()
 -{
 -      _video = false;
 -}
 -
 -void
 -Player::disable_audio ()
 +Player::setup_pieces ()
  {
 -      _audio = false;
 -}
 +      list<shared_ptr<Piece> > old_pieces = _pieces;
 +      _pieces.clear ();
  
 -bool
 -Player::pass ()
 -{
 -      if (!_have_valid_pieces) {
 -              setup_pieces ();
 -      }
 +      ContentList content = _playlist->content ();
  
 -      Time earliest_t = TIME_MAX;
 -      shared_ptr<Piece> earliest;
 -      enum {
 -              VIDEO,
 -              AUDIO
 -      } type = VIDEO;
 +      for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
  
 -      for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              if ((*i)->decoder->done () || (*i)->content->length_after_trim() == 0) {
 +              if (!(*i)->paths_valid ()) {
                        continue;
                }
 -
 -              shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder);
 -              shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 -
 -              if (_video && vd) {
 -                      if ((*i)->video_position < earliest_t) {
 -                              earliest_t = (*i)->video_position;
 -                              earliest = *i;
 -                              type = VIDEO;
 +              
 +              shared_ptr<Decoder> decoder;
 +              optional<FrameRateChange> frc;
 +
 +              /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */
 +              DCPTime best_overlap_t;
 +              shared_ptr<VideoContent> best_overlap;
 +              for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
 +                      shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
 +                      if (!vc) {
 +                              continue;
 +                      }
 +                      
 +                      DCPTime const overlap = max (vc->position(), (*i)->position()) - min (vc->end(), (*i)->end());
 +                      if (overlap > best_overlap_t) {
 +                              best_overlap = vc;
 +                              best_overlap_t = overlap;
                        }
                }
  
 -              if (_audio && ad && ad->has_audio ()) {
 -                      if ((*i)->audio_position < earliest_t) {
 -                              earliest_t = (*i)->audio_position;
 -                              earliest = *i;
 -                              type = AUDIO;
 -                      }
 +              optional<FrameRateChange> best_overlap_frc;
 +              if (best_overlap) {
 +                      best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
 +              } else {
 +                      /* No video overlap; e.g. if the DCP is just audio */
 +                      best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
                }
 -      }
  
 -      if (!earliest) {
 -              flush ();
 -              return true;
 -      }
 +              /* FFmpeg */
 +              shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
 +              if (fc) {
 +                      decoder.reset (new FFmpegDecoder (fc, _film->log()));
 +                      frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
 +              }
  
 -      switch (type) {
 -      case VIDEO:
 -              if (earliest_t > _video_position) {
 -                      emit_black ();
 -              } else {
 -                      if (earliest->repeating ()) {
 -                              earliest->repeat (this);
 -                      } else {
 -                              earliest->decoder->pass ();
 -                      }
 +              shared_ptr<const DCPContent> dc = dynamic_pointer_cast<const DCPContent> (*i);
 +              if (dc) {
 +                      decoder.reset (new DCPDecoder (dc, _film->log ()));
 +                      frc = FrameRateChange (dc->video_frame_rate(), _film->video_frame_rate());
                }
 -              break;
  
 -      case AUDIO:
 -              if (earliest_t > _audio_position) {
 -                      emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position));
 -              } else {
 -                      earliest->decoder->pass ();
 -
 -                      if (earliest->decoder->done()) {
 -                              shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (earliest->content);
 -                              assert (ac);
 -                              shared_ptr<Resampler> re = resampler (ac, false);
 -                              if (re) {
 -                                      shared_ptr<const AudioBuffers> b = re->flush ();
 -                                      if (b->frames ()) {
 -                                              process_audio (
 -                                                      earliest,
 -                                                      b,
 -                                                      ac->audio_length() * ac->output_audio_frame_rate() / ac->content_audio_frame_rate(),
 -                                                      true
 -                                                      );
 -                                      }
 +              /* ImageContent */
 +              shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
 +              if (ic) {
 +                      /* See if we can re-use an old ImageDecoder */
 +                      for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
 +                              shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
 +                              if (imd && imd->content() == ic) {
 +                                      decoder = imd;
                                }
                        }
 -              }
 -              break;
 -      }
  
 -      if (_audio) {
 -              boost::optional<Time> audio_done_up_to;
 -              for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -                      if ((*i)->decoder->done ()) {
 -                              continue;
 +                      if (!decoder) {
 +                              decoder.reset (new ImageDecoder (ic));
                        }
  
 -                      shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 -                      if (ad && ad->has_audio ()) {
 -                              audio_done_up_to = min (audio_done_up_to.get_value_or (TIME_MAX), (*i)->audio_position);
 -                      }
 +                      frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
                }
  
 -              if (audio_done_up_to) {
 -                      TimedAudioBuffers<Time> tb = _audio_merger.pull (audio_done_up_to.get ());
 -                      Audio (tb.audio, tb.time);
 -                      _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +              /* SndfileContent */
 +              shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
 +              if (sc) {
 +                      decoder.reset (new SndfileDecoder (sc));
 +                      frc = best_overlap_frc;
                }
 -      }
 -              
 -      return false;
 -}
 -
 -/** @param extra Amount of extra time to add to the content frame's time (for repeat) */
 -void
 -Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const ImageProxy> image, Eyes eyes, Part part, bool same, VideoContent::Frame frame, Time extra)
 -{
 -      /* Keep a note of what came in so that we can repeat it if required */
 -      _last_incoming_video.weak_piece = weak_piece;
 -      _last_incoming_video.image = image;
 -      _last_incoming_video.eyes = eyes;
 -      _last_incoming_video.part = part;
 -      _last_incoming_video.same = same;
 -      _last_incoming_video.frame = frame;
 -      _last_incoming_video.extra = extra;
 -      
 -      shared_ptr<Piece> piece = weak_piece.lock ();
 -      if (!piece) {
 -              return;
 -      }
  
 -      shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
 -      assert (content);
 -
 -      FrameRateChange frc (content->video_frame_rate(), _film->video_frame_rate());
 -      if (frc.skip && (frame % 2) == 1) {
 -              return;
 -      }
 -
 -      Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate());
 -      if (content->trimmed (relative_time)) {
 -              return;
 -      }
 -
 -      Time const time = content->position() + relative_time + extra - content->trim_start ();
 -      libdcp::Size const image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
 -
 -      shared_ptr<PlayerVideoFrame> pi (
 -              new PlayerVideoFrame (
 -                      image,
 -                      content->crop(),
 -                      image_size,
 -                      _video_container_size,
 -                      _film->scaler(),
 -                      eyes,
 -                      part,
 -                      content->colour_conversion()
 -                      )
 -              );
 -      
 -      if (_film->with_subtitles ()) {
 -              for (list<Subtitle>::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      if (i->covers (time)) {
 -                              /* This may be true for more than one of _subtitles, but the last (latest-starting)
 -                                 one is the one we want to use, so that's ok.
 -                              */
 -                              Position<int> const container_offset (
 -                                      (_video_container_size.width - image_size.width) / 2,
 -                                      (_video_container_size.height - image_size.width) / 2
 -                                      );
 -                              
 -                              pi->set_subtitle (i->out_image(), i->out_position() + container_offset);
 -                      }
 +              /* SubRipContent */
 +              shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
 +              if (rc) {
 +                      decoder.reset (new SubRipDecoder (rc));
 +                      frc = best_overlap_frc;
                }
 -      }
  
 -      /* Clear out old subtitles */
 -      for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ) {
 -              list<Subtitle>::iterator j = i;
 -              ++j;
 -              
 -              if (i->ends_before (time)) {
 -                      _subtitles.erase (i);
 +              /* DCPSubtitleContent */
 +              shared_ptr<const DCPSubtitleContent> dsc = dynamic_pointer_cast<const DCPSubtitleContent> (*i);
 +              if (dsc) {
 +                      decoder.reset (new DCPSubtitleDecoder (dsc));
 +                      frc = best_overlap_frc;
                }
  
 -              i = j;
 +              _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
        }
  
 -#ifdef DCPOMATIC_DEBUG
 -      _last_video = piece->content;
 -#endif
 +      _have_valid_pieces = true;
 +}
  
 -      Video (pi, same, time);
 +void
 +Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
 +{
 +      shared_ptr<Content> c = w.lock ();
 +      if (!c) {
 +              return;
 +      }
  
 -      _last_emit_was_black = false;
 -      _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate());
 +      if (
 +              property == ContentProperty::POSITION ||
 +              property == ContentProperty::LENGTH ||
 +              property == ContentProperty::TRIM_START ||
 +              property == ContentProperty::TRIM_END ||
 +              property == ContentProperty::PATH ||
 +              property == VideoContentProperty::VIDEO_FRAME_TYPE ||
 +              property == DCPContentProperty::CAN_BE_PLAYED
 +              ) {
 +              
 +              _have_valid_pieces = false;
 +              Changed (frequent);
  
 -      if (frc.repeat > 1 && !piece->repeating ()) {
 -              piece->set_repeat (_last_incoming_video, frc.repeat - 1);
 +      } else if (
 +              property == SubtitleContentProperty::USE_SUBTITLES ||
 +              property == SubtitleContentProperty::SUBTITLE_X_OFFSET ||
 +              property == SubtitleContentProperty::SUBTITLE_Y_OFFSET ||
-               property == SubtitleContentProperty::SUBTITLE_SCALE ||
++              property == SubtitleContentProperty::SUBTITLE_X_SCALE ||
++              property == SubtitleContentProperty::SUBTITLE_Y_SCALE ||
 +              property == VideoContentProperty::VIDEO_CROP ||
 +              property == VideoContentProperty::VIDEO_SCALE ||
 +              property == VideoContentProperty::VIDEO_FRAME_RATE
 +              ) {
 +              
 +              Changed (frequent);
        }
  }
  
  /** @param already_resampled true if this data has already been through the chain up to the resampler */
  void
 -Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame, bool already_resampled)
 +Player::playlist_changed ()
  {
 -      shared_ptr<Piece> piece = weak_piece.lock ();
 -      if (!piece) {
 -              return;
 -      }
 +      _have_valid_pieces = false;
 +      Changed (false);
 +}
  
 -      shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> (piece->content);
 -      assert (content);
 +void
 +Player::set_video_container_size (dcp::Size s)
 +{
 +      _video_container_size = s;
  
 -      if (!already_resampled) {
 -              /* Gain */
 -              if (content->audio_gain() != 0) {
 -                      shared_ptr<AudioBuffers> gain (new AudioBuffers (audio));
 -                      gain->apply_gain (content->audio_gain ());
 -                      audio = gain;
 -              }
 -              
 -              /* Resample */
 -              if (content->content_audio_frame_rate() != content->output_audio_frame_rate()) {
 -                      shared_ptr<Resampler> r = resampler (content, true);
 -                      pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> ro = r->run (audio, frame);
 -                      audio = ro.first;
 -                      frame = ro.second;
 -              }
 -      }
 -      
 -      Time const relative_time = _film->audio_frames_to_time (frame);
 +      _black_image.reset (new Image (PIX_FMT_RGB24, _video_container_size, true));
 +      _black_image->make_black ();
 +}
  
 -      if (content->trimmed (relative_time)) {
 -              return;
 -      }
 +void
 +Player::film_changed (Film::Property p)
 +{
 +      /* Here we should notice Film properties that affect our output, and
 +         alert listeners that our output now would be different to how it was
 +         last time we were run.
 +      */
  
 -      Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time - content->trim_start ();
 -      
 -      /* Remap channels */
 -      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->frames()));
 -      dcp_mapped->make_silent ();
 -
 -      AudioMapping map = content->audio_mapping ();
 -      for (int i = 0; i < map.content_channels(); ++i) {
 -              for (int j = 0; j < _film->audio_channels(); ++j) {
 -                      if (map.get (i, static_cast<libdcp::Channel> (j)) > 0) {
 -                              dcp_mapped->accumulate_channel (
 -                                      audio.get(),
 -                                      i,
 -                                      static_cast<libdcp::Channel> (j),
 -                                      map.get (i, static_cast<libdcp::Channel> (j))
 -                                      );
 -                      }
 -              }
 +      if (p == Film::SCALER || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
 +              Changed (false);
        }
 +}
  
 -      audio = dcp_mapped;
 -
 -      /* We must cut off anything that comes before the start of all time */
 -      if (time < 0) {
 -              int const frames = - time * _film->audio_frame_rate() / TIME_HZ;
 -              if (frames >= audio->frames ()) {
 -                      return;
 +list<PositionImage>
 +Player::transform_image_subtitles (list<ImageSubtitle> subs) const
 +{
 +      list<PositionImage> all;
 +      
 +      for (list<ImageSubtitle>::const_iterator i = subs.begin(); i != subs.end(); ++i) {
 +              if (!i->image) {
 +                      continue;
                }
  
 -              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames));
 -              trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0);
 -
 -              audio = trimmed;
 -              time = 0;
 +              /* We will scale the subtitle up to fit _video_container_size */
 +              dcp::Size scaled_size (i->rectangle.width * _video_container_size.width, i->rectangle.height * _video_container_size.height);
 +              
 +              /* Then we need a corrective translation, consisting of two parts:
 +               *
 +               * 1.  that which is the result of the scaling of the subtitle by _video_container_size; this will be
 +               *     rect.x * _video_container_size.width and rect.y * _video_container_size.height.
 +               *
 +               * 2.  that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
-                *     (width_before_subtitle_scale * (1 - subtitle_scale) / 2) and
-                *     (height_before_subtitle_scale * (1 - subtitle_scale) / 2).
++               *     (width_before_subtitle_scale * (1 - subtitle_x_scale) / 2) and
++               *     (height_before_subtitle_scale * (1 - subtitle_y_scale) / 2).
 +               *
 +               * Combining these two translations gives these expressions.
 +               */
 +
 +              all.push_back (
 +                      PositionImage (
 +                              i->image->scale (
 +                                      scaled_size,
 +                                      Scaler::from_id ("bicubic"),
 +                                      i->image->pixel_format (),
 +                                      true
 +                                      ),
 +                              Position<int> (
 +                                      rint (_video_container_size.width * i->rectangle.x),
 +                                      rint (_video_container_size.height * i->rectangle.y)
 +                                      )
 +                              )
 +                      );
        }
  
 -      _audio_merger.push (audio, time);
 -      piece->audio_position += _film->audio_frames_to_time (audio->frames ());
 +      return all;
  }
  
  void
 -Player::flush ()
 +Player::set_approximate_size ()
  {
 -      TimedAudioBuffers<Time> tb = _audio_merger.flush ();
 -      if (_audio && tb.audio) {
 -              Audio (tb.audio, tb.time);
 -              _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 -      }
 -
 -      while (_video && _video_position < _audio_position) {
 -              emit_black ();
 -      }
 +      _approximate_size = true;
 +}
  
 -      while (_audio && _audio_position < _video_position) {
 -              emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
 -      }
 -      
 +shared_ptr<PlayerVideo>
 +Player::black_player_video_frame (DCPTime time) const
 +{
 +      return shared_ptr<PlayerVideo> (
 +              new PlayerVideo (
 +                      shared_ptr<const ImageProxy> (new RawImageProxy (_black_image, _film->log ())),
 +                      time,
 +                      Crop (),
 +                      _video_container_size,
 +                      _video_container_size,
 +                      Scaler::from_id ("bicubic"),
 +                      EYES_BOTH,
 +                      PART_WHOLE,
 +                      Config::instance()->colour_conversions().front().conversion
 +              )
 +      );
  }
  
 -/** Seek so that the next pass() will yield (approximately) the requested frame.
 - *  Pass accurate = true to try harder to get close to the request.
 - *  @return true on error
 - */
 -void
 -Player::seek (Time t, bool accurate)
 +/** @return All PlayerVideos at the given time (there may be two frames for 3D) */
 +list<shared_ptr<PlayerVideo> >
 +Player::get_video (DCPTime time, bool accurate)
  {
        if (!_have_valid_pieces) {
                setup_pieces ();
        }
 +      
 +      list<shared_ptr<Piece> > ov = overlaps<VideoContent> (
 +              time,
 +              time + DCPTime::from_frames (1, _film->video_frame_rate ())
 +              );
  
 -      if (_pieces.empty ()) {
 -              return;
 -      }
 +      list<shared_ptr<PlayerVideo> > pvf;
  
 -      for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> ((*i)->content);
 -              if (!vc) {
 -                      continue;
 +      if (ov.empty ()) {
 +              /* No video content at this time */
 +              pvf.push_back (black_player_video_frame (time));
 +      } else {
 +              /* Create a PlayerVideo from the content's video at this time */
 +
 +              shared_ptr<Piece> piece = ov.back ();
 +              shared_ptr<VideoDecoder> decoder = dynamic_pointer_cast<VideoDecoder> (piece->decoder);
 +              assert (decoder);
 +              shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
 +              assert (content);
 +
 +              list<ContentVideo> content_video = decoder->get_video (dcp_to_content_video (piece, time), accurate);
 +              if (content_video.empty ()) {
 +                      pvf.push_back (black_player_video_frame (time));
 +                      return pvf;
                }
 +              
 +              dcp::Size image_size = content->scale().size (content, _video_container_size, _film->frame_size (), _approximate_size ? 4 : 1);
 +              if (_approximate_size) {
 +                      image_size.width &= ~3;
 +                      image_size.height &= ~3;
 +              }
 +              
 +              for (list<ContentVideo>::const_iterator i = content_video.begin(); i != content_video.end(); ++i) {
 +                      pvf.push_back (
 +                              shared_ptr<PlayerVideo> (
 +                                      new PlayerVideo (
 +                                              i->image,
 +                                              content_video_to_dcp (piece, i->frame),
 +                                              content->crop (),
 +                                              image_size,
 +                                              _video_container_size,
 +                                              _film->scaler(),
 +                                              i->eyes,
 +                                              i->part,
 +                                              content->colour_conversion ()
 +                                              )
 +                                      )
 +                              );
 +              }
 +      }
  
 -              /* s is the offset of t from the start position of this content */
 -              Time s = t - vc->position ();
 -              s = max (static_cast<Time> (0), s);
 -              s = min (vc->length_after_trim(), s);
 -
 -              /* Hence set the piece positions to the `global' time */
 -              (*i)->video_position = (*i)->audio_position = vc->position() + s;
 +      /* Add subtitles (for possible burn-in) to whatever PlayerVideos we got */
  
 -              /* And seek the decoder */
 -              dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
 -                      vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
 -                      );
 +      PlayerSubtitles ps = get_subtitles (time, DCPTime::from_frames (1, _film->video_frame_rate ()), false);
  
 -              (*i)->reset_repeat ();
 -      }
 +      list<PositionImage> sub_images;
  
 -      _video_position = _audio_position = t;
 +      /* Image subtitles */
 +      list<PositionImage> c = transform_image_subtitles (ps.image);
 +      copy (c.begin(), c.end(), back_inserter (sub_images));
  
 -      /* XXX: don't seek audio because we don't need to... */
 +      /* Text subtitles (rendered to images) */
 +      sub_images.push_back (render_subtitles (ps.text, _video_container_size));
 +      
 +      if (!sub_images.empty ()) {
 +              for (list<shared_ptr<PlayerVideo> >::const_iterator i = pvf.begin(); i != pvf.end(); ++i) {
 +                      (*i)->set_subtitle (merge (sub_images));
 +              }
 +      }       
 +              
 +      return pvf;
  }
  
 -void
 -Player::setup_pieces ()
 +shared_ptr<AudioBuffers>
 +Player::get_audio (DCPTime time, DCPTime length, bool accurate)
  {
 -      list<shared_ptr<Piece> > old_pieces = _pieces;
 +      if (!_have_valid_pieces) {
 +              setup_pieces ();
 +      }
  
 -      _pieces.clear ();
 +      AudioFrame const length_frames = length.frames (_film->audio_frame_rate ());
  
 -      ContentList content = _playlist->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
 +      shared_ptr<AudioBuffers> audio (new AudioBuffers (_film->audio_channels(), length_frames));
 +      audio->make_silent ();
 +      
 +      list<shared_ptr<Piece> > ov = overlaps<AudioContent> (time, time + length);
 +      if (ov.empty ()) {
 +              return audio;
 +      }
  
 -      for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
 +      for (list<shared_ptr<Piece> >::iterator i = ov.begin(); i != ov.end(); ++i) {
  
 -              if (!(*i)->paths_valid ()) {
 +              shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> ((*i)->content);
 +              assert (content);
 +              shared_ptr<AudioDecoder> decoder = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 +              assert (decoder);
 +
 +              if (content->audio_frame_rate() == 0) {
 +                      /* This AudioContent has no audio (e.g. if it is an FFmpegContent with no
 +                       * audio stream).
 +                       */
                        continue;
                }
  
 -              shared_ptr<Piece> piece (new Piece (*i));
 +              /* The time that we should request from the content */
 +              DCPTime request = time - DCPTime::from_seconds (content->audio_delay() / 1000.0);
 +              DCPTime offset;
 +              if (request < DCPTime ()) {
 +                      /* We went off the start of the content, so we will need to offset
 +                         the stuff we get back.
 +                      */
 +                      offset = -request;
 +                      request = DCPTime ();
 +              }
  
 -              /* XXX: into content? */
 +              AudioFrame const content_frame = dcp_to_content_audio (*i, request);
  
 -              shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
 -              if (fc) {
 -                      shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (_film, fc, _video, _audio));
 -                      
 -                      fd->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, _5, 0));
 -                      fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2, false));
 -                      fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
 +              /* Audio from this piece's decoder (which might be more or less than what we asked for) */
 +              shared_ptr<ContentAudio> all = decoder->get_audio (content_frame, length_frames, accurate);
  
 -                      fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true);
 -                      piece->decoder = fd;
 +              /* Gain */
 +              if (content->audio_gain() != 0) {
 +                      shared_ptr<AudioBuffers> gain (new AudioBuffers (all->audio));
 +                      gain->apply_gain (content->audio_gain ());
 +                      all->audio = gain;
                }
 -              
 -              shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
 -              if (ic) {
 -                      bool reusing = false;
 -                      
 -                      /* See if we can re-use an old ImageDecoder */
 -                      for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
 -                              shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
 -                              if (imd && imd->content() == ic) {
 -                                      piece = *j;
 -                                      reusing = true;
 -                              }
 -                      }
  
 -                      if (!reusing) {
 -                              shared_ptr<ImageDecoder> id (new ImageDecoder (_film, ic));
 -                              id->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, _5, 0));
 -                              piece->decoder = id;
 +              /* Remap channels */
 +              shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), all->audio->frames()));
 +              dcp_mapped->make_silent ();
 +              AudioMapping map = content->audio_mapping ();
 +              for (int i = 0; i < map.content_channels(); ++i) {
 +                      for (int j = 0; j < _film->audio_channels(); ++j) {
 +                              if (map.get (i, static_cast<dcp::Channel> (j)) > 0) {
 +                                      dcp_mapped->accumulate_channel (
 +                                              all->audio.get(),
 +                                              i,
 +                                              j,
 +                                              map.get (i, static_cast<dcp::Channel> (j))
 +                                              );
 +                              }
                        }
                }
 +              
 +              all->audio = dcp_mapped;
  
 -              shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
 -              if (sc) {
 -                      shared_ptr<AudioDecoder> sd (new SndfileDecoder (_film, sc));
 -                      sd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2, false));
 -
 -                      piece->decoder = sd;
 -              }
 -
 -              _pieces.push_back (piece);
 +              audio->accumulate_frames (
 +                      all->audio.get(),
 +                      content_frame - all->frame,
 +                      offset.frames (_film->audio_frame_rate()),
 +                      min (AudioFrame (all->audio->frames()), length_frames) - offset.frames (_film->audio_frame_rate ())
 +                      );
        }
  
 -      _have_valid_pieces = true;
 +      return audio;
  }
  
 -void
 -Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
 +VideoFrame
 +Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -      shared_ptr<Content> c = w.lock ();
 -      if (!c) {
 -              return;
 -      }
 -
 -      if (
 -              property == ContentProperty::POSITION || property == ContentProperty::LENGTH ||
 -              property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END ||
 -              property == VideoContentProperty::VIDEO_FRAME_TYPE 
 -              ) {
 -              
 -              _have_valid_pieces = false;
 -              Changed (frequent);
 -
 -      } else if (
 -              property == SubtitleContentProperty::SUBTITLE_X_OFFSET ||
 -              property == SubtitleContentProperty::SUBTITLE_Y_OFFSET ||
 -              property == SubtitleContentProperty::SUBTITLE_X_SCALE ||
 -              property == SubtitleContentProperty::SUBTITLE_Y_SCALE
 -              ) {
 -
 -              for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      i->update (_film, _video_container_size);
 -              }
 -              
 -              Changed (frequent);
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (DCPTime::Type (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      } else if (
 -              property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_SCALE ||
 -              property == VideoContentProperty::VIDEO_FRAME_RATE
 -              ) {
 -              
 -              Changed (frequent);
 -
 -      } else if (property == ContentProperty::PATH) {
 -
 -              _have_valid_pieces = false;
 -              Changed (frequent);
 -      }
 +      /* Convert this to the content frame */
 +      return DCPTime (s + piece->content->trim_start()).frames (_film->video_frame_rate()) * piece->frc.factor ();
  }
  
 -void
 -Player::playlist_changed ()
 +DCPTime
 +Player::content_video_to_dcp (shared_ptr<const Piece> piece, VideoFrame f) const
  {
 -      _have_valid_pieces = false;
 -      Changed (false);
 +      DCPTime t = DCPTime::from_frames (f / piece->frc.factor (), _film->video_frame_rate()) - piece->content->trim_start () + piece->content->position ();
 +      if (t < DCPTime ()) {
 +              t = DCPTime ();
 +      }
 +
 +      return t;
  }
  
 -void
 -Player::set_video_container_size (libdcp::Size s)
 +AudioFrame
 +Player::dcp_to_content_audio (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -      _video_container_size = s;
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (DCPTime::Type (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      shared_ptr<Image> im (new Image (PIX_FMT_RGB24, _video_container_size, true));
 -      im->make_black ();
 -      
 -      _black_frame.reset (
 -              new PlayerVideoFrame (
 -                      shared_ptr<ImageProxy> (new RawImageProxy (im, _film->log ())),
 -                      Crop(),
 -                      _video_container_size,
 -                      _video_container_size,
 -                      Scaler::from_id ("bicubic"),
 -                      EYES_BOTH,
 -                      PART_WHOLE,
 -                      ColourConversion ()
 -                      )
 -              );
 +      /* Convert this to the content frame */
 +      return DCPTime (s + piece->content->trim_start()).frames (_film->audio_frame_rate());
  }
  
 -shared_ptr<Resampler>
 -Player::resampler (shared_ptr<AudioContent> c, bool create)
 +ContentTime
 +Player::dcp_to_content_subtitle (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -      map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c);
 -      if (i != _resamplers.end ()) {
 -              return i->second;
 -      }
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (DCPTime::Type (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      if (!create) {
 -              return shared_ptr<Resampler> ();
 -      }
 -
 -      LOG_GENERAL (
 -              "Creating new resampler for %1 to %2 with %3 channels", c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()
 -              );
 -
 -      shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()));
 -      _resamplers[c] = r;
 -      return r;
 +      return ContentTime (s + piece->content->trim_start(), piece->frc);
  }
  
  void
 -Player::emit_black ()
 +PlayerStatistics::dump (shared_ptr<Log> log) const
  {
 -#ifdef DCPOMATIC_DEBUG
 -      _last_video.reset ();
 -#endif
 -
 -      Video (_black_frame, _last_emit_was_black, _video_position);
 -      _video_position += _film->video_frames_to_time (1);
 -      _last_emit_was_black = true;
 +      log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat), Log::TYPE_GENERAL);
 +      log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence.seconds()), Log::TYPE_GENERAL);
  }
  
 -void
 -Player::emit_silence (OutputAudioFrame most)
 +PlayerStatistics const &
 +Player::statistics () const
  {
 -      if (most == 0) {
 -              return;
 -      }
 -      
 -      OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
 -      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
 -      silence->make_silent ();
 -      Audio (silence, _audio_position);
 -      _audio_position += _film->audio_frames_to_time (N);
 +      return _statistics;
  }
  
 -void
 -Player::film_changed (Film::Property p)
 +PlayerSubtitles
 +Player::get_subtitles (DCPTime time, DCPTime length, bool starting)
  {
 -      /* Here we should notice Film properties that affect our output, and
 -         alert listeners that our output now would be different to how it was
 -         last time we were run.
 -      */
 +      list<shared_ptr<Piece> > subs = overlaps<SubtitleContent> (time, time + length);
  
 -      if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
 -              Changed (false);
 -      }
 -}
 +      PlayerSubtitles ps (time, length);
  
 -void
 -Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
 -{
 -      if (!image) {
 -              /* A null image means that we should stop any current subtitles at `from' */
 -              for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      i->set_stop (from);
 +      for (list<shared_ptr<Piece> >::const_iterator j = subs.begin(); j != subs.end(); ++j) {
 +              shared_ptr<SubtitleContent> subtitle_content = dynamic_pointer_cast<SubtitleContent> ((*j)->content);
 +              if (!subtitle_content->use_subtitles ()) {
 +                      continue;
                }
 -      } else {
 -              _subtitles.push_back (Subtitle (_film, _video_container_size, weak_piece, image, rect, from, to));
 -      }
 -}
  
 -/** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
 - *  @return false if this could not be done.
 - */
 -bool
 -Player::repeat_last_video ()
 -{
 -      if (!_last_incoming_video.image || !_have_valid_pieces) {
 -              return false;
 -      }
 +              shared_ptr<SubtitleDecoder> subtitle_decoder = dynamic_pointer_cast<SubtitleDecoder> ((*j)->decoder);
 +              ContentTime const from = dcp_to_content_subtitle (*j, time);
 +              /* XXX: this video_frame_rate() should be the rate that the subtitle content has been prepared for */
 +              ContentTime const to = from + ContentTime::from_frames (1, _film->video_frame_rate ());
  
 -      process_video (
 -              _last_incoming_video.weak_piece,
 -              _last_incoming_video.image,
 -              _last_incoming_video.eyes,
 -              _last_incoming_video.part,
 -              _last_incoming_video.same,
 -              _last_incoming_video.frame,
 -              _last_incoming_video.extra
 -              );
 +              list<ContentImageSubtitle> image = subtitle_decoder->get_image_subtitles (ContentTimePeriod (from, to), starting);
 +              for (list<ContentImageSubtitle>::iterator i = image.begin(); i != image.end(); ++i) {
 +                      
 +                      /* Apply content's subtitle offsets */
 +                      i->sub.rectangle.x += subtitle_content->subtitle_x_offset ();
 +                      i->sub.rectangle.y += subtitle_content->subtitle_y_offset ();
 +
 +                      /* Apply content's subtitle scale */
-                       i->sub.rectangle.width *= subtitle_content->subtitle_scale ();
-                       i->sub.rectangle.height *= subtitle_content->subtitle_scale ();
++                      i->sub.rectangle.width *= subtitle_content->subtitle_x_scale ();
++                      i->sub.rectangle.height *= subtitle_content->subtitle_y_scale ();
 +
 +                      /* Apply a corrective translation to keep the subtitle centred after that scale */
-                       i->sub.rectangle.x -= i->sub.rectangle.width * (subtitle_content->subtitle_scale() - 1);
-                       i->sub.rectangle.y -= i->sub.rectangle.height * (subtitle_content->subtitle_scale() - 1);
++                      i->sub.rectangle.x -= i->sub.rectangle.width * (subtitle_content->subtitle_x_scale() - 1);
++                      i->sub.rectangle.y -= i->sub.rectangle.height * (subtitle_content->subtitle_y_scale() - 1);
 +                      
 +                      ps.image.push_back (i->sub);
 +              }
 +
 +              list<ContentTextSubtitle> text = subtitle_decoder->get_text_subtitles (ContentTimePeriod (from, to), starting);
 +              for (list<ContentTextSubtitle>::const_iterator i = text.begin(); i != text.end(); ++i) {
 +                      copy (i->subs.begin(), i->subs.end(), back_inserter (ps.text));
 +              }
 +      }
  
 -      return true;
 +      return ps;
  }
index 0e56619455b09613a7ada5229d0b932a2fe72e11,3702eef4152c484ee3b044804ebdc86aabdaae91..5b370847ba08d2f2f156301f5864982f7820efae
  */
  
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "subtitle_content.h"
  #include "util.h"
  #include "exceptions.h"
 +#include "safe_stringstream.h"
  
  #include "i18n.h"
  
  using std::string;
  using std::vector;
 +using std::cout;
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  int const SubtitleContentProperty::SUBTITLE_X_OFFSET = 500;
  int const SubtitleContentProperty::SUBTITLE_Y_OFFSET = 501;
- int const SubtitleContentProperty::SUBTITLE_SCALE = 502;
- int const SubtitleContentProperty::USE_SUBTITLES = 503;
+ int const SubtitleContentProperty::SUBTITLE_X_SCALE = 502;
+ int const SubtitleContentProperty::SUBTITLE_Y_SCALE = 503;
++int const SubtitleContentProperty::USE_SUBTITLES = 504;
 +
 +SubtitleContent::SubtitleContent (shared_ptr<const Film> f)
 +      : Content (f)
 +      , _use_subtitles (false)
 +      , _subtitle_x_offset (0)
 +      , _subtitle_y_offset (0)
-       , _subtitle_scale (1)
++      , _subtitle_x_scale (1)
++      , _subtitle_y_scale (1)
 +{
 +
 +}
  
  SubtitleContent::SubtitleContent (shared_ptr<const Film> f, boost::filesystem::path p)
        : Content (f, p)
 +      , _use_subtitles (false)
        , _subtitle_x_offset (0)
        , _subtitle_y_offset (0)
-       , _subtitle_scale (1)
+       , _subtitle_x_scale (1)
+       , _subtitle_y_scale (1)
  {
  
  }
  
 -SubtitleContent::SubtitleContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
 +SubtitleContent::SubtitleContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version)
        : Content (f, node)
 +      , _use_subtitles (false)
        , _subtitle_x_offset (0)
        , _subtitle_y_offset (0)
-       , _subtitle_scale (1)
+       , _subtitle_x_scale (1)
+       , _subtitle_y_scale (1)
  {
 +      if (version >= 32) {
 +              _use_subtitles = node->bool_child ("UseSubtitles");
 +      } else {
 +              _use_subtitles = false;
 +      }
 +      
        if (version >= 7) {
                _subtitle_x_offset = node->number_child<float> ("SubtitleXOffset");
                _subtitle_y_offset = node->number_child<float> ("SubtitleYOffset");
        } else {
                _subtitle_y_offset = node->number_child<float> ("SubtitleOffset");
        }
-       
-       _subtitle_scale = node->number_child<float> ("SubtitleScale");
+       if (version >= 10) {
+               _subtitle_x_scale = node->number_child<float> ("SubtitleXScale");
+               _subtitle_y_scale = node->number_child<float> ("SubtitleYScale");
+       } else {
+               _subtitle_x_scale = _subtitle_y_scale = node->number_child<float> ("SubtitleScale");
+       }
  }
  
  SubtitleContent::SubtitleContent (shared_ptr<const Film> f, vector<shared_ptr<Content> > c)
        for (size_t i = 0; i < c.size(); ++i) {
                shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (c[i]);
  
 +              if (sc->use_subtitles() != ref->use_subtitles()) {
 +                      throw JoinError (_("Content to be joined must have the same 'use subtitles' setting."));
 +              }
 +
                if (sc->subtitle_x_offset() != ref->subtitle_x_offset()) {
                        throw JoinError (_("Content to be joined must have the same subtitle X offset."));
                }
                        throw JoinError (_("Content to be joined must have the same subtitle Y offset."));
                }
  
-               if (sc->subtitle_scale() != ref->subtitle_scale()) {
-                       throw JoinError (_("Content to be joined must have the same subtitle scale."));
+               if (sc->subtitle_x_scale() != ref->subtitle_x_scale()) {
+                       throw JoinError (_("Content to be joined must have the same subtitle X scale."));
+               }
+               if (sc->subtitle_y_scale() != ref->subtitle_y_scale()) {
+                       throw JoinError (_("Content to be joined must have the same subtitle Y scale."));
                }
        }
  
 +      _use_subtitles = ref->use_subtitles ();
        _subtitle_x_offset = ref->subtitle_x_offset ();
        _subtitle_y_offset = ref->subtitle_y_offset ();
-       _subtitle_scale = ref->subtitle_scale ();
+       _subtitle_x_scale = ref->subtitle_x_scale ();
+       _subtitle_y_scale = ref->subtitle_y_scale ();
  }
  
  void
  SubtitleContent::as_xml (xmlpp::Node* root) const
  {
 +      root->add_child("UseSubtitles")->add_child_text (raw_convert<string> (_use_subtitles));
        root->add_child("SubtitleXOffset")->add_child_text (raw_convert<string> (_subtitle_x_offset));
        root->add_child("SubtitleYOffset")->add_child_text (raw_convert<string> (_subtitle_y_offset));
-       root->add_child("SubtitleScale")->add_child_text (raw_convert<string> (_subtitle_scale));
+       root->add_child("SubtitleXScale")->add_child_text (raw_convert<string> (_subtitle_x_scale));
+       root->add_child("SubtitleYScale")->add_child_text (raw_convert<string> (_subtitle_y_scale));
  }
  
 +void
 +SubtitleContent::set_use_subtitles (bool u)
 +{
 +      {
 +              boost::mutex::scoped_lock lm (_mutex);
 +              _use_subtitles = u;
 +      }
 +      signal_changed (SubtitleContentProperty::USE_SUBTITLES);
 +}
 +      
  void
  SubtitleContent::set_subtitle_x_offset (double o)
  {
@@@ -153,23 -130,21 +168,34 @@@ SubtitleContent::set_subtitle_y_offset 
  }
  
  void
- SubtitleContent::set_subtitle_scale (double s)
+ SubtitleContent::set_subtitle_x_scale (double s)
+ {
+       {
+               boost::mutex::scoped_lock lm (_mutex);
+               _subtitle_x_scale = s;
+       }
+       signal_changed (SubtitleContentProperty::SUBTITLE_X_SCALE);
+ }
+ void
+ SubtitleContent::set_subtitle_y_scale (double s)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
-               _subtitle_scale = s;
+               _subtitle_y_scale = s;
        }
-       signal_changed (SubtitleContentProperty::SUBTITLE_SCALE);
+       signal_changed (SubtitleContentProperty::SUBTITLE_Y_SCALE);
  }
-         << "_" << raw_convert<string> (subtitle_scale())
 +
 +string
 +SubtitleContent::identifier () const
 +{
 +      SafeStringStream s;
 +      s << Content::identifier()
++        << "_" << raw_convert<string> (subtitle_x_scale())
++        << "_" << raw_convert<string> (subtitle_y_scale())
 +        << "_" << raw_convert<string> (subtitle_x_offset())
 +        << "_" << raw_convert<string> (subtitle_y_offset());
 +
 +      return s.str ();
 +}
index 97649f4d525590a2ad1fdb333a6b789bad9087e1,329368e4432f92ddd698e4f294d8128474bbbbf2..c3c25232f4893ec59776d42c20f483ec5f72acc1
@@@ -27,39 -27,24 +27,41 @@@ class SubtitleContentPropert
  public:
        static int const SUBTITLE_X_OFFSET;
        static int const SUBTITLE_Y_OFFSET;
-       static int const SUBTITLE_SCALE;
+       static int const SUBTITLE_X_SCALE;
+       static int const SUBTITLE_Y_SCALE;
 +      static int const USE_SUBTITLES;
  };
  
 +/** @class SubtitleContent
 + *  @brief Parent for content which has the potential to include subtitles.
 + *
 + *  Although inheriting from this class indicates that the content could
 + *  have subtitles, it may not.  ::has_subtitles() will tell you.
 + */
  class SubtitleContent : public virtual Content
  {
  public:
 +      SubtitleContent (boost::shared_ptr<const Film>);
        SubtitleContent (boost::shared_ptr<const Film>, boost::filesystem::path);
 -      SubtitleContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int version);
 +      SubtitleContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int version);
        SubtitleContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
 -      
 +
        void as_xml (xmlpp::Node *) const;
 +      std::string identifier () const;
 +
 +      virtual bool has_subtitles () const = 0;
  
 +      void set_use_subtitles (bool);
        void set_subtitle_x_offset (double);
        void set_subtitle_y_offset (double);
-       void set_subtitle_scale (double);
+       void set_subtitle_x_scale (double);
+       void set_subtitle_y_scale (double);
  
 +      bool use_subtitles () const {
 +              boost::mutex::scoped_lock lm (_mutex);
 +              return _use_subtitles;
 +      }
 +
        double subtitle_x_offset () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _subtitle_x_offset;
                return _subtitle_y_offset;
        }
  
-       double subtitle_scale () const {
+       double subtitle_x_scale () const {
                boost::mutex::scoped_lock lm (_mutex);
-               return _subtitle_scale;
+               return _subtitle_x_scale;
+       }
+       double subtitle_y_scale () const {
+               boost::mutex::scoped_lock lm (_mutex);
+               return _subtitle_y_scale;
        }
 -      
 +
  private:
 -      friend class ffmpeg_pts_offset_test;
 +      friend struct ffmpeg_pts_offset_test;
  
 +      bool _use_subtitles;
        /** x offset for placing subtitles, as a proportion of the container width;
         * +ve is further right, -ve is further left.
         */
         *  +ve is further down the frame, -ve is further up.
         */
        double _subtitle_y_offset;
-       /** scale factor to apply to subtitles */
-       double _subtitle_scale;
+       /** x scale factor to apply to subtitles */
+       double _subtitle_x_scale;
+       /** y scale factor to apply to subtitles */
+       double _subtitle_y_scale;
  };
  
  #endif
diff --combined src/wx/subtitle_panel.cc
index fecc85106f6f889d5a1bd725ac10978022bae7ac,7953682fca6fd25f857062b31fee4885a241fdfd..21d6f8e5ba6c46a48a57004fa625e3a584af4641
  #include <boost/lexical_cast.hpp>
  #include <wx/spinctrl.h>
  #include "lib/ffmpeg_content.h"
 +#include "lib/subrip_content.h"
 +#include "lib/ffmpeg_subtitle_stream.h"
 +#include "lib/dcp_subtitle_content.h"
 +#include "lib/subrip_decoder.h"
 +#include "lib/dcp_subtitle_decoder.h"
  #include "subtitle_panel.h"
  #include "film_editor.h"
  #include "wx_util.h"
 +#include "subtitle_view.h"
 +#include "content_panel.h"
  
  using std::vector;
  using std::string;
@@@ -37,17 -30,16 +37,17 @@@ using boost::shared_ptr
  using boost::lexical_cast;
  using boost::dynamic_pointer_cast;
  
 -SubtitlePanel::SubtitlePanel (FilmEditor* e)
 -      : FilmEditorPanel (e, _("Subtitles"))
 +SubtitlePanel::SubtitlePanel (ContentPanel* p)
 +      : ContentSubPanel (p, _("Subtitles"))
 +      , _view (0)
  {
        wxFlexGridSizer* grid = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
        _sizer->Add (grid, 0, wxALL, 8);
  
 -      _with_subtitles = new wxCheckBox (this, wxID_ANY, _("With Subtitles"));
 -      grid->Add (_with_subtitles, 1);
 +      _use = new wxCheckBox (this, wxID_ANY, _("Use subtitles"));
 +      grid->Add (_use);
        grid->AddSpacer (0);
 -      
 +
        {
                add_label_to_sizer (grid, this, _("X Offset"), true);
                wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
        }
        
        {
-               add_label_to_sizer (grid, this, _("Scale"), true);
+               add_label_to_sizer (grid, this, _("Scale"), true);
                wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
-               _scale = new wxSpinCtrl (this);
-               s->Add (_scale);
+               _x_scale = new wxSpinCtrl (this);
+               s->Add (_x_scale);
                add_label_to_sizer (s, this, _("%"), false);
                grid->Add (s);
        }
  
+       {
+               add_label_to_sizer (grid, this, _("Y Scale"), true);
+               wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+               _y_scale = new wxSpinCtrl (this);
+               s->Add (_y_scale);
+               add_label_to_sizer (s, this, _("%"), false);
+               grid->Add (s);
+       }
+       
        add_label_to_sizer (grid, this, _("Stream"), true);
        _stream = new wxChoice (this, wxID_ANY);
        grid->Add (_stream, 1, wxEXPAND);
 +
 +      _view_button = new wxButton (this, wxID_ANY, _("View..."));
 +      grid->Add (_view_button);
        
        _x_offset->SetRange (-100, 100);
        _y_offset->SetRange (-100, 100);
-       _scale->SetRange (1, 1000);
-       _scale->SetValue (100);
+       _x_scale->SetRange (10, 1000);
+       _y_scale->SetRange (10, 1000);
  
 -      _with_subtitles->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&SubtitlePanel::with_subtitles_toggled, this));
 -      _x_offset->Bind       (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_offset_changed, this));
 -      _y_offset->Bind       (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_offset_changed, this));
 -      _x_scale->Bind        (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_scale_changed, this));
 -      _y_scale->Bind        (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_scale_changed, this));
 -      _stream->Bind         (wxEVT_COMMAND_CHOICE_SELECTED,  boost::bind (&SubtitlePanel::stream_changed, this));
 +      _use->Bind         (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&SubtitlePanel::use_toggled, this));
 +      _x_offset->Bind    (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_offset_changed, this));
 +      _y_offset->Bind    (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_offset_changed, this));
-       _scale->Bind       (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::scale_changed, this));
++      _x_scale->Bind       (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_scale_changed, this));
++      _y_scale->Bind       (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_scale_changed, this));
 +      _stream->Bind      (wxEVT_COMMAND_CHOICE_SELECTED,  boost::bind (&SubtitlePanel::stream_changed, this));
 +      _view_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED,   boost::bind (&SubtitlePanel::view_clicked, this));
  }
  
  void
  SubtitlePanel::film_changed (Film::Property property)
  {
 -      switch (property) {
 -      case Film::CONTENT:
 -              setup_sensitivity ();
 -              break;
 -      case Film::WITH_SUBTITLES:
 -              checked_set (_with_subtitles, _editor->film()->with_subtitles ());
 +      if (property == Film::CONTENT) {
                setup_sensitivity ();
 -              break;
 -      default:
 -              break;
        }
  }
  
  void
  SubtitlePanel::film_content_changed (int property)
  {
 -      FFmpegContentList fc = _editor->selected_ffmpeg_content ();
 -      SubtitleContentList sc = _editor->selected_subtitle_content ();
 +      FFmpegContentList fc = _parent->selected_ffmpeg ();
 +      SubtitleContentList sc = _parent->selected_subtitle ();
  
        shared_ptr<FFmpegContent> fcs;
        if (fc.size() == 1) {
        if (sc.size() == 1) {
                scs = sc.front ();
        }
 -      
 +
        if (property == FFmpegContentProperty::SUBTITLE_STREAMS) {
                _stream->Clear ();
                if (fcs) {
                        }
                }
                setup_sensitivity ();
 +      } else if (property == SubtitleContentProperty::USE_SUBTITLES) {
 +              checked_set (_use, scs ? scs->use_subtitles() : false);
 +              setup_sensitivity ();
        } else if (property == SubtitleContentProperty::SUBTITLE_X_OFFSET) {
                checked_set (_x_offset, scs ? (scs->subtitle_x_offset() * 100) : 0);
        } else if (property == SubtitleContentProperty::SUBTITLE_Y_OFFSET) {
                checked_set (_y_offset, scs ? (scs->subtitle_y_offset() * 100) : 0);
-       } else if (property == SubtitleContentProperty::SUBTITLE_SCALE) {
-               checked_set (_scale, scs ? (scs->subtitle_scale() * 100) : 100);
+       } else if (property == SubtitleContentProperty::SUBTITLE_X_SCALE) {
+               checked_set (_x_scale, scs ? int (rint (scs->subtitle_x_scale() * 100)) : 100);
+       } else if (property == SubtitleContentProperty::SUBTITLE_Y_SCALE) {
+               checked_set (_y_scale, scs ? int (rint (scs->subtitle_y_scale() * 100)) : 100);
        }
  }
  
  void
 -SubtitlePanel::with_subtitles_toggled ()
 +SubtitlePanel::use_toggled ()
  {
 -      if (!_editor->film()) {
 -              return;
 +      SubtitleContentList c = _parent->selected_subtitle ();
 +      for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) {
 +              (*i)->set_use_subtitles (_use->GetValue());
        }
 -
 -      _editor->film()->set_with_subtitles (_with_subtitles->GetValue ());
  }
  
  void
  SubtitlePanel::setup_sensitivity ()
  {
 -      bool h = false;
 -      bool j = false;
 -      if (_editor->film()) {
 -              h = _editor->film()->has_subtitles ();
 -              j = _editor->film()->with_subtitles ();
 +      int any_subs = 0;
 +      int ffmpeg_subs = 0;
 +      int subrip_or_dcp_subs = 0;
 +      SubtitleContentList c = _parent->selected_subtitle ();
 +      for (SubtitleContentList::const_iterator i = c.begin(); i != c.end(); ++i) {
 +              shared_ptr<const FFmpegContent> fc = boost::dynamic_pointer_cast<const FFmpegContent> (*i);
 +              shared_ptr<const SubRipContent> sc = boost::dynamic_pointer_cast<const SubRipContent> (*i);
 +              shared_ptr<const DCPSubtitleContent> dsc = boost::dynamic_pointer_cast<const DCPSubtitleContent> (*i);
 +              if (fc) {
 +                      if (fc->has_subtitles ()) {
 +                              ++ffmpeg_subs;
 +                              ++any_subs;
 +                      }
 +              } else if (sc || dsc) {
 +                      ++subrip_or_dcp_subs;
 +                      ++any_subs;
 +              } else {
 +                      ++any_subs;
 +              }
        }
 +              
 +      _use->Enable (any_subs > 0);
 +      bool const use = _use->GetValue ();
        
 -      _with_subtitles->Enable (h);
 -      _x_offset->Enable (j);
 -      _y_offset->Enable (j);
 -      _x_scale->Enable (j);
 -      _y_scale->Enable (j);
 -      _stream->Enable (j);
 +      _x_offset->Enable (any_subs > 0 && use);
 +      _y_offset->Enable (any_subs > 0 && use);
-       _scale->Enable (any_subs > 0 && use);
++      _x_scale->Enable (any_subs > 0 && use);
++      _y_scale->Enable (any_subs > 0 && use);
 +      _stream->Enable (ffmpeg_subs == 1);
 +      _view_button->Enable (subrip_or_dcp_subs == 1);
  }
  
  void
  SubtitlePanel::stream_changed ()
  {
 -      FFmpegContentList fc = _editor->selected_ffmpeg_content ();
 +      FFmpegContentList fc = _parent->selected_ffmpeg ();
        if (fc.size() != 1) {
                return;
        }
  void
  SubtitlePanel::x_offset_changed ()
  {
 -      SubtitleContentList c = _editor->selected_subtitle_content ();
 -      if (c.size() == 1) {
 -              c.front()->set_subtitle_x_offset (_x_offset->GetValue() / 100.0);
 +      SubtitleContentList c = _parent->selected_subtitle ();
 +      for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) {
 +              (*i)->set_subtitle_x_offset (_x_offset->GetValue() / 100.0);
        }
  }
  
  void
  SubtitlePanel::y_offset_changed ()
  {
 -      SubtitleContentList c = _editor->selected_subtitle_content ();
 -      if (c.size() == 1) {
 -              c.front()->set_subtitle_y_offset (_y_offset->GetValue() / 100.0);
 +      SubtitleContentList c = _parent->selected_subtitle ();
 +      for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) {
 +              (*i)->set_subtitle_y_offset (_y_offset->GetValue() / 100.0);
        }
  }
  
  void
- SubtitlePanel::scale_changed ()
+ SubtitlePanel::x_scale_changed ()
+ {
 -      SubtitleContentList c = _editor->selected_subtitle_content ();
++      SubtitleContentList c = _parent->selected_subtitle ();
+       if (c.size() == 1) {
+               c.front()->set_subtitle_x_scale (_x_scale->GetValue() / 100.0);
+       }
+ }
+ void
+ SubtitlePanel::y_scale_changed ()
  {
 -      SubtitleContentList c = _editor->selected_subtitle_content ();
 -      if (c.size() == 1) {
 -              c.front()->set_subtitle_y_scale (_y_scale->GetValue() / 100.0);
 +      SubtitleContentList c = _parent->selected_subtitle ();
 +      for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) {
-               (*i)->set_subtitle_scale (_scale->GetValue() / 100.0);
++              (*i)->set_subtitle_y_scale (_y_scale->GetValue() / 100.0);
        }
  }
  
@@@ -242,37 -241,8 +264,38 @@@ voi
  SubtitlePanel::content_selection_changed ()
  {
        film_content_changed (FFmpegContentProperty::SUBTITLE_STREAMS);
 +      film_content_changed (SubtitleContentProperty::USE_SUBTITLES);
        film_content_changed (SubtitleContentProperty::SUBTITLE_X_OFFSET);
        film_content_changed (SubtitleContentProperty::SUBTITLE_Y_OFFSET);
-       film_content_changed (SubtitleContentProperty::SUBTITLE_SCALE);
+       film_content_changed (SubtitleContentProperty::SUBTITLE_X_SCALE);
+       film_content_changed (SubtitleContentProperty::SUBTITLE_Y_SCALE);
  }
 +
 +void
 +SubtitlePanel::view_clicked ()
 +{
 +      if (_view) {
 +              _view->Destroy ();
 +              _view = 0;
 +      }
 +
 +      SubtitleContentList c = _parent->selected_subtitle ();
 +      assert (c.size() == 1);
 +
 +      shared_ptr<SubtitleDecoder> decoder;
 +      
 +      shared_ptr<SubRipContent> sr = dynamic_pointer_cast<SubRipContent> (c.front ());
 +      if (sr) {
 +              decoder.reset (new SubRipDecoder (sr));
 +      }
 +      
 +      shared_ptr<DCPSubtitleContent> dc = dynamic_pointer_cast<DCPSubtitleContent> (c.front ());
 +      if (dc) {
 +              decoder.reset (new DCPSubtitleDecoder (dc));
 +      }
 +      
 +      if (decoder) {
 +              _view = new SubtitleView (this, _parent->film(), decoder, c.front()->position ());
 +              _view->Show ();
 +      }
 +}
diff --combined src/wx/subtitle_panel.h
index 9e60db34b23de055ff7208f797622de16ea70fa2,7f5d9239d1d477fbad194b2ab6a2a34618466633..bcff995a0029a5ef223d5381113648aaef893fc0
@@@ -1,5 -1,5 +1,5 @@@
  /*
 -    Copyright (C) 2012-2013 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 "film_editor_panel.h"
 +#include "content_sub_panel.h"
  
  class wxCheckBox;
  class wxSpinCtrl;
 +class SubtitleView;
  
 -class SubtitlePanel : public FilmEditorPanel
 +class SubtitlePanel : public ContentSubPanel
  {
  public:
 -      SubtitlePanel (FilmEditor *);
 +      SubtitlePanel (ContentPanel *);
  
        void film_changed (Film::Property);
        void film_content_changed (int);
        void content_selection_changed ();
        
  private:
 -      void with_subtitles_toggled ();
 +      void use_toggled ();
        void x_offset_changed ();
        void y_offset_changed ();
-       void scale_changed ();
+       void x_scale_changed ();
+       void y_scale_changed ();
        void stream_changed ();
 +      void view_clicked ();
  
        void setup_sensitivity ();
        
 -      wxCheckBox* _with_subtitles;
 +      wxCheckBox* _use;
        wxSpinCtrl* _x_offset;
        wxSpinCtrl* _y_offset;
-       wxSpinCtrl* _scale;
+       wxSpinCtrl* _x_scale;
+       wxSpinCtrl* _y_scale;
        wxChoice* _stream;
 +      wxButton* _view_button;
 +      SubtitleView* _view;
  };