Merge 1.0-seek and subtitle-content.
authorCarl Hetherington <cth@carlh.net>
Wed, 15 Jan 2014 21:23:33 +0000 (21:23 +0000)
committerCarl Hetherington <cth@carlh.net>
Wed, 15 Jan 2014 21:23:33 +0000 (21:23 +0000)
20 files changed:
1  2 
src/lib/decoded.h
src/lib/ffmpeg_decoder.cc
src/lib/player.cc
src/lib/player.h
src/lib/subrip.cc
src/lib/subrip.h
src/lib/subrip_content.cc
src/lib/subrip_content.h
src/lib/subrip_decoder.cc
src/lib/subrip_decoder.h
src/lib/subrip_subtitle.h
src/lib/subtitle_decoder.cc
src/lib/subtitle_decoder.h
src/lib/util.cc
src/wx/film_editor.cc
test/ratio_test.cc
test/subrip_test.cc
test/test.cc
test/test.h
test/wscript

index 0000000000000000000000000000000000000000,ff32e43f2b67a115673a53268b229618e0815750..f4ebe0dbd18a5ed7236372c352861bf5cca28888
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,118 +1,145 @@@
 -class DecodedSubtitle : public Decoded
+ /*
+     Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
+     This program is free software; you can redistribute it and/or modify
+     it under the terms of the GNU General Public License as published by
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ #ifndef DCPOMATIC_LIB_DECODED_H
+ #define DCPOMATIC_LIB_DECODED_H
++#include <libdcp/subtitle_asset.h>
+ #include "types.h"
+ #include "rect.h"
+ #include "util.h"
+ class Image;
+ class Decoded
+ {
+ public:
+       Decoded ()
+               : dcp_time (0)
+       {}
+       virtual ~Decoded () {}
+       virtual void set_dcp_times (VideoFrame, AudioFrame, FrameRateChange, DCPTime) = 0;
+       DCPTime dcp_time;
+ };
+ /** One frame of video from a VideoDecoder */
+ class DecodedVideo : public Decoded
+ {
+ public:
+       DecodedVideo ()
+               : eyes (EYES_BOTH)
+               , same (false)
+               , frame (0)
+       {}
+       DecodedVideo (boost::shared_ptr<const Image> im, Eyes e, bool s, VideoFrame f)
+               : image (im)
+               , eyes (e)
+               , same (s)
+               , frame (f)
+       {}
+       void set_dcp_times (VideoFrame video_frame_rate, AudioFrame, FrameRateChange frc, DCPTime offset)
+       {
+               dcp_time = frame * TIME_HZ * frc.factor() / video_frame_rate + offset;
+       }
+       
+       boost::shared_ptr<const Image> image;
+       Eyes eyes;
+       bool same;
+       VideoFrame frame;
+ };
+ class DecodedAudio : public Decoded
+ {
+ public:
+       DecodedAudio (boost::shared_ptr<const AudioBuffers> d, AudioFrame f)
+               : data (d)
+               , frame (f)
+       {}
+       void set_dcp_times (VideoFrame, AudioFrame audio_frame_rate, FrameRateChange, DCPTime offset)
+       {
+               dcp_time = frame * TIME_HZ / audio_frame_rate + offset;
+       }
+       
+       boost::shared_ptr<const AudioBuffers> data;
+       AudioFrame frame;
+ };
 -      DecodedSubtitle ()
++class DecodedImageSubtitle : public Decoded
+ {
+ public:
 -      DecodedSubtitle (boost::shared_ptr<Image> im, dcpomatic::Rect<double> r, ContentTime f, ContentTime t)
++      DecodedImageSubtitle ()
+               : content_time (0)
+               , content_time_to (0)
+               , dcp_time_to (0)
+       {}
++      DecodedImageSubtitle (boost::shared_ptr<Image> im, dcpomatic::Rect<double> r, ContentTime f, ContentTime t)
+               : image (im)
+               , rect (r)
+               , content_time (f)
+               , content_time_to (t)
+               , dcp_time_to (0)
+       {}
+       void set_dcp_times (VideoFrame, AudioFrame, FrameRateChange frc, DCPTime offset)
+       {
+               dcp_time = rint (content_time / frc.speed_up) + offset;
+               dcp_time_to = rint (content_time_to / frc.speed_up) + offset;
+       }
+       boost::shared_ptr<Image> image;
+       dcpomatic::Rect<double> rect;
+       ContentTime content_time;
+       ContentTime content_time_to;
+       DCPTime dcp_time_to;
+ };
++class DecodedTextSubtitle : public Decoded
++{
++public:
++      DecodedTextSubtitle ()
++              : dcp_time_to (0)
++      {}
++
++      DecodedTextSubtitle (std::list<libdcp::Subtitle> s)
++              : subs (s)
++      {}
++
++      void set_dcp_times (VideoFrame, AudioFrame, FrameRateChange frc, DCPTime offset)
++      {
++              if (subs.empty ()) {
++                      return;
++              }
++
++              /* Assuming that all subs are at the same time */
++              dcp_time = rint (subs.front().in().to_ticks() * 4 * TIME_HZ / frc.speed_up) + offset;
++              dcp_time_to = rint (subs.front().out().to_ticks() * 4 * TIME_HZ / frc.speed_up) + offset;
++      }
++
++      std::list<libdcp::Subtitle> subs;
++      DCPTime dcp_time_to;
++};
++
+ #endif
index a051bcd1283d9f120b51516e00fa24f013e65eb6,52afe2a271e79360cf8fea54257f3e6b239495c9..55f4eb5c69b63cc97c0c5ce6ba1bb44db619b0ff
@@@ -555,11 -559,16 +559,11 @@@ 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)
        */
