Merge speed-up branch.
authorCarl Hetherington <cth@carlh.net>
Sun, 16 Dec 2012 14:53:22 +0000 (14:53 +0000)
committerCarl Hetherington <cth@carlh.net>
Sun, 16 Dec 2012 14:53:22 +0000 (14:53 +0000)
1  2 
src/lib/examine_content_job.cc
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_decoder.h
src/lib/film.cc
src/lib/film.h
src/lib/imagemagick_decoder.h
src/lib/transcoder.cc
src/lib/transcoder.h
src/lib/video_decoder.h
src/wx/film_editor.cc
src/wx/film_viewer.cc

index dc2fc305cdd5618524371bf299affbd101ba0ee6,93333605b8e6a6983fc63fd668f420bd426fae35..70a04b8255059262ec0de2fc8e0c929a277b6702
@@@ -26,6 -26,7 +26,6 @@@
  #include "options.h"
  #include "decoder_factory.h"
  #include "decoder.h"
 -#include "imagemagick_encoder.h"
  #include "transcoder.h"
  #include "log.h"
  #include "film.h"
@@@ -59,30 -60,111 +59,46 @@@ ExamineContentJob::name () cons
  void
  ExamineContentJob::run ()
  {
-       /* Decode the content to get an accurate length */
 -      float progress_remaining = 1;
++      descend (1);
  
-       /* We don't want to use any existing length here, as progress
-          will be messed up.
+       /* Set the film's length to either
+          a) a length judged by running through the content or
+          b) the length from a decoder's header.
        */
-       _film->unset_length ();
-       _film->set_crop (Crop ());
 -
+       if (!_film->trust_content_header()) {
+               /* Decode the content to get an accurate length */
+               
+               /* We don't want to use any existing length here, as progress
+                  will be messed up.
+               */
+               _film->unset_length ();
++              _film->set_crop (Crop ());
+               
 -              shared_ptr<Options> o (new Options ("", "", ""));
 -              o->out_size = Size (512, 512);
 -              o->apply_crop = false;
++              shared_ptr<DecodeOptions> o (new DecodeOptions);
+               o->decode_audio = false;
+               
 -              descend (0.5);
 -              
 -              pair<shared_ptr<VideoDecoder>, shared_ptr<AudioDecoder> > decoders = decoder_factory (_film, o, this);
++              Decoders decoders = decoder_factory (_film, o, this);
+               
+               set_progress_unknown ();
 -              while (!decoders.first->pass()) {
++              while (!decoders.video->pass()) {
+                       /* keep going */
+               }
+               
 -              _film->set_length (decoders.first->video_frame());
++              _film->set_length (decoders.video->video_frame());
+               
+               _film->log()->log (String::compose ("Video length examined as %1 frames", _film->length().get()));
+               
 -              ascend ();
 -              
 -              progress_remaining -= 0.5;
 -              
+       } else {
 -              /* Get a quick decoder to get the content's length from its header.
 -                 It would have been nice to just use the thumbnail transcoder's decoder,
 -                 but that's a bit fiddly, and this isn't too expensive.
 -              */
++              /* Get a quick decoder to get the content's length from its header */
+               
 -              shared_ptr<Options> o (new Options ("", "", ""));
 -              o->out_size = Size (1024, 1024);
 -              pair<shared_ptr<VideoDecoder>, shared_ptr<AudioDecoder> > d = decoder_factory (_film, o, 0);
 -              _film->set_length (d.first->length());
++              shared_ptr<DecodeOptions> o (new DecodeOptions);
++              Decoders d = decoder_factory (_film, o, 0);
++              _film->set_length (d.video->length());
        
-       shared_ptr<DecodeOptions> o (new DecodeOptions);
-       o->decode_audio = false;
-       descend (1);
-       Decoders decoders = decoder_factory (_film, o, this);
-       set_progress_unknown ();
-       while (!decoders.video->pass()) {
-               /* keep going */
+               _film->log()->log (String::compose ("Video length obtained from header as %1 frames", _film->length().get()));
        }
  
-       _film->set_length (decoders.video->video_frame());
 -      /* Now make thumbnails for it */
--
-       _film->log()->log (String::compose ("Video length is %1 frames", _film->length()));
 -      descend (progress_remaining);
 -
 -      try {
 -              shared_ptr<Options> o (new Options (_film->dir ("thumbs"), ".png", ""));
 -              o->out_size = _film->size ();
 -              o->apply_crop = false;
 -              o->decode_audio = false;
 -              o->decode_video_skip = _film->length().get() / 128;
 -              o->decode_subtitles = true;
 -              shared_ptr<ImageMagickEncoder> e (new ImageMagickEncoder (_film, o));
 -              Transcoder w (_film, o, this, e);
 -              w.go ();
 -
 -              /* Now set the film's length from the transcoder's decoder, since we
 -                 went to all the trouble of going through the content.
 -              */
 -
 -              _film->set_length (w.video_decoder()->video_frame());
 -              
 -      } catch (std::exception& e) {
 -
 -              ascend ();
 -              set_progress (1);
 -              set_error (e.what ());
 -              set_state (FINISHED_ERROR);
 -              return;
 -              
 -      }
 -
 -      string const tdir = _film->dir ("thumbs");
 -      vector<SourceFrame> thumbs;
 -
 -      for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (tdir); i != boost::filesystem::directory_iterator(); ++i) {
 -
 -              /* Aah, the sweet smell of progress */
 -#if BOOST_FILESYSTEM_VERSION == 3             
 -              string const l = boost::filesystem::path(*i).leaf().generic_string();
 -#else
 -              string const l = i->leaf ();
 -#endif
 -              
 -              size_t const d = l.find (".png");
 -              size_t const t = l.find (".tmp");
 -              if (d != string::npos && t == string::npos) {
 -                      thumbs.push_back (atoi (l.substr (0, d).c_str()));
 -              }
 -      }
 -
 -      sort (thumbs.begin(), thumbs.end());
 -      _film->set_thumbs (thumbs);     
--
        ascend ();
        set_progress (1);
        set_state (FINISHED_OK);
index 24ee89b21c30fb0f81d516683ecbed9cd69cdd12,acaf149f43ade599dff78d95c109d36c86133097..136843190d9cbd1cb46290eb8e5dcd7c209ee671
@@@ -59,7 -59,7 +59,7 @@@ using boost::shared_ptr
  using boost::optional;
  using boost::dynamic_pointer_cast;
  
 -FFmpegDecoder::FFmpegDecoder (shared_ptr<Film> f, shared_ptr<const Options> o, Job* j)
 +FFmpegDecoder::FFmpegDecoder (shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j)
        : Decoder (f, o, j)
        , VideoDecoder (f, o, j)
        , AudioDecoder (f, o, j)
@@@ -77,8 -77,6 +77,8 @@@
        setup_video ();
        setup_audio ();
        setup_subtitle ();
 +
 +      _film_connection = f->Changed.connect (bind (&FFmpegDecoder::film_changed, this, _1));
  }
  
  FFmpegDecoder::~FFmpegDecoder ()
  void
  FFmpegDecoder::setup_general ()
  {
 -      int r;
 -      
        av_register_all ();
  
 -      if ((r = avformat_open_input (&_format_context, _film->content_path().c_str(), 0, 0)) != 0) {
 +      if (avformat_open_input (&_format_context, _film->content_path().c_str(), 0, 0) < 0) {
                throw OpenFileError (_film->content_path ());
        }
  
@@@ -164,14 -164,7 +164,7 @@@ FFmpegDecoder::setup_video (
                throw DecodeError ("could not find video decoder");
        }
  
-       /* I think this prevents problems with green hash on decodes and
-          "changing frame properties on the fly is not supported by all filters"
-          messages with some content.  Although I'm not sure; needs checking.
-       */
-       AVDictionary* opts = 0;
-       av_dict_set (&opts, "threads", "1", 0);
-       
-       if (avcodec_open2 (_video_codec_context, _video_codec, &opts) < 0) {
+       if (avcodec_open2 (_video_codec_context, _video_codec, 0) < 0) {
                throw DecodeError ("could not open video decoder");
        }
  }
@@@ -272,10 -265,46 +265,10 @@@ FFmpegDecoder::pass (
                                _film->log()->log (String::compose ("Used only %1 bytes of %2 in packet", r, _packet.size));
                        }
  
 -                      /* Where we are in the output, in seconds */
 -                      double const out_pts_seconds = video_frame() / frames_per_second();
 -
 -                      /* Where we are in the source, in seconds */
 -                      double const source_pts_seconds = av_q2d (_format_context->streams[_packet.stream_index]->time_base)
 -                              * av_frame_get_best_effort_timestamp(_frame);
 -
 -                      _film->log()->log (
 -                              String::compose ("Source video frame ready; source at %1, output at %2", source_pts_seconds, out_pts_seconds),
 -                              Log::VERBOSE
 -                              );
 -
 -                      if (!_first_video) {
 -                              _first_video = source_pts_seconds;
 -                      }
 -
 -                      /* Difference between where we are and where we should be */
 -                      double const delta = source_pts_seconds - _first_video.get() - out_pts_seconds;
 -                      double const one_frame = 1 / frames_per_second();
 -
 -                      /* Insert frames if required to get out_pts_seconds up to pts_seconds */
 -                      if (delta > one_frame) {
 -                              int const extra = rint (delta / one_frame);
 -                              for (int i = 0; i < extra; ++i) {
 -                                      repeat_last_video ();
 -                                      _film->log()->log (
 -                                              String::compose (
 -                                                      "Extra video frame inserted at %1s; source frame %2, source PTS %3 (at %4 fps)",
 -                                                      out_pts_seconds, video_frame(), source_pts_seconds, frames_per_second()
 -                                                      )
 -                                              );
 -                              }
 -                      }
 -
 -                      if (delta > -one_frame) {
 -                              /* Process this frame */
 -                              filter_and_emit_video (_frame);
 +                      if (_opt->video_sync) {
 +                              out_with_sync ();
                        } else {
 -                              /* Otherwise we are omitting a frame to keep things right */
 -                              _film->log()->log (String::compose ("Frame removed at %1s", out_pts_seconds));
 +                              filter_and_emit_video (_frame);
                        }
                }
  
@@@ -528,8 -557,6 +521,8 @@@ FFmpegDecoder::set_subtitle_stream (sha
  void
  FFmpegDecoder::filter_and_emit_video (AVFrame* frame)
  {
 +      boost::mutex::scoped_lock lm (_filter_graphs_mutex);
 +      
        shared_ptr<FilterGraph> graph;
  
        list<shared_ptr<FilterGraph> >::iterator i = _filter_graphs.begin();
        }
  
        if (i == _filter_graphs.end ()) {
 -              graph.reset (new FilterGraph (_film, this, _opt->apply_crop, Size (frame->width, frame->height), (AVPixelFormat) frame->format));
 +              graph.reset (new FilterGraph (_film, this, Size (frame->width, frame->height), (AVPixelFormat) frame->format));
                _filter_graphs.push_back (graph);
                _film->log()->log (String::compose ("New graph for %1x%2, pixel format %3", frame->width, frame->height, frame->format));
        } else {
  
        list<shared_ptr<Image> > images = graph->process (frame);
  
 +      SourceFrame const sf = av_q2d (_format_context->streams[_video_stream]->time_base)
 +              * av_frame_get_best_effort_timestamp(_frame) * frames_per_second();
 +
        for (list<shared_ptr<Image> >::iterator i = images.begin(); i != images.end(); ++i) {
 -              emit_video (*i);
 +              emit_video (*i, sf);
        }
  }
  
 +bool
 +FFmpegDecoder::seek (SourceFrame f)
 +{
 +      int64_t const t = static_cast<int64_t>(f) / (av_q2d (_format_context->streams[_video_stream]->time_base) * frames_per_second());
 +      int const r = av_seek_frame (_format_context, _video_stream, t, 0);
 +      avcodec_flush_buffers (_video_codec_context);
 +      return r < 0;
 +}
 +
  shared_ptr<FFmpegAudioStream>
  FFmpegAudioStream::create (string t, optional<int> v)
  {
@@@ -616,67 -631,9 +609,73 @@@ FFmpegAudioStream::to_string () cons
        return String::compose ("ffmpeg %1 %2 %3 %4", _id, _sample_rate, _channel_layout, _name);
  }
  
 +void
 +FFmpegDecoder::out_with_sync ()
 +{
 +      /* Where we are in the output, in seconds */
 +      double const out_pts_seconds = video_frame() / frames_per_second();
 +      
 +      /* Where we are in the source, in seconds */
 +      double const source_pts_seconds = av_q2d (_format_context->streams[_packet.stream_index]->time_base)
 +              * av_frame_get_best_effort_timestamp(_frame);
 +      
 +      _film->log()->log (
 +              String::compose ("Source video frame ready; source at %1, output at %2", source_pts_seconds, out_pts_seconds),
 +              Log::VERBOSE
 +              );
 +      
 +      if (!_first_video) {
 +              _first_video = source_pts_seconds;
 +      }
 +      
 +      /* Difference between where we are and where we should be */
 +      double const delta = source_pts_seconds - _first_video.get() - out_pts_seconds;
 +      double const one_frame = 1 / frames_per_second();
 +      
 +      /* Insert frames if required to get out_pts_seconds up to pts_seconds */
 +      if (delta > one_frame) {
 +              int const extra = rint (delta / one_frame);
 +              for (int i = 0; i < extra; ++i) {
 +                      repeat_last_video ();
 +                      _film->log()->log (
 +                              String::compose (
 +                                      "Extra video frame inserted at %1s; source frame %2, source PTS %3 (at %4 fps)",
 +                                      out_pts_seconds, video_frame(), source_pts_seconds, frames_per_second()
 +                                                      )
 +                              );
 +              }
 +      }
 +      
 +      if (delta > -one_frame) {
 +              /* Process this frame */
 +              filter_and_emit_video (_frame);
 +      } else {
 +              /* Otherwise we are omitting a frame to keep things right */
 +              _film->log()->log (String::compose ("Frame removed at %1s", out_pts_seconds));
 +      }
 +}
 +
 +void
 +FFmpegDecoder::film_changed (Film::Property p)
 +{
 +      switch (p) {
 +      case Film::CROP:
 +      {
 +              boost::mutex::scoped_lock lm (_filter_graphs_mutex);
 +              _filter_graphs.clear ();
 +      }
 +      OutputChanged ();
 +      break;
 +
 +      default:
 +              break;
 +      }
 +}
 +
+ /** @return Length (in video frames) according to our content's header */
+ SourceFrame
+ FFmpegDecoder::length () const
+ {
+       return (double(_format_context->duration) / AV_TIME_BASE) * frames_per_second();
+ }
++
diff --combined src/lib/ffmpeg_decoder.h
index 1db46a4230c7390b908ea651684be5d9b06820c7,1771551fcf1080cb5bb7b38b5d0744d3992c3d65..35688003ec9b9975cbedd251d9b20cacf5879520
@@@ -26,7 -26,6 +26,7 @@@
  #include <stdint.h>
  #include <boost/shared_ptr.hpp>
  #include <boost/optional.hpp>
 +#include <boost/thread/mutex.hpp>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libpostproc/postprocess.h>
@@@ -35,7 -34,6 +35,7 @@@
  #include "decoder.h"
  #include "video_decoder.h"
  #include "audio_decoder.h"
 +#include "film.h"
  
  struct AVFilterGraph;
  struct AVCodecContext;
@@@ -58,7 -56,7 +58,7 @@@ public
                , _name (n)
                , _id (i)
        {}
-                 
        std::string to_string () const;
  
        std::string name () const {
@@@ -86,11 -84,12 +86,12 @@@ private
  class FFmpegDecoder : public VideoDecoder, public AudioDecoder
  {
  public:
 -      FFmpegDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const Options>, Job *);
 +      FFmpegDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *);
        ~FFmpegDecoder ();
  
        float frames_per_second () const;
        Size native_size () const;
+       SourceFrame length () const;
        int time_base_numerator () const;
        int time_base_denominator () const;
        int sample_aspect_ratio_numerator () const;
        void set_audio_stream (boost::shared_ptr<AudioStream>);
        void set_subtitle_stream (boost::shared_ptr<SubtitleStream>);
  
 +      bool seek (SourceFrame);
 +
  private:
  
        bool pass ();
        AVSampleFormat audio_sample_format () const;
        int bytes_per_audio_sample () const;
  
 +      void out_with_sync ();
        void filter_and_emit_video (AVFrame *);
  
        void setup_general ();
        void maybe_add_subtitle ();
        boost::shared_ptr<AudioBuffers> deinterleave_audio (uint8_t* data, int size);
  
 +      void film_changed (Film::Property);
 +      boost::signals2::scoped_connection _film_connection;
 +
        std::string stream_name (AVStream* s) const;
  
        AVFormatContext* _format_context;
        boost::optional<double> _first_audio;
  
        std::list<boost::shared_ptr<FilterGraph> > _filter_graphs;
 +      boost::mutex _filter_graphs_mutex;
  };
diff --combined src/lib/film.cc
index e7f47c462c550c4bb8bc9fedb97cad1871bdd1ed,e2a4cbeda60efda1089f2b8e840c6a64940f5702..4cfe7de0abce543e9b7c71e9743c422bf8542338
@@@ -31,6 -31,7 +31,6 @@@
  #include <boost/date_time.hpp>
  #include "film.h"
  #include "format.h"
 -#include "imagemagick_encoder.h"
  #include "job.h"
  #include "filter.h"
  #include "transcoder.h"
@@@ -85,6 -86,7 +85,7 @@@ int const Film::state_version = 1
  
  Film::Film (string d, bool must_exist)
        : _use_dci_name (true)
+       , _trust_content_header (true)
        , _dcp_content_type (0)
        , _format (0)
        , _scaler (Scaler::from_id ("bicubic"))
@@@ -143,6 -145,7 +144,7 @@@ Film::Film (Film const & o
        , _name              (o._name)
        , _use_dci_name      (o._use_dci_name)
        , _content           (o._content)
+       , _trust_content_header (o._trust_content_header)
        , _dcp_content_type  (o._dcp_content_type)
        , _format            (o._format)
        , _crop              (o._crop)
        , _studio            (o._studio)
        , _facility          (o._facility)
        , _package_type      (o._package_type)
 -      , _thumbs            (o._thumbs)
        , _size              (o._size)
        , _length            (o._length)
        , _content_digest    (o._content_digest)
@@@ -258,37 -262,35 +260,37 @@@ Film::make_dcp (bool transcode
                throw MissingSettingError ("name");
        }
  
 -      shared_ptr<Options> o (new Options (j2k_dir(), ".j2c", dir ("wavs")));
 -      o->out_size = format()->dcp_size ();
 -      o->padding = format()->dcp_padding (shared_from_this ());
 -      o->ratio = format()->ratio_as_float (shared_from_this ());
 +      shared_ptr<EncodeOptions> oe (new EncodeOptions (j2k_dir(), ".j2c", dir ("wavs")));
 +      oe->out_size = format()->dcp_size ();
 +      oe->padding = format()->dcp_padding (shared_from_this ());
        if (dcp_length ()) {
 -              o->video_decode_range = make_pair (dcp_trim_start(), dcp_trim_start() + dcp_length().get());
 +              oe->video_range = make_pair (dcp_trim_start(), dcp_trim_start() + dcp_length().get());
                if (audio_stream()) {
 -                      o->audio_decode_range = make_pair (
 -                              video_frames_to_audio_frames (o->video_decode_range.get().first, audio_stream()->sample_rate(), frames_per_second()),
 -                              video_frames_to_audio_frames (o->video_decode_range.get().second, audio_stream()->sample_rate(), frames_per_second())
 +                      oe->audio_range = make_pair (
 +                              video_frames_to_audio_frames (oe->video_range.get().first, audio_stream()->sample_rate(), frames_per_second()),
 +                              video_frames_to_audio_frames (oe->video_range.get().second, audio_stream()->sample_rate(), frames_per_second())
                                );
                }
                        
        }
 -      o->decode_subtitles = with_subtitles ();
 -      o->decode_video_skip = dcp_frame_rate (frames_per_second()).skip;
 +      
 +      oe->video_skip = dcp_frame_rate (frames_per_second()).skip;
 +
 +      shared_ptr<DecodeOptions> od (new DecodeOptions);
 +      od->decode_subtitles = with_subtitles ();
  
        shared_ptr<Job> r;
  
        if (transcode) {
                if (dcp_ab()) {
 -                      r = JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (shared_from_this(), o, shared_ptr<Job> ())));
 +                      r = JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (shared_from_this(), od, oe, shared_ptr<Job> ())));
                } else {
 -                      r = JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this(), o, shared_ptr<Job> ())));
 +                      r = JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this(), od, oe, shared_ptr<Job> ())));
                }
        }
  
 -      r = JobManager::instance()->add (shared_ptr<Job> (new CheckHashesJob (shared_from_this(), o, r)));
 -      JobManager::instance()->add (shared_ptr<Job> (new MakeDCPJob (shared_from_this(), o, r)));
 +      r = JobManager::instance()->add (shared_ptr<Job> (new CheckHashesJob (shared_from_this(), od, oe, r)));
 +      JobManager::instance()->add (shared_ptr<Job> (new MakeDCPJob (shared_from_this(), oe, r)));
  }
  
  /** Start a job to examine our content file */
