Merge master.
authorCarl Hetherington <cth@carlh.net>
Wed, 15 Jan 2014 16:36:28 +0000 (16:36 +0000)
committerCarl Hetherington <cth@carlh.net>
Wed, 15 Jan 2014 16:36:28 +0000 (16:36 +0000)
1  2 
src/lib/encoder.cc
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_examiner.cc
src/lib/film.cc
src/lib/player.cc
src/lib/transcode_job.cc

diff --combined src/lib/encoder.cc
index ca9134c04d29e52400579330eb34d4fa75a5196e,fbec3e4d0d0e65c2a4dfa51653e0461d18873777..d26f776148c74fd34c0ff35abd264857b87e9266
@@@ -68,9 -68,6 +68,6 @@@ Encoder::Encoder (shared_ptr<const Film
  Encoder::~Encoder ()
  {
        terminate_threads ();
-       if (_writer) {
-               _writer->finish ();
-       }
  }
  
  /** Add a worker thread for a each thread on a remote server.  Caller must hold
@@@ -218,7 -215,7 +215,7 @@@ Encoder::process_video (shared_ptr<Play
                TIMING ("adding to queue of %1", _queue.size ());
                _queue.push_back (shared_ptr<DCPVideoFrame> (
                                          new DCPVideoFrame (
 -                                                image->image(), _video_frames_out, eyes, conversion, _film->video_frame_rate(),
 +                                                image->image(PIX_FMT_RGB24, false), _video_frames_out, eyes, conversion, _film->video_frame_rate(),
                                                  _film->j2k_bandwidth(), _film->resolution(), _film->log()
                                                  )
                                          ));
index 8742c48ecd6360021bcb309636ba376a300ccc28,a6f9a17c3bc7978e91821e64758f1be52a2361b6..52afe2a271e79360cf8fea54257f3e6b239495c9
@@@ -68,7 -68,9 +68,7 @@@ FFmpegDecoder::FFmpegDecoder (shared_pt
        , _subtitle_codec (0)
        , _decode_video (video)
        , _decode_audio (audio)
 -      , _video_pts_offset (0)
 -      , _audio_pts_offset (0)
 -      , _just_sought (false)
 +      , _pts_offset (0)
  {
        setup_subtitle ();
  
           Then we remove big initial gaps in PTS and we allow our
           insertion of black frames to work.
  
 -         We will do:
 -           audio_pts_to_use = audio_pts_from_ffmpeg + audio_pts_offset;
 -           video_pts_to_use = video_pts_from_ffmpeg + video_pts_offset;
 +         We will do pts_to_use = pts_from_ffmpeg + pts_offset;
        */
  
        bool const have_video = video && c->first_video();
 -      bool const have_audio = audio && c->audio_stream() && c->audio_stream()->first_audio;
 +      bool const have_audio = _decode_audio && c->audio_stream () && c->audio_stream()->first_audio;
  
        /* First, make one of them start at 0 */
  
        if (have_audio && have_video) {
 -              _video_pts_offset = _audio_pts_offset = - min (c->first_video().get(), c->audio_stream()->first_audio.get());
 +              _pts_offset = - min (c->first_video().get(), c->audio_stream()->first_audio.get());
        } else if (have_video) {
 -              _video_pts_offset = - c->first_video().get();
 +              _pts_offset = - c->first_video().get();
        } else if (have_audio) {
 -              _audio_pts_offset = - c->audio_stream()->first_audio.get();
 +              _pts_offset = - c->audio_stream()->first_audio.get();
        }
  
        /* Now adjust both so that the video pts starts on a frame */
        if (have_video && have_audio) {
 -              double first_video = c->first_video().get() + _video_pts_offset;
 +              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 */
                        first_video = ceil (first_video * c->video_frame_rate()) / c->video_frame_rate ();
                }
  
 -              _video_pts_offset += first_video - old_first_video;
 -              _audio_pts_offset += first_video - old_first_video;
 +              _pts_offset += first_video - old_first_video;
        }
  }
  
@@@ -138,10 -143,12 +138,10 @@@ FFmpegDecoder::flush (
                decode_audio_packet ();
        }
  
 -      /* Stop us being asked for any more data */
 -      _video_position = _ffmpeg_content->video_length ();
 -      _audio_position = _ffmpeg_content->audio_length ();
 +      AudioDecoder::flush ();
  }
  
 -void
 +bool
  FFmpegDecoder::pass ()
  {
        int r = av_read_frame (_format_context, &_packet);
                }
  
                flush ();
 -              return;
 +              return true;
        }
  