-       double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _video_pts_offset;
 -<<<<<<< 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) + _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 faf783ef6e6be9f40e1b35d87a1d3d0d9022e556,77630f0e3c8e4e5d4f442351ae05e876d7d78f00..cb6d519842c5c6cfce1ac92d5f1fe83f74a04e0c
  #include "job.h"
  #include "image.h"
  #include "ratio.h"
- #include "resampler.h"
  #include "log.h"
  #include "scaler.h"
 +#include "render_subtitles.h"
  
  using std::list;
  using std::cout;
@@@ -186,60 -154,93 +157,98 @@@ Player::pass (
                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);
++      shared_ptr<DecodedImageSubtitle> dis = dynamic_pointer_cast<DecodedImageSubtitle> (earliest_decoded);
++      shared_ptr<DecodedTextSubtitle> dts = dynamic_pointer_cast<DecodedTextSubtitle> (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 ();
++      } else if (dis && _video) {
++              _image_subtitle.piece = earliest_piece;
++              _image_subtitle.subtitle = dis;
++              update_subtitle_from_image ();
++      } else if (dts && _video) {
++              _text_subtitle.piece = earliest_piece;
++              _text_subtitle.subtitle = dts;
++              update_subtitle_from_text ();
+       }
+       if (consume) {
+               earliest_piece->decoder->consume ();
+       }                       
+       
        return false;
  }
  
@@@ -456,28 -442,17 +450,43 @@@ Player::setup_pieces (
  
        for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
  
-               shared_ptr<Piece> piece (new Piece (*i));
+               shared_ptr<Decoder> decoder;
+               optional<FrameRateChange> frc;
  
-               /* XXX: into content? */
++              /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */
++              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(), (*i)->position()) - min (vc->end(), (*i)->end());
++                      if (overlap > best_overlap_t) {
++                              best_overlap = vc;
++                              best_overlap_t = overlap;
++                      }
++              }
 +
++              optional<FrameRateChange> best_overlap_frc;
++              if (best_overlap) {
++                      best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
++              } else {
++                      /* No video overlap; e.g. if the DCP is just audio */
++                      best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
++              }
++
++              /* FFmpeg */
                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->ImageSubtitle.connect (bind (&Player::process_image_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
-                       fd->TextSubtitle.connect (bind (&Player::process_text_subtitle, this, weak_ptr<Piece> (piece), _1));
-                       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());
                }
--              
++
++              /* ImageContent */
                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 (!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());
                }
  
++              /* SndfileContent */
                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));
-                       piece->decoder = sd;
+                       decoder.reset (new SndfileDecoder (_film, sc));
++                      frc = best_overlap_frc;
 +              }
  
 -                      /* 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;
 -                              }
 -                      }
 -
 -                      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 ());
 -                      }
++              /* SubRipContent */
 +              shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
 +              if (rc) {
-                       shared_ptr<SubRipDecoder> sd (new SubRipDecoder (_film, rc));
-                       sd->TextSubtitle.connect (bind (&Player::process_text_subtitle, this, weak_ptr<Piece> (piece), _1));
-                       piece->decoder = sd;
++                      decoder.reset (new SubRipDecoder (_film, rc));
++                      frc = best_overlap_frc;
                }
  
-               _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;
@@@ -640,36 -615,15 +632,15 @@@ Player::film_changed (Film::Property p
        }
  }
  