@@@ -299,6 -301,12 +301,6 @@@ Film::examine_content (
                return;
        }
  
 -      set_thumbs (vector<SourceFrame> ());
 -      boost::filesystem::remove_all (dir ("thumbs"));
 -
 -      /* This call will recreate the directory */
 -      dir ("thumbs");
 -      
        _examine_content_job.reset (new ExamineContentJob (shared_from_this(), shared_ptr<Job> ()));
        _examine_content_job->Finished.connect (bind (&Film::examine_content_finished, this));
        JobManager::instance()->add (_examine_content_job);
@@@ -349,6 -357,35 +351,6 @@@ Film::encoded_frames () cons
        return N;
  }
  
 -/** Return the filename of a subtitle image if one exists for a given thumb index.
 - *  @param Thumbnail index.
 - *  @return Position of the image within the source frame, and the image filename, if one exists.
 - *  Otherwise the filename will be empty.
 - */
 -pair<Position, string>
 -Film::thumb_subtitle (int n) const
 -{
 -      string sub_file = thumb_base(n) + ".sub";
 -      if (!boost::filesystem::exists (sub_file)) {
 -              return pair<Position, string> ();
 -      }
 -
 -      pair<Position, string> sub;
 -      
 -      ifstream f (sub_file.c_str ());
 -      multimap<string, string> kv = read_key_value (f);
 -      for (map<string, string>::const_iterator i = kv.begin(); i != kv.end(); ++i) {
 -              if (i->first == "x") {
 -                      sub.first.x = lexical_cast<int> (i->second);
 -              } else if (i->first == "y") {
 -                      sub.first.y = lexical_cast<int> (i->second);
 -                      sub.second = String::compose ("%1.sub.png", thumb_base(n));
 -              }
 -      }
 -      
 -      return sub;
 -}
 -
  /** Write state to our `metadata' file */
  void
  Film::write_metadata () const
        f << "name " << _name << "\n";
        f << "use_dci_name " << _use_dci_name << "\n";
        f << "content " << _content << "\n";