-       avcodec_get_frame_defaults (_frame);
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
  
        }
  
        av_free_packet (&_packet);
 +      return false;
  }
  
  /** @param data pointer to array of pointers to buffers.
@@@ -291,135 -295,68 +289,135 @@@ FFmpegDecoder::bytes_per_audio_sample (
        return av_get_bytes_per_sample (audio_sample_format ());
  }
  
 -void
 -FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate)
 +int
 +FFmpegDecoder::minimal_run (boost::function<bool (optional<ContentTime>, optional<ContentTime>, int)> finished)
  {
 -      double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base);
 +      int frames_read = 0;
 +      optional<ContentTime> last_video;
 +      optional<ContentTime> last_audio;
  
 -      /* If we are doing an accurate seek, our initial shot will be 5 frames (5 being
 -         a number plucked from the air) earlier than we want to end up.  The loop below
 -         will hopefully then step through to where we want to be.
 -      */
 -      int initial = frame;
 +      while (!finished (last_video, last_audio, frames_read)) {
 +              int r = av_read_frame (_format_context, &_packet);
 +              if (r < 0) {
 +                      /* We should flush our decoders here, possibly yielding a few more frames,
 +                         but the consequence of having to do that is too hideous to contemplate.
 +                         Instead we give up and say that you can't seek too close to the end
 +                         of a file.
 +                      */
 +                      return frames_read;
 +              }
  
 -      if (accurate) {
 -              initial -= 5;
 +              ++frames_read;
 +
 +              double const time_base = av_q2d (_format_context->streams[_packet.stream_index]->time_base);
 +
 +              if (_packet.stream_index == _video_stream) {
 +
 +                      avcodec_get_frame_defaults (_frame);
 +                      
 +                      int finished = 0;
 +                      r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
 +                      if (r >= 0 && finished) {
 +                              last_video = rint (
 +                                      (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * TIME_HZ
 +                                      );
 +                      }
 +
 +              } else if (_ffmpeg_content->audio_stream() && _packet.stream_index == _ffmpeg_content->audio_stream()->index (_format_context)) {
 +                      AVPacket copy_packet = _packet;
 +                      while (copy_packet.size > 0) {
 +
 +                              int finished;
 +                              r = avcodec_decode_audio4 (audio_codec_context(), _frame, &finished, &_packet);
 +                              if (r >= 0 && finished) {
 +                                      last_audio = rint (
 +                                              (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * TIME_HZ
 +                                              );
 +                              }
 +                                      
 +                              copy_packet.data += r;
 +                              copy_packet.size -= r;
 +                      }
 +              }
 +              
 +              av_free_packet (&_packet);
        }
  
 -      if (initial < 0) {
 -              initial = 0;
 +      return frames_read;
 +}
 +
 +bool
 +FFmpegDecoder::seek_overrun_finished (ContentTime seek, optional<ContentTime> last_video, optional<ContentTime> last_audio) const
 +{
 +      return (last_video && last_video.get() >= seek) || (last_audio && last_audio.get() >= seek);
 +}
 +
 +bool
 +FFmpegDecoder::seek_final_finished (int n, int done) const
 +{
 +      return n == done;
 +}
 +
 +void
 +FFmpegDecoder::seek_and_flush (ContentTime t)
 +{
 +      int64_t s = ((double (t) / TIME_HZ) - _pts_offset) /
 +              av_q2d (_format_context->streams[_video_stream]->time_base);
 +
 +      if (_ffmpeg_content->audio_stream ()) {
 +              s = min (
 +                      s, int64_t (
 +                              ((double (t) / TIME_HZ) - _pts_offset) /
 +                              av_q2d (_ffmpeg_content->audio_stream()->stream(_format_context)->time_base)
 +                              )
 +                      );
        }
  
 -      /* Initial seek time in the stream's timebase */
 -      int64_t const initial_vt = ((initial / _ffmpeg_content->video_frame_rate()) - _video_pts_offset) / time_base;
 +      /* Ridiculous empirical hack */
 +      s--;
  
 -      av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD);
 +      av_seek_frame (_format_context, _video_stream, s, AVSEEK_FLAG_BACKWARD);
  
        avcodec_flush_buffers (video_codec_context());
 +      if (audio_codec_context ()) {
 +              avcodec_flush_buffers (audio_codec_context ());
 +      }
        if (_subtitle_codec_context) {
                avcodec_flush_buffers (_subtitle_codec_context);
        }
 +}
  
 -      _just_sought = true;
 -      _video_position = frame;
 +void
 +FFmpegDecoder::seek (ContentTime time, bool accurate)
 +{
 +      Decoder::seek (time, accurate);
 +      AudioDecoder::seek (time, accurate);
        
 -      if (frame == 0 || !accurate) {
 -              /* We're already there, or we're as close as we need to be */
 -              return;
 +      /* If we are doing an accurate seek, our initial shot will be 200ms (200 being
 +         a number plucked from the air) earlier than we want to end up.  The loop below
 +         will hopefully then step through to where we want to be.
 +      */
 +
 +      ContentTime pre_roll = accurate ? (0.2 * TIME_HZ) : 0;
 +      ContentTime initial_seek = time - pre_roll;
 +      if (initial_seek < 0) {
 +              initial_seek = 0;
        }
  
 -      while (1) {
 -              int r = av_read_frame (_format_context, &_packet);
 -              if (r < 0) {
 -                      return;
 -              }
 +      /* Initial seek time in the video stream's timebase */
  
 -              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 + _video_pts_offset) * _ffmpeg_content->video_frame_rate()
 -                              );
 +      seek_and_flush (initial_seek);
  
 -                      if (_video_position >= (frame - 1)) {
 -                              av_free_packet (&_packet);
 -                              break;
 -                      }
 -              }
 -              
 -              av_free_packet (&_packet);
 +      if (!accurate) {
 +              /* That'll do */
 +              return;
 +      }
 +
 +      int const N = minimal_run (boost::bind (&FFmpegDecoder::seek_overrun_finished, this, time, _1, _2));
 +
 +      seek_and_flush (initial_seek);
 +      if (N > 0) {
 +              minimal_run (boost::bind (&FFmpegDecoder::seek_final_finished, this, N - 1, _3));
        }
  }
  
@@@ -436,7 -373,6 +434,7 @@@ FFmpegDecoder::decode_audio_packet (
  
                int frame_finished;
                int const decode_result = avcodec_decode_audio4 (audio_codec_context(), _frame, &frame_finished, &copy_packet);
 +
                if (decode_result < 0) {
                        shared_ptr<const Film> film = _film.lock ();
                        assert (film);
                }
  
                if (frame_finished) {
 -                      
 -                      if (_audio_position == 0) {
 -                              /* Where we are in the source, in seconds */
 -                              double const pts = av_q2d (_format_context->streams[copy_packet.stream_index]->time_base)
 -                                      * av_frame_get_best_effort_timestamp(_frame) + _audio_pts_offset;
 -
 -                              if (pts > 0) {
 -                                      /* Emit some silence */
 -                                      shared_ptr<AudioBuffers> silence (
 -                                              new AudioBuffers (
 -                                                      _ffmpeg_content->audio_channels(),
 -                                                      pts * _ffmpeg_content->content_audio_frame_rate()
 -                                                      )
 -                                              );
 -                                      
 -                                      silence->make_silent ();
 -                                      audio (silence, _audio_position);
 -                              }
 -                      }
 +                      ContentTime const ct = (
 +                              av_frame_get_best_effort_timestamp (_frame) *
 +                              av_q2d (_ffmpeg_content->audio_stream()->stream (_format_context)->time_base)
 +                              + _pts_offset
 +                              ) * TIME_HZ;
                        
                        int const data_size = av_samples_get_buffer_size (
                                0, audio_codec_context()->channels, _frame->nb_samples, audio_sample_format (), 1
                                );
 -                      
 -                      audio (deinterleave_audio (_frame->data, data_size), _audio_position);
 +
 +                      audio (deinterleave_audio (_frame->data, data_size), ct);
                }
                        
                copy_packet.data += decode_result;
@@@ -504,9 -454,45 +502,9 @@@ FFmpegDecoder::decode_video_packet (
                }
                
                if (i->second != AV_NOPTS_VALUE) {
 -
 -                      double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _video_pts_offset;
 -
 -                      if (_just_sought) {
 -                              /* We just did a seek, so disable any attempts to correct for where we
 -                                 are / should be.
 -                              */
 -                              _video_position = rint (pts * _ffmpeg_content->video_frame_rate ());
 -                              _just_sought = false;
 -                      }
 -
 -                      double const next = _video_position / _ffmpeg_content->video_frame_rate();
 -                      double const one_frame = 1 / _ffmpeg_content->video_frame_rate ();
 -                      double delta = pts - next;
 -
 -                      while (delta > one_frame) {
 -                              /* This PTS is more than one frame forward in time of where we think we should be; emit
 -                                 a black frame.
 -                              */
 -
 -                              /* XXX: I think this should be a copy of the last frame... */
 -                              boost::shared_ptr<Image> black (
 -                                      new Image (
 -                                              static_cast<AVPixelFormat> (_frame->format),
 -                                              libdcp::Size (video_codec_context()->width, video_codec_context()->height),
 -                                              true
 -                                              )
 -                                      );
 -                              
 -                              black->make_black ();
 -                              video (image, false, _video_position);
 -                              delta -= one_frame;
 -                      }
 -
 -                      if (delta > -one_frame) {
 -                              /* This PTS is within a frame of being right; emit this (otherwise it will be dropped) */
 -                              video (image, false, _video_position);
 -                      }
 -                              
 +                      double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset;
 +                      VideoFrame const f = rint (pts * _ffmpeg_content->video_frame_rate ());
 +                      video (image, false, f);
                } else {
                        shared_ptr<const Film> film = _film.lock ();
                        assert (film);
@@@ -539,6 -525,14 +537,6 @@@ FFmpegDecoder::setup_subtitle (
        }
  }
  
 -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 ()
  {
        /* Subtitle PTS in seconds (within the source, not taking into account any of the
           source that we may have chopped off for the DCP)
        */
++<<<<<<< HEAD
 +      double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _pts_offset;
 +      
++=======
+       double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _video_pts_offset;
++>>>>>>> master
        /* hence start time for this sub */
 -      Time const from = (packet_time + (double (sub.start_display_time) / 1e3)) * TIME_HZ;
 -      Time const to = (packet_time + (double (sub.end_display_time) / 1e3)) * TIME_HZ;
 +      ContentTime const from = (packet_time + (double (sub.start_display_time) / 1e3)) * TIME_HZ;
 +      ContentTime const to = (packet_time + (double (sub.end_display_time) / 1e3)) * TIME_HZ;
  
        AVSubtitleRect const * rect = sub.rects[0];
  
index e86a82fad72d5b98bac507a2111d3a2bf4e90a3a,a63090d12e72a5bbfc90185a7df2b758012af839..38dd678bbfa91da1f80562f3dac0d7fcf78c0829
@@@ -70,7 -70,6 +70,6 @@@ FFmpegExaminer::FFmpegExaminer (shared_
                }
  
                int frame_finished;
-               avcodec_get_frame_defaults (_frame);
  
                AVCodecContext* context = _format_context->streams[_packet.stream_index]->codec;
  
@@@ -135,10 -134,10 +134,10 @@@ FFmpegExaminer::video_size () cons
  }
  
  /** @return Length (in video frames) according to our content's header */
 -VideoContent::Frame
 +VideoFrame
  FFmpegExaminer::video_length () const
  {
 -      VideoContent::Frame const length = (double (_format_context->duration) / AV_TIME_BASE) * video_frame_rate();
 +      VideoFrame const length = (double (_format_context->duration) / AV_TIME_BASE) * video_frame_rate();
        return max (1, length);
  }
  
diff --combined src/lib/film.cc
index 57d23ec4eed51e3596e89f0abad5376c53f0b75f,1290cbda21fc67f44b924d323607262c1799bff4..099bacfdcb7b5513358f968555e03a864703e338
@@@ -265,6 -265,14 +265,14 @@@ Film::make_dcp (
  #else
        log()->log ("libdcp built in optimised mode.");
  #endif
+ #ifdef DCPOMATIC_WINDOWS
+       OSVERSIONINFO info;
+       info.dwOSVersionInfoSize = sizeof (info);
+       GetVersionEx (&info);
+       log()->log (String::compose ("Windows version %1.%2.%3 SP %4", info.dwMajorVersion, info.dwMinorVersion, info.dwBuildNumber, info.szCSDVersion));
+ #endif        
+       
        log()->log (String::compose ("CPU: %1, %2 processors", cpu_info(), boost::thread::hardware_concurrency ()));
        list<pair<string, string> > const m = mount_info ();
        for (list<pair<string, string> >::const_iterator i = m.begin(); i != m.end(); ++i) {
@@@ -845,7 -853,7 +853,7 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
@@@ -857,18 -865,12 +865,18 @@@ Film::has_subtitles () cons
        return _playlist->has_subtitles ();
  }
  
 -OutputVideoFrame
 +VideoFrame
  Film::best_video_frame_rate () const
  {
        return _playlist->best_dcp_frame_rate ();
  }
  
 +FrameRateChange
 +Film::active_frame_rate_change (DCPTime t) const
 +{
 +      return _playlist->active_frame_rate_change (t, video_frame_rate ());
 +}
 +
  void
  Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
  {
@@@ -887,31 -889,31 +895,31 @@@ Film::playlist_changed (
        signal_changed (CONTENT);
  }     
  
 -OutputAudioFrame
 -Film::time_to_audio_frames (Time t) const
 +AudioFrame
 +Film::time_to_audio_frames (DCPTime t) const
  {
        return t * audio_frame_rate () / TIME_HZ;
  }
  
 -OutputVideoFrame
 -Film::time_to_video_frames (Time t) const
 +VideoFrame
 +Film::time_to_video_frames (DCPTime t) const
  {
        return t * video_frame_rate () / TIME_HZ;
  }
  
 -Time
 -Film::audio_frames_to_time (OutputAudioFrame f) const
 +DCPTime
 +Film::audio_frames_to_time (AudioFrame f) const
  {
        return f * TIME_HZ / audio_frame_rate ();
  }
  
 -Time
 -Film::video_frames_to_time (OutputVideoFrame f) const
 +DCPTime
 +Film::video_frames_to_time (VideoFrame f) const
  {
        return f * TIME_HZ / video_frame_rate ();
  }
  
 -OutputAudioFrame
 +AudioFrame
  Film::audio_frame_rate () const
  {
        /* XXX */
diff --combined src/lib/player.cc
index 260476242fcb91acacebb87a1cb50b61d90367bc,ce51097334be3e6a48107033e8446568d2dc0c4f..77630f0e3c8e4e5d4f442351ae05e876d7d78f00
@@@ -18,7 -18,6 +18,7 @@@
  */
  
  #include <stdint.h>
 +#include <algorithm>
  #include "player.h"
  #include "film.h"
  #include "ffmpeg_decoder.h"
@@@ -32,6 -31,7 +32,6 @@@
  #include "job.h"
  #include "image.h"
  #include "ratio.h"
 -#include "resampler.h"
  #include "log.h"
  #include "scaler.h"
  
@@@ -45,20 -45,69 +45,20 @@@ using std::map
  using boost::shared_ptr;
  using boost::weak_ptr;
  using boost::dynamic_pointer_cast;
 +using boost::optional;
  
  class Piece
  {
  public:
 -      Piece (shared_ptr<Content> c)
 -              : content (c)
 -              , video_position (c->position ())
 -              , audio_position (c->position ())
 -              , repeat_to_do (0)
 -              , repeat_done (0)
 -      {}
 -      
 -      Piece (shared_ptr<Content> c, shared_ptr<Decoder> d)
 +      Piece (shared_ptr<Content> c, shared_ptr<Decoder> d, FrameRateChange f)
                : content (c)
                , decoder (d)
 -              , video_position (c->position ())
 -              , audio_position (c->position ())
 +              , frc (f)
        {}
  
 -      /** Set this piece to repeat a video frame a given number of times */
 -      void set_repeat (IncomingVideo video, int num)
 -      {
 -              repeat_video = video;
 -              repeat_to_do = num;
 -              repeat_done = 0;
 -      }
 -
 -      void reset_repeat ()
 -      {
 -              repeat_video.image.reset ();
 -              repeat_to_do = 0;
 -              repeat_done = 0;
 -      }
 -
 -      bool repeating () const
 -      {
 -              return repeat_done != repeat_to_do;
 -      }
 -
 -      void repeat (Player* player)
 -      {
 -              player->process_video (
 -                      repeat_video.weak_piece,
 -                      repeat_video.image,
 -                      repeat_video.eyes,
 -                      repeat_done > 0,
 -                      repeat_video.frame,
 -                      (repeat_done + 1) * (TIME_HZ / player->_film->video_frame_rate ())
 -                      );
 -
 -              ++repeat_done;
 -      }
 -      
        shared_ptr<Content> content;
        shared_ptr<Decoder> decoder;
 -      /** Time of the last video we emitted relative to the start of the DCP */
 -      Time video_position;
 -      /** Time of the last audio we emitted relative to the start of the DCP */
 -      Time audio_position;
 -
 -      IncomingVideo repeat_video;
 -      int repeat_to_do;
 -      int repeat_done;
 +      FrameRateChange frc;
  };
  
  Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
        , _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)
 +      , _just_did_inaccurate_seek (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));
@@@ -99,157 -146,111 +99,157 @@@ Player::pass (
                setup_pieces ();
        }
  
 -      Time earliest_t = TIME_MAX;
 -      shared_ptr<Piece> earliest;
 -      enum {
 -              VIDEO,
 -              AUDIO
 -      } type = VIDEO;
 +      /* Interrogate all our pieces to find the one with the earliest decoded data */
 +
 +      shared_ptr<Piece> earliest_piece;
 +      shared_ptr<Decoded> earliest_decoded;
 +      DCPTime earliest_time = TIME_MAX;
 +      DCPTime earliest_audio = TIME_MAX;
  
        for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              if ((*i)->decoder->done ()) {
 -                      continue;
 -              }
  
 -              shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder);
 -              shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 +              DCPTime const offset = (*i)->content->position() - (*i)->content->trim_start();
 +              
 +              bool done = false;
 +              shared_ptr<Decoded> dec;
 +              while (!done) {
 +                      dec = (*i)->decoder->peek ();
 +                      if (!dec) {
 +                              /* Decoder has nothing else to give us */
 +                              break;
 +                      }
  
 -              if (_video && vd) {
 -                      if ((*i)->video_position < earliest_t) {
 -                              earliest_t = (*i)->video_position;
 -                              earliest = *i;
 -                              type = VIDEO;
 +                      dec->set_dcp_times (_film->video_frame_rate(), _film->audio_frame_rate(), (*i)->frc, offset);
 +                      DCPTime const t = dec->dcp_time - offset;
 +                      if (t >= ((*i)->content->full_length() - (*i)->content->trim_end ())) {
 +                              /* In the end-trimmed part; decoder has nothing else to give us */
 +                              dec.reset ();
 +                              done = true;
 +                      } else if (t >= (*i)->content->trim_start ()) {
 +                              /* Within the un-trimmed part; everything's ok */
 +                              done = true;
 +                      } else {
 +                              /* Within the start-trimmed part; get something else */
 +                              (*i)->decoder->consume ();
                        }
                }
  
 -              if (_audio && ad && ad->has_audio ()) {
 -                      if ((*i)->audio_position < earliest_t) {
 -                              earliest_t = (*i)->audio_position;
 -                              earliest = *i;
 -                              type = AUDIO;
 -                      }
 +              if (!dec) {
 +                      continue;
 +              }
 +
 +              if (dec->dcp_time < earliest_time) {
 +                      earliest_piece = *i;
 +                      earliest_decoded = dec;
 +                      earliest_time = dec->dcp_time;
                }
 -      }
  
 -      if (!earliest) {
 +              if (dynamic_pointer_cast<DecodedAudio> (dec) && dec->dcp_time < earliest_audio) {
 +                      earliest_audio = dec->dcp_time;
 +              }
 +      }
 +              
 +      if (!earliest_piece) {
                flush ();
                return true;
        }
  
 -      switch (type) {
 -      case VIDEO:
 -              if (earliest_t > _video_position) {
 -                      emit_black ();
 -              } else {
 -                      if (earliest->repeating ()) {
 -                              earliest->repeat (this);
 +      if (earliest_audio != TIME_MAX) {
 +              TimedAudioBuffers<DCPTime> tb = _audio_merger.pull (max (int64_t (0), earliest_audio));
 +              Audio (tb.audio, tb.time);
 +              /* This assumes that the audio_frames_to_time conversion is exact
 +                 so that there are no accumulated errors caused by rounding.
 +              */
 +              _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +      }
 +
 +      /* Emit the earliest thing */
 +
 +      shared_ptr<DecodedVideo> dv = dynamic_pointer_cast<DecodedVideo> (earliest_decoded);
 +      shared_ptr<DecodedAudio> da = dynamic_pointer_cast<DecodedAudio> (earliest_decoded);
 +      shared_ptr<DecodedSubtitle> ds = dynamic_pointer_cast<DecodedSubtitle> (earliest_decoded);
 +
 +      /* Will be set to false if we shouldn't consume the peeked DecodedThing */
 +      bool consume = true;
 +
 +      if (dv && _video) {
 +
 +              if (_just_did_inaccurate_seek) {
 +
 +                      /* Just emit; no subtlety */
 +                      emit_video (earliest_piece, dv);
 +                      step_video_position (dv);
 +                      
 +              } else if (dv->dcp_time > _video_position) {
 +
 +                      /* Too far ahead */
 +
 +                      list<shared_ptr<Piece> >::iterator i = _pieces.begin();
 +                      while (i != _pieces.end() && ((*i)->content->position() >= _video_position || _video_position >= (*i)->content->end())) {
 +                              ++i;
 +                      }
 +
 +                      if (i == _pieces.end() || !_last_incoming_video.video || !_have_valid_pieces) {
 +                              /* We're outside all video content */
 +                              emit_black ();
 +                              _statistics.video.black++;
                        } else {
 -                              earliest->decoder->pass ();
 +                              /* We're inside some video; repeat the frame */
 +                              _last_incoming_video.video->dcp_time = _video_position;
 +                              emit_video (_last_incoming_video.weak_piece, _last_incoming_video.video);
 +                              step_video_position (_last_incoming_video.video);
 +                              _statistics.video.repeat++;
                        }
 -              }
 -              break;
  
 -      case AUDIO:
 -              if (earliest_t > _audio_position) {
 -                      emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position));
 +                      consume = false;
 +
 +              } else if (dv->dcp_time == _video_position) {
 +                      /* We're ok */
 +                      emit_video (earliest_piece, dv);
 +                      step_video_position (dv);
 +                      _statistics.video.good++;
                } 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 ());
 -                                      }
 -                              }
 -                      }
 +                      /* Too far behind: skip */
 +                      _statistics.video.skip++;
                }
 -              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;
 -                      }
 +              _just_did_inaccurate_seek = false;
  
 -                      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);
 -                      }
 -              }
 +      } else if (da && _audio) {
  
 -              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 ());
 +              if (da->dcp_time > _audio_position) {
 +                      /* Too far ahead */
 +                      emit_silence (da->dcp_time - _audio_position);
 +                      consume = false;
 +                      _statistics.audio.silence += (da->dcp_time - _audio_position);
 +              } else if (da->dcp_time == _audio_position) {
 +                      /* We're ok */
 +                      emit_audio (earliest_piece, da);
 +                      _statistics.audio.good += da->data->frames();
 +              } else {
 +                      /* Too far behind: skip */
 +                      _statistics.audio.skip += da->data->frames();
                }
 -      }
                
 +      } else if (ds && _video) {
 +              _in_subtitle.piece = earliest_piece;
 +              _in_subtitle.subtitle = ds;
 +              update_subtitle ();
 +      }
 +
 +      if (consume) {
 +              earliest_piece->decoder->consume ();
 +      }                       
 +      
        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 Image> image, Eyes eyes, bool same, VideoContent::Frame frame, Time extra)
 +Player::emit_video (weak_ptr<Piece> weak_piece, shared_ptr<DecodedVideo> video)
  {
        /* 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.same = same;
 -      _last_incoming_video.frame = frame;
 -      _last_incoming_video.extra = extra;
 +      _last_incoming_video.video = video;
        
        shared_ptr<Piece> piece = weak_piece.lock ();
        if (!piece) {
        shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
        assert (content);
  
 -      FrameRateConversion frc (content->video_frame_rate(), _film->video_frame_rate());
 -      if (frc.skip && (frame % 2) == 1) {
 -              return;
 -      }
 -
 -      Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate());
 -      if (content->trimmed (relative_time)) {
 -              return;
 -      }
 +      FrameRateChange frc (content->video_frame_rate(), _film->video_frame_rate());
  
 -      Time const time = content->position() + relative_time + extra - content->trim_start ();
        float const ratio = content->ratio() ? content->ratio()->ratio() : content->video_size_after_crop().ratio();
 -      libdcp::Size const image_size = fit_ratio_within (ratio, _video_container_size);
 +      libdcp::Size image_size = fit_ratio_within (ratio, _video_container_size);
 +      if (_approximate_size) {
 +              image_size.width &= ~3;
 +              image_size.height &= ~3;
 +      }
  
        shared_ptr<PlayerImage> pi (
                new PlayerImage (
 -                      image,
 +                      video->image,
                        content->crop(),
                        image_size,
                        _video_container_size,
                        )
                );
        
 -      if (_film->with_subtitles () && _out_subtitle.image && time >= _out_subtitle.from && time <= _out_subtitle.to) {
 +      if (
 +              _film->with_subtitles () &&
 +              _out_subtitle.image &&
 +              video->dcp_time >= _out_subtitle.from && video->dcp_time <= _out_subtitle.to
 +              ) {
  
                Position<int> const container_offset (
                        (_video_container_size.width - image_size.width) / 2,
 -                      (_video_container_size.height - image_size.width) / 2
 +                      (_video_container_size.height - image_size.height) / 2
                        );
  
                pi->set_subtitle (_out_subtitle.image, _out_subtitle.position + container_offset);
        }
+               
                                            
  #ifdef DCPOMATIC_DEBUG
        _last_video = piece->content;
  #endif
  
 -      Video (pi, eyes, content->colour_conversion(), same, time);
 -
 +      Video (pi, video->eyes, content->colour_conversion(), video->same, video->dcp_time);
 +      
        _last_emit_was_black = false;
 -      _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate());
 +}
  
 -      if (frc.repeat > 1 && !piece->repeating ()) {
 -              piece->set_repeat (_last_incoming_video, frc.repeat - 1);
 +void
 +Player::step_video_position (shared_ptr<DecodedVideo> video)
 +{
 +      /* This is a bit of a hack; don't update _video_position if EYES_RIGHT is on its way */
 +      if (video->eyes != EYES_LEFT) {
 +              /* This assumes that the video_frames_to_time conversion is exact
 +                 so that there are no accumulated errors caused by rounding.
 +              */
 +              _video_position += _film->video_frames_to_time (1);
        }
  }
  
  void
 -Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame)
 +Player::emit_audio (weak_ptr<Piece> weak_piece, shared_ptr<DecodedAudio> audio)
  {
        shared_ptr<Piece> piece = weak_piece.lock ();
        if (!piece) {
  
        /* Gain */
        if (content->audio_gain() != 0) {
 -              shared_ptr<AudioBuffers> gain (new AudioBuffers (audio));
 +              shared_ptr<AudioBuffers> gain (new AudioBuffers (audio->data));
                gain->apply_gain (content->audio_gain ());
 -              audio = gain;
 +              audio->data = 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);
 -
 -      if (content->trimmed (relative_time)) {
 -              return;
 -      }
 -
 -      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()));
 +      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->data->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(),
 +                                      audio->data.get(),
                                        i,
                                        static_cast<libdcp::Channel> (j),
                                        map.get (i, static_cast<libdcp::Channel> (j))
                }
        }
  
 -      audio = dcp_mapped;
 +      audio->data = 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 ()) {
 +      /* Delay */
 +      audio->dcp_time += content->audio_delay() * TIME_HZ / 1000;
 +      if (audio->dcp_time < 0) {
 +              int const frames = - audio->dcp_time * _film->audio_frame_rate() / TIME_HZ;
 +              if (frames >= audio->data->frames ()) {
                        return;
                }
  
 -              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames));
 -              trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0);
 +              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->data->channels(), audio->data->frames() - frames));
 +              trimmed->copy_from (audio->data.get(), audio->data->frames() - frames, frames, 0);
  
 -              audio = trimmed;
 -              time = 0;
 +              audio->data = trimmed;
 +              audio->dcp_time = 0;
        }
  
 -      _audio_merger.push (audio, time);
 -      piece->audio_position += _film->audio_frames_to_time (audio->frames ());
 +      _audio_merger.push (audio->data, audio->dcp_time);
  }
  
  void
  Player::flush ()
  {
 -      TimedAudioBuffers<Time> tb = _audio_merger.flush ();
 +      TimedAudioBuffers<DCPTime> tb = _audio_merger.flush ();
        if (_audio && tb.audio) {
                Audio (tb.audio, tb.time);
                _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
        }
  
        while (_audio && _audio_position < _video_position) {
 -              emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
 +              emit_silence (_video_position - _audio_position);
        }
        
  }
   *  @return true on error
   */
  void
 -Player::seek (Time t, bool accurate)
 +Player::seek (DCPTime t, bool accurate)
  {
        if (!_have_valid_pieces) {
                setup_pieces ();
        }
  
        for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> ((*i)->content);
 -              if (!vc) {
 -                      continue;
 -              }
 -
                /* s is the offset of t from the start position of this content */
 -              Time s = t - vc->position ();
 -              s = max (static_cast<Time> (0), s);
 -              s = min (vc->length_after_trim(), s);
 +              DCPTime s = t - (*i)->content->position ();
 +              s = max (static_cast<DCPTime> (0), s);
 +              s = min ((*i)->content->length_after_trim(), s);
  
 -              /* Hence set the piece positions to the `global' time */
 -              (*i)->video_position = (*i)->audio_position = vc->position() + s;
 +              /* Convert this to the content time */
 +              ContentTime ct = (s + (*i)->content->trim_start()) * (*i)->frc.speed_up;
  
                /* And seek the decoder */
 -              dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
 -                      vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
 -                      );
 -
 -              (*i)->reset_repeat ();
 +              (*i)->decoder->seek (ct, accurate);
        }
  
 -      _video_position = _audio_position = t;
 +      _video_position = time_round_up (t, TIME_HZ / _film->video_frame_rate());
 +      _audio_position = time_round_up (t, TIME_HZ / _film->audio_frame_rate());
  
 -      /* XXX: don't seek audio because we don't need to... */
 +      _audio_merger.clear (_audio_position);
 +
 +      if (!accurate) {
 +              /* We just did an inaccurate seek, so it's likely that the next thing seen
 +                 out of pass() will be a fair distance from _{video,audio}_position.  Setting
 +                 this flag stops pass() from trying to fix that: we assume that if it
 +                 was an inaccurate seek then the caller does not care too much about
 +                 inserting black/silence to keep the time tidy.
 +              */
 +              _just_did_inaccurate_seek = true;
 +      }
  }
  
  void
  Player::setup_pieces ()
  {
        list<shared_ptr<Piece> > old_pieces = _pieces;
 -
        _pieces.clear ();
  
        ContentList content = _playlist->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
  
        for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
  
 -              shared_ptr<Piece> piece (new Piece (*i));
 -
 -              /* XXX: into content? */
 +              shared_ptr<Decoder> decoder;
 +              optional<FrameRateChange> frc;
  
                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, 0));
 -                      fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
 -                      fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
 -
 -                      fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true);
 -                      piece->decoder = fd;
 +                      decoder.reset (new FFmpegDecoder (_film, fc, _video, _audio));
 +                      frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
                }
                
                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;
 +                                      decoder = imd;
                                }
                        }
  
 -                      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, 0));
 -                              piece->decoder = id;
 +                      if (!decoder) {
 +                              decoder.reset (new ImageDecoder (_film, ic));
                        }
 +
 +                      frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
                }
  
                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));
 +                      decoder.reset (new SndfileDecoder (_film, sc));
 +
 +                      /* Working out the frc for this content is a bit tricky: what if it overlaps
 +                         two pieces of video content with different frame rates?  For now, use
 +                         the one with the best overlap.
 +                      */
 +
 +                      DCPTime best_overlap_t = 0;
 +                      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(), sc->position()) - min (vc->end(), sc->end());
 +                              if (overlap > best_overlap_t) {
 +                                      best_overlap = vc;
 +                                      best_overlap_t = overlap;
 +                              }
 +                      }
  
 -                      piece->decoder = sd;
 +                      if (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 */
 +                              frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
 +                      }
                }
  
 -              _pieces.push_back (piece);
 +              ContentTime st = (*i)->trim_start() * frc->speed_up;
 +              decoder->seek (st, true);
 +              
 +              _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
        }
  
        _have_valid_pieces = true;
 +
 +      /* The Piece for the _last_incoming_video will no longer be valid */
 +      _last_incoming_video.video.reset ();
 +
 +      _video_position = _audio_position = 0;
  }
  
  void