- void
- Player::process_image_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
- {
-       _image_subtitle.piece = weak_piece;
-       _image_subtitle.image = image;
-       _image_subtitle.rect = rect;
-       _image_subtitle.from = from;
-       _image_subtitle.to = to;
-       update_subtitle_from_image ();
- }
- void
- Player::process_text_subtitle (weak_ptr<Piece>, list<libdcp::Subtitle> s)
- {
-       _text_subtitles = s;
-       
-       update_subtitle_from_text ();
- }
- /** Update _out_subtitle from _image_subtitle */
  void
 -Player::update_subtitle ()
 +Player::update_subtitle_from_image ()
  {
 -      shared_ptr<Piece> piece = _in_subtitle.piece.lock ();
 +      shared_ptr<Piece> piece = _image_subtitle.piece.lock ();
        if (!piece) {
                return;
        }
  
-       if (!_image_subtitle.image) {
 -      if (!_in_subtitle.subtitle->image) {
++      if (!_image_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 = _image_subtitle.rect;
 -      dcpomatic::Rect<double> in_rect = _in_subtitle.subtitle->rect;
++      dcpomatic::Rect<double> in_rect = _image_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.subtitle->image->scale (
 +      
-       _out_subtitle.image = _image_subtitle.image->scale (
++      _out_subtitle.image = _image_subtitle.subtitle->image->scale (
                scaled_size,
                Scaler::from_id ("bicubic"),
-               _image_subtitle.image->pixel_format (),
 -              PIX_FMT_RGBA,
++              _image_subtitle.subtitle->image->pixel_format (),
                true
                );
--
 -<<<<<<< HEAD
 -      _out_subtitle.from = _in_subtitle.subtitle->dcp_time;
 -      _out_subtitle.to = _in_subtitle.subtitle->dcp_time_to;
 -=======
--      /* XXX: hack */
-       Time from = _image_subtitle.from;
-       Time to = _image_subtitle.to;
 -      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
++      _out_subtitle.from = _image_subtitle.subtitle->dcp_time;
++      _out_subtitle.to = _image_subtitle.subtitle->dcp_time_to;
  }
  
  /** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
@@@ -743,17 -698,13 +701,23 @@@ Player::repeat_last_video (
        return true;
  }
  
-       if (_text_subtitles.empty ()) {
 +void
 +Player::update_subtitle_from_text ()
 +{
-       render_subtitles (_text_subtitles, _video_container_size, _out_subtitle.image, _out_subtitle.position);
++      if (_text_subtitle.subtitle->subs.empty ()) {
 +              _out_subtitle.image.reset ();
 +              return;
 +      }
 +
 -
++      render_subtitles (_text_subtitle.subtitle->subs, _video_container_size, _out_subtitle.image, _out_subtitle.position);
 +}
 +
+ void
+ Player::set_approximate_size ()
+ {
+       _approximate_size = true;
+ }
+                             
  PlayerImage::PlayerImage (
        shared_ptr<const Image> in,
        Crop crop,
index 88d05ea6478b2691bffe722127db49039854b833,5f7c553a0f9f0bd44b2c6b49c628c59520d9381c..377e8bd18e5ae3634d4faa0848f19fbcee088082
@@@ -122,18 -146,15 +147,16 @@@ private
        void setup_pieces ();
        void playlist_changed ();
        void content_changed (boost::weak_ptr<Content>, int, bool);
-       void do_seek (Time, bool);
+       void do_seek (DCPTime, bool);
        void flush ();
        void emit_black ();
-       void emit_silence (OutputAudioFrame);
-       boost::shared_ptr<Resampler> resampler (boost::shared_ptr<AudioContent>, bool);
+       void emit_silence (AudioFrame);
        void film_changed (Film::Property);
 -      void update_subtitle ();
 +      void update_subtitle_from_image ();
 +      void update_subtitle_from_text ();
+       void emit_video (boost::weak_ptr<Piece>, boost::shared_ptr<DecodedVideo>);
+       void emit_audio (boost::weak_ptr<Piece>, boost::shared_ptr<DecodedAudio>);
+       void step_video_position (boost::shared_ptr<DecodedVideo>);
  
        boost::shared_ptr<const Film> _film;
        boost::shared_ptr<const Playlist> _playlist;
  
        struct {
                boost::weak_ptr<Piece> piece;
-               boost::shared_ptr<Image> image;
-               dcpomatic::Rect<double> rect;
-               Time from;
-               Time to;
 -              boost::shared_ptr<DecodedSubtitle> subtitle;
 -      } _in_subtitle;
++              boost::shared_ptr<DecodedImageSubtitle> subtitle;
 +      } _image_subtitle;
  
-       std::list<libdcp::Subtitle> _text_subtitles;
 +      struct {
-               boost::shared_ptr<Image> image;
++              boost::weak_ptr<Piece> piece;
++              boost::shared_ptr<DecodedTextSubtitle> subtitle;
++      } _text_subtitle;
++      
+       struct {
                Position<int> position;
-               Time from;
-               Time to;
+               boost::shared_ptr<Image> image;
+               DCPTime from;
+               DCPTime to;
        } _out_subtitle;
  
  #ifdef DCPOMATIC_DEBUG
index be2c6f666b269e9c9f0bd933093735d19cafecad,0000000000000000000000000000000000000000..380a2ce2cb7e7757c2e1fc7472f571d72aa0a693
mode 100644,000000..100644
--- /dev/null
@@@ -1,236 -1,0 +1,236 @@@
- Time
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include <boost/algorithm/string.hpp>
 +#include "subrip.h"
 +#include "subrip_content.h"
 +#include "subrip_subtitle.h"
 +#include "cross.h"
 +#include "exceptions.h"
 +
 +#include "i18n.h"
 +
 +using std::string;
 +using std::list;
 +using std::vector;
 +using std::cout;
 +using boost::shared_ptr;
 +using boost::lexical_cast;
 +using boost::algorithm::trim;
 +
 +SubRip::SubRip (shared_ptr<const SubRipContent> content)
 +{
 +      FILE* f = fopen_boost (content->path (0), "r");
 +      if (!f) {
 +              throw OpenFileError (content->path (0));
 +      }
 +
 +      enum {
 +              COUNTER,
 +              METADATA,
 +              CONTENT
 +      } state = COUNTER;
 +
 +      char buffer[256];
 +      int next_count = 1;
 +
 +      boost::optional<SubRipSubtitle> current;
 +      list<string> lines;
 +      
 +      while (!feof (f)) {
 +              fgets (buffer, sizeof (buffer), f);
 +              if (feof (f)) {
 +                      break;
 +              }
 +              
 +              string line (buffer);
 +              trim_right_if (line, boost::is_any_of ("\n\r"));
 +              
 +              switch (state) {
 +              case COUNTER:
 +              {
 +                      int x = 0;
 +                      try {
 +                              x = lexical_cast<int> (line);
 +                      } catch (...) {
 +
 +                      }
 +                      
 +                      if (x == next_count) {
 +                              state = METADATA;
 +                              ++next_count;
 +                              current = SubRipSubtitle ();
 +                      } else {
 +                              throw SubRipError (line, _("a subtitle count"), content->path (0));
 +                      }
 +              }
 +              break;
 +              case METADATA:
 +              {
 +                      vector<string> p;
 +                      boost::algorithm::split (p, line, boost::algorithm::is_any_of (" "));
 +                      if (p.size() != 3 && p.size() != 7) {
 +                              throw SubRipError (line, _("a time/position line"), content->path (0));
 +                      }
 +
 +                      current->from = convert_time (p[0]);
 +                      current->to = convert_time (p[2]);
 +
 +                      if (p.size() > 3) {
 +                              current->x1 = convert_coordinate (p[3]);
 +                              current->x2 = convert_coordinate (p[4]);
 +                              current->y1 = convert_coordinate (p[5]);
 +                              current->y2 = convert_coordinate (p[6]);
 +                      }
 +                      state = CONTENT;
 +                      break;
 +              }
 +              case CONTENT:
 +                      if (line.empty ()) {
 +                              state = COUNTER;
 +                              current->pieces = convert_content (lines);
 +                              _subtitles.push_back (current.get ());
 +                              current.reset ();
 +                              lines.clear ();
 +                      } else {
 +                              lines.push_back (line);
 +                      }
 +                      break;
 +              }
 +      }
 +
 +      if (state == CONTENT) {
 +              current->pieces = convert_content (lines);
 +              _subtitles.push_back (current.get ());
 +      }
 +
 +      fclose (f);
 +}
 +
-       Time r = 0;
++ContentTime
 +SubRip::convert_time (string t)
 +{
- Time
++      ContentTime r = 0;
 +
 +      vector<string> a;
 +      boost::algorithm::split (a, t, boost::is_any_of (":"));
 +      assert (a.size() == 3);
 +      r += lexical_cast<int> (a[0]) * 60 * 60 * TIME_HZ;
 +      r += lexical_cast<int> (a[1]) * 60 * TIME_HZ;
 +
 +      vector<string> b;
 +      boost::algorithm::split (b, a[2], boost::is_any_of (","));
 +      r += lexical_cast<int> (b[0]) * TIME_HZ;
 +      r += lexical_cast<int> (b[1]) * TIME_HZ / 1000;
 +
 +      return r;
 +}
 +
 +int
 +SubRip::convert_coordinate (string t)
 +{
 +      vector<string> a;
 +      boost::algorithm::split (a, t, boost::is_any_of (":"));
 +      assert (a.size() == 2);
 +      return lexical_cast<int> (a[1]);
 +}
 +
 +void
 +SubRip::maybe_content (list<SubRipSubtitlePiece>& pieces, SubRipSubtitlePiece& p)
 +{
 +      if (!p.text.empty ()) {
 +              pieces.push_back (p);
 +              p.text.clear ();
 +      }
 +}
 +
 +list<SubRipSubtitlePiece>
 +SubRip::convert_content (list<string> t)
 +{
 +      list<SubRipSubtitlePiece> pieces;
 +      
 +      SubRipSubtitlePiece p;
 +
 +      enum {
 +              TEXT,
 +              TAG
 +      } state = TEXT;
 +
 +      string tag;
 +
 +      /* XXX: missing <font> support */
 +      /* XXX: nesting of tags e.g. <b>foo<i>bar<b>baz</b>fred</i>jim</b> might
 +         not work, I think.
 +      */
 +
 +      for (list<string>::const_iterator i = t.begin(); i != t.end(); ++i) {
 +              for (size_t j = 0; j < i->size(); ++j) {
 +                      switch (state) {
 +                      case TEXT:
 +                              if ((*i)[j] == '<' || (*i)[j] == '{') {
 +                                      state = TAG;
 +                              } else {
 +                                      p.text += (*i)[j];
 +                              }
 +                              break;
 +                      case TAG:
 +                              if ((*i)[j] == '>' || (*i)[j] == '}') {
 +                                      if (tag == "b") {
 +                                              maybe_content (pieces, p);
 +                                              p.bold = true;
 +                                      } else if (tag == "/b") {
 +                                              maybe_content (pieces, p);
 +                                              p.bold = false;
 +                                      } else if (tag == "i") {
 +                                              maybe_content (pieces, p);
 +                                              p.italic = true;
 +                                      } else if (tag == "/i") {
 +                                              maybe_content (pieces, p);
 +                                              p.italic = false;
 +                                      } else if (tag == "u") {
 +                                              maybe_content (pieces, p);
 +                                              p.underline = true;
 +                                      } else if (tag == "/u") {
 +                                              maybe_content (pieces, p);
 +                                              p.underline = false;
 +                                      }
 +                                      tag.clear ();
 +                                      state = TEXT;
 +                              } else {
 +                                      tag += (*i)[j];
 +                              }
 +                              break;
 +                      }
 +              }
 +      }
 +
 +      maybe_content (pieces, p);
 +
 +      return pieces;
 +}
 +
++ContentTime
 +SubRip::length () const
 +{
 +      if (_subtitles.empty ()) {
 +              return 0;
 +      }
 +
 +      return _subtitles.back().to;
 +}
index 91bc7156b65b0a6b6c26e1288b32d5198872fb0e,0000000000000000000000000000000000000000..e7d21675f41e140e11777d750e64087855d4c680
mode 100644,000000..100644
--- /dev/null
@@@ -1,53 -1,0 +1,53 @@@
-       Time length () const;
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#ifndef DCPOMATIC_SUBRIP_H
 +#define DCPOMATIC_SUBRIP_H
 +
 +#include "subrip_subtitle.h"
 +
 +class SubRipContent;
 +class subrip_time_test;
 +class subrip_coordinate_test;
 +class subrip_content_test;
 +class subrip_parse_test;
 +
 +class SubRip
 +{
 +public:
 +      SubRip (boost::shared_ptr<const SubRipContent>);
 +
-       static Time convert_time (std::string);
++      ContentTime length () const;
 +
 +protected:
 +      std::vector<SubRipSubtitle> _subtitles;
 +      
 +private:
 +      friend class subrip_time_test;
 +      friend class subrip_coordinate_test;
 +      friend class subrip_content_test;
 +      friend class subrip_parse_test;
 +      
++      static ContentTime convert_time (std::string);
 +      static int convert_coordinate (std::string);
 +      static std::list<SubRipSubtitlePiece> convert_content (std::list<std::string>);
 +      static void maybe_content (std::list<SubRipSubtitlePiece> &, SubRipSubtitlePiece &);
 +};
 +
 +#endif
index b13f64dedc29d56fd7000f86ee4ed6f9e3ff3fda,0000000000000000000000000000000000000000..48d3528e14d881265dd93466b622038c207492a1
mode 100644,000000..100644
--- /dev/null
@@@ -1,99 -1,0 +1,99 @@@
- Time
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include "subrip_content.h"
 +#include "util.h"
 +#include "subrip.h"
 +
 +#include "i18n.h"
 +
 +using std::stringstream;
 +using std::string;
 +using boost::shared_ptr;
 +
 +SubRipContent::SubRipContent (shared_ptr<const Film> film, boost::filesystem::path path)
 +      : Content (film, path)
 +      , SubtitleContent (film, path)
 +{
 +
 +}
 +
 +SubRipContent::SubRipContent (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node, int)
 +      : Content (film, node)
 +      , SubtitleContent (film, node)
 +{
 +
 +}
 +
 +void
 +SubRipContent::examine (boost::shared_ptr<Job> job)
 +{
 +      Content::examine (job);
 +      SubRip s (shared_from_this ());
 +      boost::mutex::scoped_lock lm (_mutex);
 +      _length = s.length ();
 +}
 +
 +string
 +SubRipContent::summary () const
 +{
 +      return path_summary() + " " + _("[subtitles]");
 +}
 +
 +string
 +SubRipContent::technical_summary () const
 +{
 +      return Content::technical_summary() + " - " + _("SubRip subtitles");
 +}
 +
 +string
 +SubRipContent::information () const
 +{
 +      
 +}
 +      
 +void
 +SubRipContent::as_xml (xmlpp::Node* node)
 +{
 +      node->add_child("Type")->add_child_text ("SubRip");
 +      Content::as_xml (node);
 +      SubtitleContent::as_xml (node);
 +}
 +
++DCPTime
 +SubRipContent::full_length () const
 +{
 +      /* XXX: this assumes that the timing of the SubRip file is appropriate
 +         for the DCP's frame rate.
 +      */
 +      return _length;
 +}
 +
 +string
 +SubRipContent::identifier () const
 +{
 +      LocaleGuard lg;
 +
 +      stringstream s;
 +      s << Content::identifier()
 +        << "_" << subtitle_scale()
 +        << "_" << subtitle_offset();
 +
 +      return s.str ();
 +}
index 30a824eec1f920c7dc08acd2e2c39725e9f6acac,0000000000000000000000000000000000000000..6138c047ec47d3a2ef8a691076c25e667ed55a42
mode 100644,000000..100644
--- /dev/null
@@@ -1,42 -1,0 +1,42 @@@
-       Time full_length () const;
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include "subtitle_content.h"
 +
 +class SubRipContent : public SubtitleContent
 +{
 +public:
 +      SubRipContent (boost::shared_ptr<const Film>, boost::filesystem::path);
 +      SubRipContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int);
 +
 +      boost::shared_ptr<SubRipContent> shared_from_this () {
 +              return boost::dynamic_pointer_cast<SubRipContent> (Content::shared_from_this ());
 +      }
 +      
 +      void examine (boost::shared_ptr<Job>);
 +      std::string summary () const;
 +      std::string technical_summary () const;
 +      std::string information () const;
 +      void as_xml (xmlpp::Node *);
-       Time _length;
++      DCPTime full_length () const;
 +      std::string identifier () const;
 +
 +private:
++      DCPTime _length;
 +};
index deee82847e642778dfd26185836778170e1c9e8b,0000000000000000000000000000000000000000..aecee4e3e7aaceb4290dccf75aaa7a79674f2bc8
mode 100644,000000..100644
--- /dev/null
@@@ -1,66 -1,0 +1,65 @@@
- void
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include "subrip_decoder.h"
 +
 +using std::list;
 +using boost::shared_ptr;
 +
 +SubRipDecoder::SubRipDecoder (shared_ptr<const Film> film, shared_ptr<const SubRipContent> content)
 +      : Decoder (film)
 +      , SubtitleDecoder (film)
 +      , SubRip (content)
 +      , _next (0)
 +{
 +
 +}
 +
- }
- bool
- SubRipDecoder::done () const
- {
-       return _next == _subtitles.size ();
++bool
 +SubRipDecoder::pass ()
 +{
++      if (_next >= _subtitles.size ()) {
++              return true;
++      }
++      
 +      list<libdcp::Subtitle> out;
 +      for (list<SubRipSubtitlePiece>::const_iterator i = _subtitles[_next].pieces.begin(); i != _subtitles[_next].pieces.end(); ++i) {
 +              out.push_back (
 +                      libdcp::Subtitle (
 +                              "Arial",
 +                              i->italic,
 +                              libdcp::Color (255, 255, 255),
 +                              72,
 +                              _subtitles[_next].from,
 +                              _subtitles[_next].to,
 +                              0.9,
 +                              libdcp::BOTTOM,
 +                              i->text,
 +                              libdcp::NONE,
 +                              libdcp::Color (255, 255, 255),
 +                              0,
 +                              0
 +                              )
 +                      );
 +      }
 +
 +      text_subtitle (out);
 +      _next++;
++      return false;
 +}
index b963bfb35d9704d8f1798d9053832c839b4c064a,0000000000000000000000000000000000000000..26d5d501006c021ee7892667246b626e817a43f3
mode 100644,000000..100644
--- /dev/null
@@@ -1,40 -1,0 +1,39 @@@
-       void pass ();
-       bool done () const;
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#ifndef DCPOMATIC_SUBRIP_DECODER_H
 +#define DCPOMATIC_SUBRIP_DECODER_H
 +
 +#include "subtitle_decoder.h"
 +#include "subrip.h"
 +
 +class SubRipContent;
 +
 +class SubRipDecoder : public SubtitleDecoder, public SubRip
 +{
 +public:
 +      SubRipDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const SubRipContent>);
 +      
++      bool pass ();
 +
 +private:
 +      size_t _next;
 +};
 +
 +#endif
index 933e0fc021ab5fd8f1b4caeee91a8d75a35f5d12,0000000000000000000000000000000000000000..f730ee492242ca3696731ee4230191da0f837f4c
mode 100644,000000..100644
--- /dev/null
@@@ -1,58 -1,0 +1,58 @@@
-       Time from;
-       Time to;
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#ifndef DCPOMATIC_SUBRIP_SUBTITLE_H
 +#define DCPOMATIC_SUBRIP_SUBTITLE_H
 +
 +#include <boost/optional.hpp>
 +#include <libdcp/types.h>
 +#include "types.h"
 +
 +struct SubRipSubtitlePiece
 +{
 +      SubRipSubtitlePiece ()
 +              : bold (false)
 +              , italic (false)
 +              , underline (false)
 +      {}
 +      
 +      std::string text;
 +      bool bold;
 +      bool italic;
 +      bool underline;
 +      libdcp::Color color;
 +};
 +
 +struct SubRipSubtitle
 +{
 +      SubRipSubtitle ()
 +              : from (0)
 +              , to (0)
 +      {}
 +      
++      ContentTime from;
++      ContentTime to;
 +      boost::optional<int> x1;
 +      boost::optional<int> x2;
 +      boost::optional<int> y1;
 +      boost::optional<int> y2;
 +      std::list<SubRipSubtitlePiece> pieces;
 +};
 +
 +#endif
index 49c04501eb26903f317bf706bf840a95de37a01c,7ba969933d077717ed125546657767d8fcc1aeda..e5cadb7b48b4e513c3b11a7ad47babc10ecda689
@@@ -20,8 -20,8 +20,9 @@@
  #include <boost/shared_ptr.hpp>
  #include "subtitle_decoder.h"
  
 +using std::list;
  using boost::shared_ptr;
+ using boost::optional;
  
  SubtitleDecoder::SubtitleDecoder (shared_ptr<const Film> f)
        : Decoder (f)
   *  Image may be 0 to say that there is no current subtitle.
   */
  void
- SubtitleDecoder::image_subtitle (shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
 -SubtitleDecoder::subtitle (shared_ptr<Image> image, dcpomatic::Rect<double> rect, ContentTime from, ContentTime to)
++SubtitleDecoder::image_subtitle (shared_ptr<Image> image, dcpomatic::Rect<double> rect, ContentTime from, ContentTime to)
  {
-       ImageSubtitle (image, rect, from, to);
 -      _pending.push_back (shared_ptr<DecodedSubtitle> (new DecodedSubtitle (image, rect, from, to)));
++      _pending.push_back (shared_ptr<DecodedImageSubtitle> (new DecodedImageSubtitle (image, rect, from, to)));
 +}
 +
 +void
 +SubtitleDecoder::text_subtitle (list<libdcp::Subtitle> s)
 +{
-       TextSubtitle (s);
++      _pending.push_back (shared_ptr<DecodedTextSubtitle> (new DecodedTextSubtitle (s)));
  }
index 31b03b066c0b940a65b27ff16e7562d7552000f3,fd1d71f33fdef23b3ef8ed7b5d69140baa4ed2cb..82662d192fbd1295d8574d7001b44d413984c353
@@@ -35,12 -32,6 +36,9 @@@ class SubtitleDecoder : public virtual 
  public:
        SubtitleDecoder (boost::shared_ptr<const Film>);
  
-       boost::signals2::signal<void (boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time)> ImageSubtitle;
-       boost::signals2::signal<void (std::list<libdcp::Subtitle>)> TextSubtitle;
  protected:
-       void image_subtitle (boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time);
 -      void subtitle (boost::shared_ptr<Image>, dcpomatic::Rect<double>, ContentTime, ContentTime);
++      void image_subtitle (boost::shared_ptr<Image>, dcpomatic::Rect<double>, ContentTime, ContentTime);
 +      void text_subtitle (std::list<libdcp::Subtitle>);
  };
 +
 +#endif
diff --cc src/lib/util.cc
Simple merge
Simple merge
index 082a6ff11eed1b651b451b4bbb537ee2e84674bd,082a6ff11eed1b651b451b4bbb537ee2e84674bd..f3cbb504f0b874b9d5ef97b65d681b2e91ff33f3
@@@ -42,7 -42,7 +42,7 @@@ BOOST_AUTO_TEST_CASE (ratio_test
  
        Ratio const * r = Ratio::from_id ("119");
        BOOST_CHECK (r);
--      BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), libdcp::Size (2048, 1080)), libdcp::Size (1285, 1080));
++      BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), libdcp::Size (2048, 1080)), libdcp::Size (1290, 1080));
  
        r = Ratio::from_id ("133");
        BOOST_CHECK (r);