+       f << "trust_content_header " << (_trust_content_header ? "1" : "0") << "\n";
        if (_dcp_content_type) {
                f << "dcp_content_type " << _dcp_content_type->pretty_name () << "\n";
        }
        f << "facility " << _facility << "\n";
        f << "package_type " << _package_type << "\n";
  
 -      /* Cached stuff; this is information about our content; we could
 -         look it up each time, but that's slow.
 -      */
 -      for (vector<SourceFrame>::const_iterator i = _thumbs.begin(); i != _thumbs.end(); ++i) {
 -              f << "thumb " << *i << "\n";
 -      }
        f << "width " << _size.width << "\n";
        f << "height " << _size.height << "\n";
        f << "length " << _length.get_value_or(0) << "\n";
@@@ -437,6 -481,7 +440,6 @@@ Film::read_metadata (
        boost::mutex::scoped_lock lm (_state_mutex);
  
        _external_audio.clear ();
 -      _thumbs.clear ();
        _content_audio_streams.clear ();
        _subtitle_streams.clear ();
  
                        _use_dci_name = (v == "1");
                } else if (k == "content") {
                        _content = v;
+               } else if (k == "trust_content_header") {
+                       _trust_content_header = (v == "1");
                } else if (k == "dcp_content_type") {
                        _dcp_content_type = DCPContentType::from_pretty_name (v);
                } else if (k == "format") {
                }
                
                /* Cached stuff */
 -              if (k == "thumb") {
 -                      int const n = atoi (v.c_str ());
 -                      /* Only add it to the list if it still exists */
 -                      if (boost::filesystem::exists (thumb_file_for_frame (n))) {
 -                              _thumbs.push_back (n);
 -                      }
 -              } else if (k == "width") {
 +              if (k == "width") {
                        _size.width = atoi (v.c_str ());
                } else if (k == "height") {
                        _size.height = atoi (v.c_str ());
        _dirty = false;
  }
  
 -/** @param n A thumb index.
 - *  @return The path to the thumb's image file.
 - */
 -string
 -Film::thumb_file (int n) const
 -{
 -      return thumb_file_for_frame (thumb_frame (n));
 -}
 -
 -/** @param n A frame index within the Film's source.
 - *  @return The path to the thumb's image file for this frame;
 - *  we assume that it exists.
 - */
 -string
 -Film::thumb_file_for_frame (SourceFrame n) const
 -{
 -      return thumb_base_for_frame(n) + ".png";
 -}
 -
 -/** @param n Thumb index.
 - *  Must not be called with the _state_mutex locked.
 - */
 -string
 -Film::thumb_base (int n) const
 -{
 -      return thumb_base_for_frame (thumb_frame (n));
 -}
 -
 -string
 -Film::thumb_base_for_frame (SourceFrame n) const
 -{
 -      stringstream s;
 -      s.width (8);
 -      s << setfill('0') << n;
 -      
 -      boost::filesystem::path p;
 -      p /= dir ("thumbs");
 -      p /= s.str ();
 -              
 -      return p.string ();
 -}
 -
 -/** @param n A thumb index.
 - *  @return The frame within the Film's source that it is for.
 - *
 - *  Must not be called with the _state_mutex locked.
 - */
 -SourceFrame
 -Film::thumb_frame (int n) const
 -{
 -      boost::mutex::scoped_lock lm (_state_mutex);
 -      assert (n < int (_thumbs.size ()));
 -      return _thumbs[n];
 -}
 -
  Size
  Film::cropped_size (Size s) const
  {
@@@ -846,21 -954,23 +851,21 @@@ Film::set_content (string c
        */
  
        try {
 -              shared_ptr<Options> o (new Options ("", "", ""));
 -              o->out_size = Size (1024, 1024);
 -              
 -              pair<shared_ptr<VideoDecoder>, shared_ptr<AudioDecoder> > d = decoder_factory (shared_from_this(), o, 0);
 +              shared_ptr<DecodeOptions> o (new DecodeOptions);
 +              Decoders d = decoder_factory (shared_from_this(), o, 0);
                
 -              set_size (d.first->native_size ());
 -              set_frames_per_second (d.first->frames_per_second ());
 -              set_subtitle_streams (d.first->subtitle_streams ());
 -              set_content_audio_streams (d.second->audio_streams ());
 +              set_size (d.video->native_size ());
 +              set_frames_per_second (d.video->frames_per_second ());
 +              set_subtitle_streams (d.video->subtitle_streams ());
 +              set_content_audio_streams (d.audio->audio_streams ());
  
                /* Start off with the first audio and subtitle streams */
 -              if (!d.second->audio_streams().empty()) {
 -                      set_content_audio_stream (d.second->audio_streams().front());
 +              if (!d.audio->audio_streams().empty()) {
 +                      set_content_audio_stream (d.audio->audio_streams().front());
                }
                
 -              if (!d.first->subtitle_streams().empty()) {
 -                      set_subtitle_stream (d.first->subtitle_streams().front());
 +              if (!d.video->subtitle_streams().empty()) {
 +                      set_subtitle_stream (d.video->subtitle_streams().front());
                }
                
                {
                signal_changed (CONTENT);
                
                set_content_digest (md5_digest (content_path ()));
-               
                examine_content ();
  
        } catch (...) {
  
        }
  }
 -      if (!_trust_content_header) && !content().empty()) {
+ void
+ Film::set_trust_content_header (bool t)
+ {
+       {
+               boost::mutex::scoped_lock lm (_state_mutex);
+               _trust_content_header = t;
+       }
+       
+       signal_changed (TRUST_CONTENT_HEADER);
++      if (!_trust_content_header && !content().empty()) {
+               /* We just said that we don't trust the content's header */
+               examine_content ();
+       }
+ }
               
  void
  Film::set_dcp_content_type (DCPContentType const * t)
@@@ -1038,7 -1164,8 +1059,7 @@@ Film::set_external_audio (vector<string
                _external_audio = a;
        }
  
 -      shared_ptr<Options> o (new Options ("", "", ""));
 -      o->decode_audio = true;
 +      shared_ptr<DecodeOptions> o (new DecodeOptions);
        shared_ptr<ExternalAudioDecoder> decoder (new ExternalAudioDecoder (shared_from_this(), o, 0));
        if (decoder->audio_stream()) {
                _external_audio_stream = decoder->audio_stream ();
@@@ -1198,6 -1325,16 +1219,6 @@@ Film::set_package_type (string p
        signal_changed (DCI_METADATA);
  }
  
 -void
 -Film::set_thumbs (vector<SourceFrame> t)
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              _thumbs = t;
 -      }
 -      signal_changed (THUMBS);
 -}
 -
  void
  Film::set_size (Size s)
  {
diff --combined src/lib/film.h
index c7ebdef12d66d7bf3f199fd49e149efd969f7483,8cd55a227994dd011040d64f3df19cb3ffca0ea3..642f2d7da9918d56fbf7774b4d3d7c482a5f0a2a
@@@ -61,6 -61,7 +61,6 @@@ public
  
        std::string j2k_dir () const;
        std::vector<std::string> audio_files () const;
 -      std::pair<Position, std::string> thumb_subtitle (int) const;
  
        void examine_content ();
        void send_dcp_to_tms ();
        std::string content_path () const;
        ContentType content_type () const;
        
 -      std::string thumb_file (int) const;
 -      std::string thumb_base (int) const;
 -      SourceFrame thumb_frame (int) const;
 -
        int target_audio_sample_rate () const;
        
        void write_metadata () const;
                NAME,
                USE_DCI_NAME,
                CONTENT,
+               TRUST_CONTENT_HEADER,
                DCP_CONTENT_TYPE,
                FORMAT,
                CROP,
                SUBTITLE_OFFSET,
                SUBTITLE_SCALE,
                DCI_METADATA,
 -              THUMBS,
                SIZE,
                LENGTH,
                CONTENT_AUDIO_STREAMS,
                return _content;
        }
  
+       bool trust_content_header () const {
+               boost::mutex::scoped_lock lm (_state_mutex);
+               return _trust_content_header;
+       }
        DCPContentType const * dcp_content_type () const {
                boost::mutex::scoped_lock lm (_state_mutex);
                return _dcp_content_type;
                return _package_type;
        }
  
 -      std::vector<SourceFrame> thumbs () const {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              return _thumbs;
 -      }
 -      
        Size size () const {
                boost::mutex::scoped_lock lm (_state_mutex);
                return _size;
        void set_name (std::string);
        void set_use_dci_name (bool);
        void set_content (std::string);
+       void set_trust_content_header (bool);
        void set_dcp_content_type (DCPContentType const *);
        void set_format (Format const *);
        void set_crop (Crop);
        void set_studio (std::string);
        void set_facility (std::string);
        void set_package_type (std::string);
 -      void set_thumbs (std::vector<SourceFrame>);
        void set_size (Size);
        void set_length (SourceFrame);
        void unset_length ();
@@@ -372,6 -391,8 +379,6 @@@ private
        /** The date that we should use in a DCI name */
        boost::gregorian::date _dci_date;
  
 -      std::string thumb_file_for_frame (SourceFrame) const;
 -      std::string thumb_base_for_frame (SourceFrame) const;
        void signal_changed (Property);
        void examine_content_finished ();
  
         *  or an absolute path.
         */
        std::string _content;
+       bool _trust_content_header;
        /** The type of content that this Film represents (feature, trailer etc.) */
        DCPContentType const * _dcp_content_type;
        /** The format to present this Film in (flat, scope, etc.) */
  
        /* Data which are cached to speed things up */
  
 -      /** Vector of frame indices for each of our `thumbnails' */
 -      std::vector<SourceFrame> _thumbs;
        /** Size, in pixels, of the source (ignoring cropping) */
        Size _size;
        /** Actual length of the source (in video frames) from examining it */
index 75107ef4febea666c1c7697f539edc2f543bfcf3,de49c1b566b6ef0a36ae2bb44c0e05d77a65da6c..cfcf4b4f6d130d8b2ea2cfeedf0362f59bda4daa
@@@ -26,7 -26,7 +26,7 @@@ namespace Magick 
  class ImageMagickDecoder : public VideoDecoder
  {
  public:
 -      ImageMagickDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const Options>, Job *);
 +      ImageMagickDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *);
  
        float frames_per_second () const {
                /* We don't know */
  
        Size native_size () const;
  
+       SourceFrame length () const {
+               /* We don't know */
+               return 0;
+       }
        int audio_channels () const {
                return 0;
        }
diff --combined src/lib/transcoder.cc
index f44a3ed7be30de313b459e2e242a13c66c7ed566,a7e79b05f74d6338cd2c5debfe6d77381b905839..87a1fb3f28c8435587e7601b11de13cde3b08a30
@@@ -44,11 -44,11 +44,11 @@@ using boost::dynamic_pointer_cast
  
  /** Construct a transcoder using a Decoder that we create and a supplied Encoder.
   *  @param f Film that we are transcoding.
 - *  @param o Options.
 + *  @param o Decode options.
   *  @param j Job that we are running under, or 0.
   *  @param e Encoder to use.
   */
 -Transcoder::Transcoder (shared_ptr<Film> f, shared_ptr<const Options> o, Job* j, shared_ptr<Encoder> e)
 +Transcoder::Transcoder (shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j, shared_ptr<Encoder> e)
        : _job (j)
        , _encoder (e)
        , _decoders (decoder_factory (f, o, j))
        }
  
        /* Set up the decoder to use the film's set streams */
 -      _decoders.first->set_subtitle_stream (f->subtitle_stream ());
 -      if (_decoders.second) {
 -              _decoders.second->set_audio_stream (f->audio_stream ());
 +      _decoders.video->set_subtitle_stream (f->subtitle_stream ());
-       _decoders.audio->set_audio_stream (f->audio_stream ());
++      if (_decoders.audio) {
++              _decoders.audio->set_audio_stream (f->audio_stream ());
+       }
  
        if (_matcher) {
 -              _decoders.first->connect_video (_matcher);
 +              _decoders.video->connect_video (_matcher);
                _matcher->connect_video (_encoder);
        } else {
 -              _decoders.first->connect_video (_encoder);
 +              _decoders.video->connect_video (_encoder);
        }
        
-       if (_matcher && _delay_line) {
 -      if (_matcher && _delay_line && _decoders.second) {
 -              _decoders.second->connect_audio (_delay_line);
++      if (_matcher && _delay_line && _decoders.audio) {
 +              _decoders.audio->connect_audio (_delay_line);
                _delay_line->connect_audio (_matcher);
                _matcher->connect_audio (_gain);
                _gain->connect_audio (_encoder);
@@@ -93,12 -95,12 +95,12 @@@ Transcoder::go (
                
                while (1) {
                        if (!done[0]) {
 -                              done[0] = _decoders.first->pass ();
 -                              _decoders.first->set_progress ();
 +                              done[0] = _decoders.video->pass ();
 +                              _decoders.video->set_progress ();
                        }
  
-                       if (!done[1] && dynamic_pointer_cast<Decoder> (_decoders.audio) != dynamic_pointer_cast<Decoder> (_decoders.video)) {
 -                      if (!done[1] && _decoders.second && dynamic_pointer_cast<Decoder> (_decoders.second) != dynamic_pointer_cast<Decoder> (_decoders.first)) {
 -                              done[1] = _decoders.second->pass ();
++                      if (!done[1] && _decoders.audio && dynamic_pointer_cast<Decoder> (_decoders.audio) != dynamic_pointer_cast<Decoder> (_decoders.video)) {
 +                              done[1] = _decoders.audio->pass ();
                        } else {
                                done[1] = true;
                        }
diff --combined src/lib/transcoder.h
index f27984aaa53f5632ecd3547695f345bdbaa2eddb,4a9667b3c5165b95aa9f8aeea2786b206f184b56..b50113742369c817aa3e0b57d28158f31d111b41
@@@ -24,8 -24,6 +24,8 @@@
   *  as a parameter to the constructor.
   */
  
 +#include "decoder_factory.h"
 +
  class Film;
  class Job;
  class Encoder;
@@@ -36,8 -34,7 +36,8 @@@ class Gain
  class VideoDecoder;
  class AudioDecoder;
  class DelayLine;
 -class Options;
 +class EncodeOptions;
 +class DecodeOptions;
  
  /** @class Transcoder
   *  @brief A class which takes a FilmState and some Options, then uses those to transcode a Film.
  class Transcoder
  {
  public:
 -      Transcoder (boost::shared_ptr<Film> f, boost::shared_ptr<const Options> o, Job* j, boost::shared_ptr<Encoder> e);
 +      Transcoder (
 +              boost::shared_ptr<Film> f,
 +              boost::shared_ptr<const DecodeOptions> o,
 +              Job* j,
 +              boost::shared_ptr<Encoder> e
 +              );
  
        void go ();
  
 -              return _decoders.first;
+       boost::shared_ptr<VideoDecoder> video_decoder () const {
++              return _decoders.video;
+       }
  protected:
        /** A Job that is running this Transcoder, or 0 */
        Job* _job;
        /** The encoder that we will use */
        boost::shared_ptr<Encoder> _encoder;
        /** The decoders that we will use */
 -      std::pair<boost::shared_ptr<VideoDecoder>, boost::shared_ptr<AudioDecoder> > _decoders;
 +      Decoders _decoders;
        boost::shared_ptr<Matcher> _matcher;
        boost::shared_ptr<DelayLine> _delay_line;
        boost::shared_ptr<Gain> _gain;
diff --combined src/lib/video_decoder.h
index 41e876e0a58c59cf4af2e62fe69429cda0d83736,685138a58c761d05307a71adb5b5dfdb74fc9889..f682941d19f8832b1b0fb7a2c98cc6f048d73260
  class VideoDecoder : public VideoSource, public virtual Decoder
  {
  public:
 -      VideoDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const Options>, Job *);
 +      VideoDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *);
  
        /** @return video frames per second, or 0 if unknown */
        virtual float frames_per_second () const = 0;
        /** @return native size in pixels */
        virtual Size native_size () const = 0;
+       /** @return length (in source video frames), according to our content's header */
+       virtual SourceFrame length () const = 0;
  
        virtual int time_base_numerator () const = 0;
        virtual int time_base_denominator () const = 0;
                return _subtitle_streams;
        }
  
 +      SourceFrame last_source_frame () const {
 +              return _last_source_frame;
 +      }
 +
  protected:
        
        virtual PixelFormat pixel_format () const = 0;
  
 -      void emit_video (boost::shared_ptr<Image>);
 +      void emit_video (boost::shared_ptr<Image>, SourceFrame);
        void emit_subtitle (boost::shared_ptr<TimedSubtitle>);
        void repeat_last_video ();
  
@@@ -76,8 -74,7 +78,8 @@@ private
        void signal_video (boost::shared_ptr<Image>, boost::shared_ptr<Subtitle>);
  
        SourceFrame _video_frame;
 -
 +      SourceFrame _last_source_frame;
 +      
        boost::shared_ptr<TimedSubtitle> _timed_subtitle;
  
        boost::shared_ptr<Image> _last_image;
diff --combined src/wx/film_editor.cc
index 7083290c536281bc8f15e2f34f3d658452b0e8d2,1e592e268d5093422688ab53fdd49c31cff3b479..d33f89ce6774e07bae1fd150f0d32e2139b4ab79
@@@ -114,6 -114,11 +114,11 @@@ FilmEditor::make_film_panel (
        _content = new wxFilePickerCtrl (_film_panel, wxID_ANY, wxT (""), wxT ("Select Content File"), wxT("*.*"));
        _film_sizer->Add (_content, 1, wxEXPAND);
  
+       _trust_content_header = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Trust content's header"));
+       video_control (_trust_content_header);
+       _film_sizer->Add (_trust_content_header, 1);
+       _film_sizer->AddSpacer (0);
        add_label_to_sizer (_film_sizer, _film_panel, "Content Type");
        _dcp_content_type = new wxComboBox (_film_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY);
        _film_sizer->Add (_dcp_content_type);
@@@ -174,6 -179,7 +179,7 @@@ FilmEditor::connect_to_widgets (
        _edit_dci_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::edit_dci_button_clicked), 0, this);
        _format->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::format_changed), 0, this);
        _content->Connect (wxID_ANY, wxEVT_COMMAND_FILEPICKER_CHANGED, wxCommandEventHandler (FilmEditor::content_changed), 0, this);
+       _trust_content_header->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::trust_content_header_changed), 0, this);
        _left_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::left_crop_changed), 0, this);
        _right_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::right_crop_changed), 0, this);
        _top_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::top_crop_changed), 0, this);