@@@ -574,6 -565,29 +575,6 @@@ Player::set_video_container_size (libdc
                );
  }
  
 -shared_ptr<Resampler>
 -Player::resampler (shared_ptr<AudioContent> c, bool create)
 -{
 -      map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c);
 -      if (i != _resamplers.end ()) {
 -              return i->second;
 -      }
 -
 -      if (!create) {
 -              return shared_ptr<Resampler> ();
 -      }
 -
 -      _film->log()->log (
 -              String::compose (
 -                      "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;
 -}
 -
  void
  Player::emit_black ()
  {
  }
  
  void
 -Player::emit_silence (OutputAudioFrame most)
 +Player::emit_silence (DCPTime most)
  {
        if (most == 0) {
                return;
        }
        
 -      OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
 -      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
 +      DCPTime t = min (most, TIME_HZ / 2);
 +      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), t * _film->audio_frame_rate() / TIME_HZ));
        silence->make_silent ();
        Audio (silence, _audio_position);
 -      _audio_position += _film->audio_frames_to_time (N);
 +      
 +      _audio_position += t;
  }
  
  void
@@@ -614,6 -627,18 +615,6 @@@ Player::film_changed (Film::Property p
        }
  }
  
 -void
 -Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
 -{
 -      _in_subtitle.piece = weak_piece;
 -      _in_subtitle.image = image;
 -      _in_subtitle.rect = rect;
 -      _in_subtitle.from = from;
 -      _in_subtitle.to = to;
 -
 -      update_subtitle ();
 -}
 -
  void
  Player::update_subtitle ()
  {
                return;
        }
  
 -      if (!_in_subtitle.image) {
 +      if (!_in_subtitle.subtitle->image) {
                _out_subtitle.image.reset ();
                return;
        }
        shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (piece->content);
        assert (sc);
  
 -      dcpomatic::Rect<double> in_rect = _in_subtitle.rect;
 +      dcpomatic::Rect<double> in_rect = _in_subtitle.subtitle->rect;
        libdcp::Size scaled_size;
  
        in_rect.y += sc->subtitle_offset ();
        
        _out_subtitle.position.x = rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - sc->subtitle_scale ()) / 2)));
        _out_subtitle.position.y = rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - sc->subtitle_scale ()) / 2)));
 -      
 -      _out_subtitle.image = _in_subtitle.image->scale (
 +
 +      _out_subtitle.image = _in_subtitle.subtitle->image->scale (
                scaled_size,
                Scaler::from_id ("bicubic"),
 -              _in_subtitle.image->pixel_format (),
 +              PIX_FMT_RGBA,
                true
                );
  
++<<<<<<< HEAD
 +      _out_subtitle.from = _in_subtitle.subtitle->dcp_time;
 +      _out_subtitle.to = _in_subtitle.subtitle->dcp_time_to;
++=======
+       /* XXX: hack */
+       Time from = _in_subtitle.from;
+       Time to = _in_subtitle.to;
+       shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (piece->content);
+       if (vc) {
+               from = rint (from * vc->video_frame_rate() / _film->video_frame_rate());
+               to = rint (to * vc->video_frame_rate() / _film->video_frame_rate());
+       }
+       
+       _out_subtitle.from = from * piece->content->position ();
+       _out_subtitle.to = to + piece->content->position ();
++>>>>>>> master
  }
  
  /** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
  bool
  Player::repeat_last_video ()
  {
 -      if (!_last_incoming_video.image || !_have_valid_pieces) {
 +      if (!_last_incoming_video.video || !_have_valid_pieces) {
                return false;
        }
  
 -      process_video (
 +      emit_video (
                _last_incoming_video.weak_piece,
 -              _last_incoming_video.image,
 -              _last_incoming_video.eyes,
 -              _last_incoming_video.same,
 -              _last_incoming_video.frame,
 -              _last_incoming_video.extra
 +              _last_incoming_video.video
                );
  
        return true;
  }
  
 +void
 +Player::set_approximate_size ()
 +{
 +      _approximate_size = true;
 +}
 +                            
 +
  PlayerImage::PlayerImage (
        shared_ptr<const Image> in,
        Crop crop,
@@@ -714,10 -745,10 +729,10 @@@ PlayerImage::set_subtitle (shared_ptr<c
  }
  
  shared_ptr<Image>
 -PlayerImage::image ()
 +PlayerImage::image (AVPixelFormat format, bool aligned)
  {
 -      shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, false);
 -
 +      shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, format, aligned);
 +      
        Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
  
        if (_subtitle_image) {
  
        return out;
  }
 +
 +void
 +PlayerStatistics::dump (shared_ptr<Log> log) const
 +{
 +      log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat));
 +      log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence));
 +}
 +
 +PlayerStatistics const &
 +Player::statistics () const
 +{
 +      return _statistics;
 +}
diff --combined src/lib/transcode_job.cc
index fe07ba2f656af010c1ff989a53a4f1e088822ed6,87fd5daefca06d06ed1e2fee5cf6eb89237d4104..8820726895446e449b12090b8f7e221a45f1b619
@@@ -64,12 -64,11 +64,11 @@@ TranscodeJob::run (
  
                _film->log()->log (N_("Transcode job completed successfully"));
  
-       } catch (std::exception& e) {
+       } catch (...) {
                set_progress (1);
                set_state (FINISHED_ERROR);
-               _film->log()->log (String::compose (N_("Transcode job failed (%1)"), e.what()));
+               _film->log()->log (N_("Transcode job failed or cancelled"));
+               _transcoder.reset ();
                throw;
        }
  }
@@@ -78,7 -77,7 +77,7 @@@ strin
  TranscodeJob::status () const
  {
        if (!_transcoder) {
-               return _("0%");
+               return Job::status ();
        }
  
        float const fps = _transcoder->current_encoding_rate ();
@@@ -111,6 -110,6 +110,6 @@@ TranscodeJob::remaining_time () cons
        }
  
        /* Compute approximate proposed length here, as it's only here that we need it */
 -      OutputVideoFrame const left = _film->time_to_video_frames (_film->length ()) - _transcoder->video_frames_out();
 +      VideoFrame const left = _film->time_to_video_frames (_film->length ()) - _transcoder->video_frames_out();
        return left / fps;
  }