index 0dbf6d3104321716724975905e8ea2ef818800c1,0000000000000000000000000000000000000000..12a77c1e1eeae27b6faf6dae56ccd13ab738c2f6
mode 100644,000000..100644
--- /dev/null
@@@ -1,205 -1,0 +1,196 @@@
- static list<libdcp::Subtitle> subtitles;
- static void
- process_subtitle (list<libdcp::Subtitle> s)
- {
-       subtitles = s;
- }
 +/*
 +    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
 +
 +    This program is free software; you can redistribute it and/or modify
 +    it under the terms of the GNU General Public License as published by
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include <boost/test/unit_test.hpp>
 +#include <libdcp/subtitle_asset.h>
 +#include "lib/subrip.h"
 +#include "lib/subrip_content.h"
 +#include "lib/subrip_decoder.h"
 +#include "lib/render_subtitles.h"
 +#include "test.h"
 +
 +using std::list;
 +using std::vector;
 +using std::string;
 +using boost::shared_ptr;
++using boost::dynamic_pointer_cast;
 +
 +/** Test SubRip::convert_time */
 +BOOST_AUTO_TEST_CASE (subrip_time_test)
 +{
 +      BOOST_CHECK_EQUAL (SubRip::convert_time ("00:03:10,500"), rint (((3 * 60) + 10 + 0.5) * TIME_HZ));
 +      BOOST_CHECK_EQUAL (SubRip::convert_time ("04:19:51,782"), rint (((4 * 3600) + (19 * 60) + 51 + 0.782) * TIME_HZ));
 +}
 +
 +/** Test SubRip::convert_coordinate */
 +BOOST_AUTO_TEST_CASE (subrip_coordinate_test)
 +{
 +      BOOST_CHECK_EQUAL (SubRip::convert_coordinate ("foo:42"), 42);
 +      BOOST_CHECK_EQUAL (SubRip::convert_coordinate ("X1:999"), 999);
 +}
 +
 +/** Test SubRip::convert_content */
 +BOOST_AUTO_TEST_CASE (subrip_content_test)
 +{
 +      list<string> c;
 +      list<SubRipSubtitlePiece> p;
 +      
 +      c.push_back ("Hello world");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 1);
 +      BOOST_CHECK_EQUAL (p.front().text, "Hello world");
 +      c.clear ();
 +
 +      c.push_back ("<b>Hello world</b>");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 1);
 +      BOOST_CHECK_EQUAL (p.front().text, "Hello world");
 +      BOOST_CHECK_EQUAL (p.front().bold, true);
 +      c.clear ();
 +
 +      c.push_back ("<i>Hello world</i>");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 1);
 +      BOOST_CHECK_EQUAL (p.front().text, "Hello world");
 +      BOOST_CHECK_EQUAL (p.front().italic, true);
 +      c.clear ();
 +
 +      c.push_back ("<u>Hello world</u>");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 1);
 +      BOOST_CHECK_EQUAL (p.front().text, "Hello world");
 +      BOOST_CHECK_EQUAL (p.front().underline, true);
 +      c.clear ();
 +
 +      c.push_back ("{b}Hello world{/b}");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 1);
 +      BOOST_CHECK_EQUAL (p.front().text, "Hello world");
 +      BOOST_CHECK_EQUAL (p.front().bold, true);
 +      c.clear ();
 +
 +      c.push_back ("{i}Hello world{/i}");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 1);
 +      BOOST_CHECK_EQUAL (p.front().text, "Hello world");
 +      BOOST_CHECK_EQUAL (p.front().italic, true);
 +      c.clear ();
 +
 +      c.push_back ("{u}Hello world{/u}");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 1);
 +      BOOST_CHECK_EQUAL (p.front().text, "Hello world");
 +      BOOST_CHECK_EQUAL (p.front().underline, true);
 +      c.clear ();
 +
 +      c.push_back ("<b>This is <i>nesting</i> of subtitles</b>");
 +      p = SubRip::convert_content (c);
 +      BOOST_CHECK_EQUAL (p.size(), 3);
 +      list<SubRipSubtitlePiece>::iterator i = p.begin ();     
 +      BOOST_CHECK_EQUAL (i->text, "This is ");
 +      BOOST_CHECK_EQUAL (i->bold, true);
 +      BOOST_CHECK_EQUAL (i->italic, false);
 +      ++i;
 +      BOOST_CHECK_EQUAL (i->text, "nesting");
 +      BOOST_CHECK_EQUAL (i->bold, true);
 +      BOOST_CHECK_EQUAL (i->italic, true);
 +      ++i;
 +      BOOST_CHECK_EQUAL (i->text, " of subtitles");
 +      BOOST_CHECK_EQUAL (i->bold, true);
 +      BOOST_CHECK_EQUAL (i->italic, false);
 +      ++i;
 +      c.clear ();
 +}
 +
 +/** Test parsing of full SubRip file content */
 +BOOST_AUTO_TEST_CASE (subrip_parse_test)
 +{
 +      shared_ptr<SubRipContent> content (new SubRipContent (shared_ptr<Film> (), "test/data/subrip.srt"));
 +      content->examine (shared_ptr<Job> ());
 +      BOOST_CHECK_EQUAL (content->full_length(), ((3 * 60) + 56.471) * TIME_HZ);
 +
 +      SubRip s (content);
 +
 +      vector<SubRipSubtitle>::const_iterator i = s._subtitles.begin();
 +
 +      BOOST_CHECK (i != s._subtitles.end ());
 +      BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 49.200) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 52.351) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->pieces.size(), 1);
 +      BOOST_CHECK_EQUAL (i->pieces.front().text, "This is a subtitle, and it goes over two lines.");
 +
 +      ++i;
 +      BOOST_CHECK (i != s._subtitles.end ());
 +      BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 52.440) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 54.351) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->pieces.size(), 1);
 +      BOOST_CHECK_EQUAL (i->pieces.front().text, "We have emboldened this");
 +      BOOST_CHECK_EQUAL (i->pieces.front().bold, true);
 +
 +      ++i;
 +      BOOST_CHECK (i != s._subtitles.end ());
 +      BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 54.440) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 56.590) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->pieces.size(), 1);
 +      BOOST_CHECK_EQUAL (i->pieces.front().text, "And italicised this.");
 +      BOOST_CHECK_EQUAL (i->pieces.front().italic, true);
 +
 +      ++i;
 +      BOOST_CHECK (i != s._subtitles.end ());
 +      BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 56.680) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 58.955) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->pieces.size(), 1);
 +      BOOST_CHECK_EQUAL (i->pieces.front().text, "Shall I compare thee to a summers' day?");
 +
 +      ++i;
 +      BOOST_CHECK (i != s._subtitles.end ());
 +      BOOST_CHECK_EQUAL (i->from, ((2 * 60) + 0.840) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->to, ((2 * 60) + 3.400) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->pieces.size(), 1);
 +      BOOST_CHECK_EQUAL (i->pieces.front().text, "Is this a dagger I see before me?");
 +
 +      ++i;
 +      BOOST_CHECK (i != s._subtitles.end ());
 +      BOOST_CHECK_EQUAL (i->from, ((3 * 60) + 54.560) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->to, ((3 * 60) + 56.471) * TIME_HZ);
 +      BOOST_CHECK_EQUAL (i->pieces.size(), 1);
 +      BOOST_CHECK_EQUAL (i->pieces.front().text, "Hello world.");
 +
 +      ++i;
 +      BOOST_CHECK (i == s._subtitles.end ());
 +}
 +