@@@ -427,6 -433,16 +433,16 @@@ FilmEditor::content_changed (wxCommandE
        }
  }
  
+ void
+ FilmEditor::trust_content_header_changed (wxCommandEvent &)
+ {
+       if (!_film) {
+               return;
+       }
+       _film->set_trust_content_header (_trust_content_header->GetValue ());
+ }
  /** Called when the DCP A/B switch has been toggled */
  void
  FilmEditor::dcp_ab_toggled (wxCommandEvent &)
@@@ -495,6 -511,9 +511,9 @@@ FilmEditor::film_changed (Film::Propert
                setup_subtitle_control_sensitivity ();
                setup_streams ();
                break;
+       case Film::TRUST_CONTENT_HEADER:
+               checked_set (_trust_content_header, _film->trust_content_header ());
+               break;
        case Film::SUBTITLE_STREAMS:
                setup_subtitle_control_sensitivity ();
                setup_streams ();
                checked_set (_dcp_content_type, DCPContentType::as_index (_film->dcp_content_type ()));
                _dcp_name->SetLabel (std_to_wx (_film->dcp_name ()));
                break;
 -      case Film::THUMBS:
 -              break;
        case Film::DCP_AB:
                checked_set (_dcp_ab, _film->dcp_ab ());
                break;
@@@ -692,6 -713,7 +711,7 @@@ FilmEditor::set_film (shared_ptr<Film> 
        film_changed (Film::NAME);
        film_changed (Film::USE_DCI_NAME);
        film_changed (Film::CONTENT);
+       film_changed (Film::TRUST_CONTENT_HEADER);
        film_changed (Film::DCP_CONTENT_TYPE);
        film_changed (Film::FORMAT);
        film_changed (Film::CROP);
@@@ -729,6 -751,7 +749,7 @@@ FilmEditor::set_things_sensitive (bool 
        _edit_dci_button->Enable (s);
        _format->Enable (s);
        _content->Enable (s);
+       _trust_content_header->Enable (s);
        _left_crop->Enable (s);
        _right_crop->Enable (s);
        _top_crop->Enable (s);
diff --combined src/wx/film_viewer.cc
index 1cf45fd4e47a568b22ae3af5c94aceb6a069535f,a821323586dd2cce1c5e29b275aac541667bca7f..8312fa5e5527bcded5dea1373fb245b47e41ffb0
  */
  
  /** @file  src/film_viewer.cc
 - *  @brief A wx widget to view `thumbnails' of a Film.
 + *  @brief A wx widget to view a preview of a Film.
   */
  
  #include <iostream>
  #include <iomanip>
 +#include <wx/tglbtn.h>
  #include "lib/film.h"
  #include "lib/format.h"
  #include "lib/util.h"
  #include "lib/job_manager.h"
  #include "lib/options.h"
  #include "lib/subtitle.h"
 +#include "lib/image.h"
 +#include "lib/scaler.h"
  #include "film_viewer.h"
  #include "wx_util.h"
 +#include "video_decoder.h"
  
  using std::string;
  using std::pair;
  using std::max;
 +using std::cout;
  using boost::shared_ptr;
  
 -class ThumbPanel : public wxPanel
 +FilmViewer::FilmViewer (shared_ptr<Film> f, wxWindow* p)
 +      : wxPanel (p)
 +      , _panel (new wxPanel (this))
 +      , _slider (new wxSlider (this, wxID_ANY, 0, 0, 4096))
 +      , _play_button (new wxToggleButton (this, wxID_ANY, wxT ("Play")))
 +      , _out_width (0)
 +      , _out_height (0)
 +      , _panel_width (0)
 +      , _panel_height (0)
  {
 -public:
 -      ThumbPanel (wxPanel* parent, shared_ptr<Film> film)
 -              : wxPanel (parent)
 -              , _film (film)
 -              , _index (0)
 -              , _frame_rebuild_needed (false)
 -              , _composition_needed (false)
 -      {}
 -
 -      /** Handle a paint event */
 -      void paint_event (wxPaintEvent& ev)
 -      {
 -              if (!_film || _film->thumbs().size() == 0) {
 -                      wxPaintDC dc (this);
 -                      return;
 -              }
 -
 -              if (_frame_rebuild_needed) {
 -                      _image.reset (new wxImage (std_to_wx (_film->thumb_file (_index))));
 -
 -                      _subtitle.reset ();
 -                      pair<Position, string> s = _film->thumb_subtitle (_index);
 -                      if (!s.second.empty ()) {
 -                              _subtitle.reset (new SubtitleView (s.first, std_to_wx (s.second)));
 -                      }
 -
 -                      _frame_rebuild_needed = false;
 -                      compose ();
 -              }
 +      wxBoxSizer* v_sizer = new wxBoxSizer (wxVERTICAL);
 +      SetSizer (v_sizer);
  
 -              if (_composition_needed) {
 -                      compose ();
 -              }
 +      v_sizer->Add (_panel, 1, wxEXPAND);
  
 -              wxPaintDC dc (this);
 -              if (_bitmap) {
 -                      dc.DrawBitmap (*_bitmap, 0, 0, false);
 -              }
 +      wxBoxSizer* h_sizer = new wxBoxSizer (wxHORIZONTAL);
 +      h_sizer->Add (_play_button, 0, wxEXPAND);
 +      h_sizer->Add (_slider, 1, wxEXPAND);
  
 -              if (_film->with_subtitles() && _subtitle) {
 -                      dc.DrawBitmap (*_subtitle->bitmap, _subtitle->transformed_area.x, _subtitle->transformed_area.y, true);
 -              }
 -      }
 +      v_sizer->Add (h_sizer, 0, wxEXPAND);
  
 -      /** Handle a size event */
 -      void size_event (wxSizeEvent &)
 -      {
 -              if (!_image) {
 -                      return;
 -              }
 +      _panel->Bind (wxEVT_PAINT, &FilmViewer::paint_panel, this);
 +      _panel->Bind (wxEVT_SIZE, &FilmViewer::panel_sized, this);
 +      _slider->Bind (wxEVT_SCROLL_THUMBTRACK, &FilmViewer::slider_moved, this);
 +      _slider->Bind (wxEVT_SCROLL_PAGEUP, &FilmViewer::slider_moved, this);
 +      _slider->Bind (wxEVT_SCROLL_PAGEDOWN, &FilmViewer::slider_moved, this);
 +      _play_button->Bind (wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, &FilmViewer::play_clicked, this);
 +      _timer.Bind (wxEVT_TIMER, &FilmViewer::timer, this);
  
 -              recompose ();
 -      }
 +      set_film (_film);
 +}
  
 -      /** @param n Thumbnail index */
 -      void set (int n)
 +void
 +FilmViewer::film_changed (Film::Property p)
 +{
 +      switch (p) {
 +      case Film::FORMAT:
 +              calculate_sizes ();
 +              update_from_raw ();
 +              break;
 +      case Film::CONTENT:
        {
 -              _index = n;
 -              _frame_rebuild_needed = true;
 -              Refresh ();
 +              shared_ptr<DecodeOptions> o (new DecodeOptions);
 +              o->decode_audio = false;
 +              o->video_sync = false;
 +              _decoders = decoder_factory (_film, o, 0);
 +              _decoders.video->Video.connect (bind (&FilmViewer::process_video, this, _1, _2));
 +              _decoders.video->OutputChanged.connect (boost::bind (&FilmViewer::decoder_changed, this));
 +              break;
        }
 -
 -      void set_film (shared_ptr<Film> f)
 -      {
 -              _film = f;
 -              if (!_film) {
 -                      clear ();
 -                      _frame_rebuild_needed = true;
 -                      Refresh ();
 -              } else {
 -                      _frame_rebuild_needed = true;
 -                      Refresh ();
 -              }
 +      default:
 +              break;
        }
 +}
  
 -      /** Clear our thumbnail image */
 -      void clear ()
 -      {
 -              _bitmap.reset ();
 -              _image.reset ();
 -              _subtitle.reset ();
 +void
 +FilmViewer::set_film (shared_ptr<Film> f)
 +{
 +      if (_film == f) {
 +              return;
        }
 +      
 +      _film = f;
  
 -      void recompose ()
 -      {
 -              _composition_needed = true;
 -              Refresh ();
 +      if (!_film) {
 +              return;
        }
  
 -      DECLARE_EVENT_TABLE ();
 -
 -private:
 +      _film->Changed.connect (boost::bind (&FilmViewer::film_changed, this, _1));
  
 -      void compose ()
 -      {
 -              _composition_needed = false;
 -              
 -              if (!_film || !_image) {
 -                      return;
 -              }
 -
 -              /* Size of the view */
 -              int vw, vh;
 -              GetSize (&vw, &vh);
 -
 -              Crop const fc = _film->crop ();
 -
 -              /* Cropped rectangle */
 -              Rect cropped_area (
 -                      fc.left,
 -                      fc.top,
 -                      _image->GetWidth() - (fc.left + fc.right),
 -                      _image->GetHeight() - (fc.top + fc.bottom)
 -                      );
 -
 -              /* Target ratio */
 -              float const target = _film->format() ? _film->format()->ratio_as_float (_film) : 1.78;
 -
 -              _transformed_image = _image->GetSubImage (wxRect (cropped_area.x, cropped_area.y, cropped_area.width, cropped_area.height));
 -
 -              float x_scale = 1;
 -              float y_scale = 1;
 -
 -              if ((float (vw) / vh) > target) {
 -                      /* view is longer (horizontally) than the ratio; fit height */
 -                      _transformed_image.Rescale (vh * target, vh, wxIMAGE_QUALITY_HIGH);
 -                      x_scale = vh * target / cropped_area.width;
 -                      y_scale = float (vh) / cropped_area.height;
 -              } else {
 -                      /* view is shorter (horizontally) than the ratio; fit width */
 -                      _transformed_image.Rescale (vw, vw / target, wxIMAGE_QUALITY_HIGH);
 -                      x_scale = float (vw) / cropped_area.width;
 -                      y_scale = (vw / target) / cropped_area.height;
 -              }
 +      film_changed (Film::CONTENT);
 +      film_changed (Film::CROP);
 +      film_changed (Film::FORMAT);
 +}
  
 -              _bitmap.reset (new wxBitmap (_transformed_image));
 +void
 +FilmViewer::decoder_changed ()
 +{
 +      seek_and_update (_decoders.video->last_source_frame ());
 +}
  
 -              if (_subtitle) {
 +void
 +FilmViewer::timer (wxTimerEvent& ev)
 +{
 +      _panel->Refresh ();
 +      _panel->Update ();
  
 -                      _subtitle->transformed_area = subtitle_transformed_area (
 -                              x_scale, y_scale, _subtitle->base_area, _film->subtitle_offset(), _film->subtitle_scale()
 -                              );
 +      shared_ptr<Image> last = _display;
 +      while (last == _display) {
 +              _decoders.video->pass ();
 +      }
  
 -                      _subtitle->transformed_image = _subtitle->base_image;
 -                      _subtitle->transformed_image.Rescale (_subtitle->transformed_area.width, _subtitle->transformed_area.height, wxIMAGE_QUALITY_HIGH);
 -                      _subtitle->transformed_area.x -= rint (_film->crop().left * x_scale);
 -                      _subtitle->transformed_area.y -= rint (_film->crop().top * y_scale);
 -                      _subtitle->bitmap.reset (new wxBitmap (_subtitle->transformed_image));
 +      if (_film->length()) {
 +              int const new_slider_position = 4096 * _decoders.video->last_source_frame() / _film->length().get();
 +              if (new_slider_position != _slider->GetValue()) {
 +                      _slider->SetValue (new_slider_position);
                }
        }
 +}
  
 -      shared_ptr<Film> _film;
 -      shared_ptr<wxImage> _image;
 -      wxImage _transformed_image;
 -      /** currently-displayed thumbnail index */
 -      int _index;
 -      shared_ptr<wxBitmap> _bitmap;
 -      bool _frame_rebuild_needed;
 -      bool _composition_needed;
  
 -      struct SubtitleView
 -      {
 -              SubtitleView (Position p, wxString const & i)
 -                      : base_image (i)
 -              {
 -                      base_area.x = p.x;
 -                      base_area.y = p.y;
 -                      base_area.width = base_image.GetWidth ();
 -                      base_area.height = base_image.GetHeight ();
 -              }
 +void
 +FilmViewer::paint_panel (wxPaintEvent& ev)
 +{
 +      wxPaintDC dc (_panel);
 +      if (!_display) {
 +              return;
 +      }
  
 -              Rect base_area;
 -              Rect transformed_area;
 -              wxImage base_image;
 -              wxImage transformed_image;
 -              shared_ptr<wxBitmap> bitmap;
 -      };
 +      wxImage i (_out_width, _out_height, _display->data()[0], true);
 +      wxBitmap b (i);
 +      dc.DrawBitmap (b, 0, 0);
 +}
  
 -      shared_ptr<SubtitleView> _subtitle;
 -};
  
 -BEGIN_EVENT_TABLE (ThumbPanel, wxPanel)
 -EVT_PAINT (ThumbPanel::paint_event)
 -EVT_SIZE (ThumbPanel::size_event)
 -END_EVENT_TABLE ()
 +void
 +FilmViewer::slider_moved (wxCommandEvent& ev)
 +{
 +      if (_film->length()) {
 +              seek_and_update (_slider->GetValue() * _film->length().get() / 4096);
 +      }
 +}
  
 -FilmViewer::FilmViewer (shared_ptr<Film> f, wxWindow* p)
 -      : wxPanel (p)
 +void
 +FilmViewer::seek_and_update (SourceFrame f)
  {
-       _decoders.video->seek (f);
 -      _sizer = new wxBoxSizer (wxVERTICAL);
 -      SetSizer (_sizer);
++      if (_decoders.video->seek (f)) {
++              return;
++      }
        
 -      _thumb_panel = new ThumbPanel (this, f);
 -      _sizer->Add (_thumb_panel, 1, wxEXPAND);
 -
 -      int const m = max ((size_t) 1, f ? f->thumbs().size() - 1 : 0);
 -      _slider = new wxSlider (this, wxID_ANY, 0, 0, m);
 -      _sizer->Add (_slider, 0, wxEXPAND | wxLEFT | wxRIGHT);
 -      set_thumbnail (0);
 -
 -      _slider->Connect (wxID_ANY, wxEVT_COMMAND_SLIDER_UPDATED, wxCommandEventHandler (FilmViewer::slider_changed), 0, this);
 +      shared_ptr<Image> last = _display;
 +      while (last == _display) {
 +              _decoders.video->pass ();
 +      }
 +      _panel->Refresh ();
 +      _panel->Update ();
 +}
  
 -      set_film (_film);
 +void
 +FilmViewer::panel_sized (wxSizeEvent& ev)
 +{
 +      _panel_width = ev.GetSize().GetWidth();
 +      _panel_height = ev.GetSize().GetHeight();
 +      calculate_sizes ();
 +      update_from_raw ();
  }
  
  void
 -FilmViewer::set_thumbnail (int n)
 +FilmViewer::update_from_raw ()
  {
 -      if (_film == 0 || int (_film->thumbs().size()) <= n) {
 +      if (!_raw) {
                return;
        }
  
 -      _thumb_panel->set (n);
 +      if (_out_width && _out_height) {
 +              _display = _raw->scale_and_convert_to_rgb (Size (_out_width, _out_height), 0, Scaler::from_id ("bicubic"));
 +      }
 +      
 +      _panel->Refresh ();
 +      _panel->Update ();
  }
  
  void
 -FilmViewer::slider_changed (wxCommandEvent &)
 +FilmViewer::calculate_sizes ()
  {
 -      set_thumbnail (_slider->GetValue ());
 +      float const panel_ratio = static_cast<float> (_panel_width) / _panel_height;
 +      float const film_ratio = _film->format() ? _film->format()->ratio_as_float(_film) : 1.78;
 +      if (panel_ratio < film_ratio) {
 +              /* panel is less widscreen than the film; clamp width */
 +              _out_width = _panel_width;
 +              _out_height = _out_width / film_ratio;
 +      } else {
 +              /* panel is more widescreen than the film; clamp heignt */
 +              _out_height = _panel_height;
 +              _out_width = _out_height * film_ratio;
 +      }
  }
  
  void
 -FilmViewer::film_changed (Film::Property p)
 +FilmViewer::play_clicked (wxCommandEvent &)
  {
 -      ensure_ui_thread ();
 -      
 -      switch (p) {
 -      case Film::THUMBS:
 -              if (_film && _film->thumbs().size() > 1) {
 -                      _slider->SetRange (0, _film->thumbs().size() - 1);
 -              } else {
 -                      _thumb_panel->clear ();
 -                      _slider->SetRange (0, 1);
 -              }
 -              
 -              _slider->SetValue (0);
 -              set_thumbnail (0);
 -              break;
 -      case Film::CONTENT:
 -              setup_visibility ();
 -              break;
 -      case Film::CROP:
 -      case Film::FORMAT:
 -      case Film::WITH_SUBTITLES:
 -      case Film::SUBTITLE_OFFSET:
 -      case Film::SUBTITLE_SCALE:
 -              _thumb_panel->recompose ();
 -              break;
 -      default:
 -              break;
 -      }
 +      check_play_state ();
  }
  
  void
 -FilmViewer::set_film (shared_ptr<Film> f)
 +FilmViewer::check_play_state ()
  {
 -      if (_film == f) {
 -              return;
 +      if (_play_button->GetValue()) {
 +              _timer.Start (1000 / _film->frames_per_second());
 +      } else {
 +              _timer.Stop ();
        }
 -      
 -      _film = f;
 -      _thumb_panel->set_film (_film);
 -
 -      if (!_film) {
 -              return;
 -      }
 -
 -      _film->Changed.connect (bind (&FilmViewer::film_changed, this, _1));
 -      film_changed (Film::CROP);
 -      film_changed (Film::THUMBS);
 -      setup_visibility ();
  }
  
  void
 -FilmViewer::setup_visibility ()
 +FilmViewer::process_video (shared_ptr<Image> image, shared_ptr<Subtitle> sub)
  {
 -      if (!_film) {
 -              return;
 +      _raw = image;
 +      if (_out_width && _out_height) {
 +              _display = _raw->scale_and_convert_to_rgb (Size (_out_width, _out_height), 0, Scaler::from_id ("bicubic"));
        }
 -
 -      ContentType const c = _film->content_type ();
 -      _slider->Show (c == VIDEO);
  }