-       decoder->TextSubtitle.connect (boost::bind (&process_subtitle, _1));
-       decoder->pass ();
 +/** Test rendering of a SubRip subtitle */
 +BOOST_AUTO_TEST_CASE (subrip_render_test)
 +{
 +      shared_ptr<SubRipContent> content (new SubRipContent (shared_ptr<Film> (), "test/data/subrip.srt"));
 +      content->examine (shared_ptr<Job> ());
 +      BOOST_CHECK_EQUAL (content->full_length(), ((3 * 60) + 56.471) * TIME_HZ);
 +
 +      shared_ptr<Film> film = new_test_film ("subrip_render_test");
 +
 +      shared_ptr<SubRipDecoder> decoder (new SubRipDecoder (film, content));
-       render_subtitles (subtitles, libdcp::Size (1998, 1080), image, position);
++      shared_ptr<DecodedTextSubtitle> dts = dynamic_pointer_cast<DecodedTextSubtitle> (decoder->peek ());
 +
 +      shared_ptr<Image> image;
 +      Position<int> position;
++      render_subtitles (dts->subs, libdcp::Size (1998, 1080), image, position);
 +      write_image (image, "build/test/subrip_render_test.png");
 +      check_file ("build/test/subrip_render_test.png", "test/data/subrip_render_test.png");
 +}
diff --cc test/test.cc
index e76c98d8bcdfb357f19c5746a611346115189a49,c0f732c46d22c9f558e6c897772d3833cf51e39f..f9d761da305dd150e858a48c794710e4615c49a5
@@@ -96,11 -95,11 +97,11 @@@ new_test_film (string name
        return f;
  }
  
 -static void
 +void
- check_file (string ref, string check)
+ check_file (boost::filesystem::path ref, boost::filesystem::path check)
  {
        uintmax_t N = boost::filesystem::file_size (ref);
-       BOOST_CHECK_EQUAL (N, boost::filesystem::file_size(check));
+       BOOST_CHECK_EQUAL (N, boost::filesystem::file_size (check));
        FILE* ref_file = fopen (ref.c_str(), "rb");
        BOOST_CHECK (ref_file);
        FILE* check_file = fopen (check.c_str(), "rb");
diff --cc test/test.h
index 8b302b1199794bf7c2b06e5556e091b0c2629681,c582966655ddc6c6ed4b6f8e2ceb0cefb14f240f..b6347a5cad354f338b5c2a23ca67ccd8b46e4340
@@@ -24,8 -23,6 +24,8 @@@ class Image
  
  extern void wait_for_jobs ();
  extern boost::shared_ptr<Film> new_test_film (std::string);
- extern void check_dcp (std::string, std::string);
- extern void check_file (std::string ref, std::string check);
+ extern void check_dcp (boost::filesystem::path, boost::filesystem::path);
++extern void check_file (boost::filesystem::path ref, boost::filesystem::path check);
  extern void check_xml (boost::filesystem::path, boost::filesystem::path, std::list<std::string>);
  extern boost::filesystem::path test_film_dir (std::string);
 +extern void write_image (boost::shared_ptr<const Image> image, boost::filesystem::path file);
diff --cc test/wscript
index 834491e194eca5b6507e12badc03ad12f7ec3573,a35a6cbf573b8605ea072ce19b10c20156c04472..de9e9f25a41a8c66f4e22783f59cf3092112a328
@@@ -36,11 -37,13 +37,14 @@@ def build(bld)
                   pixel_formats_test.cc
                   play_test.cc
                   ratio_test.cc
+                  repeat_frame_test.cc
                   resampler_test.cc
                   scaling_test.cc
+                  seek_zero_test.cc
                   silence_padding_test.cc
+                  skip_frame_test.cc
                   stream_test.cc
 +                 subrip_test.cc
                   test.cc
                   threed_test.cc
                   util_test.cc