Various work on better seeking (and seeking of audio).
authorCarl Hetherington <cth@carlh.net>
Wed, 11 Dec 2013 10:31:18 +0000 (10:31 +0000)
committerCarl Hetherington <cth@carlh.net>
Wed, 11 Dec 2013 10:31:18 +0000 (10:31 +0000)
29 files changed:
doc/design/resampling.tex
src/lib/audio_content.cc
src/lib/audio_content.h
src/lib/audio_merger.h
src/lib/decoder.h
src/lib/ffmpeg.cc
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_decoder.h
src/lib/film.cc
src/lib/film.h
src/lib/image_content.cc
src/lib/image_decoder.cc
src/lib/image_decoder.h
src/lib/player.cc
src/lib/playlist.cc
src/lib/playlist.h
src/lib/sndfile_decoder.cc
src/lib/sndfile_decoder.h
src/lib/util.cc
src/lib/util.h
src/lib/video_content.cc
src/lib/video_decoder.h
src/wx/video_panel.cc
test/ffmpeg_seek_test.cc [new file with mode: 0644]
test/frame_rate_test.cc
test/long_ffmpeg_seek_test.cc [new file with mode: 0644]
test/test.cc
test/wscript

index 44aeee9b1b4c8abcdea917a7aeeebe66620c7815..cf9cfb1edc220bcdfa6f242b4aeee94427c2a0d5 100644 (file)
@@ -1,4 +1,5 @@
 \documentclass{article}
+\usepackage{amsmath}
 \begin{document}
 
 Here is what resampling we need to do.  Content video is at $C_V$ fps, audio at $C_A$.  
@@ -18,6 +19,7 @@ $C_V$ is a DCI rate, $C_A$ is not.  e.g.\ if $C_V = 24$, $C_A = 44.1\times{}10^3
 \textbf{Resample $C_A$ to the DCI rate.}
 
 \section{Hard case 1}
+\label{sec:hard1}
 
 $C_V$ is not a DCI rate, $C_A$ is, e.g.\ if $C_V = 25$, $C_A =
 48\times{}10^3$.  We will run the video at a nearby DCI rate $F_V$,
@@ -31,5 +33,24 @@ resample audio to $25 * 48\times{}10^3 / 24 = 50\times{}10^3$.
 \medskip
 \textbf{Resample $C_A$ to $C_V C_A / F_V$}
 
+\section{Hard case 2}
+
+Neither $C_V$ nor $C_A$ is not a DCI rate, e.g.\ if $C_V = 25$, $C_A =
+44.1\times{}10^3$.  We will run the video at a nearby DCI rate $F_V$,
+meaning that it will run faster or slower than it should.  We first
+resample the audio to a DCI rate $F_A$, then perform as with
+Section~\ref{sec:hard1} above.
+
+\medskip
+\textbf{Resample $C_A$ to $C_V F_A / F_V$}
+
+
+\section{The general case}
+
+Given a DCP running at $F_V$ and $F_A$ and a piece of content at $C_V$
+and $C_A$, resample the audio to $R_A$ where
+\begin{align*}
+R_A &= \frac{C_V F_A}{F_V}
+\end{align*}
 
 \end{document}
index 97372b962a7285368d2317d1d3be5c769d313eb4..0c458668118f91659e4261674a174e6fe2089183 100644 (file)
@@ -148,3 +148,27 @@ AudioContent::technical_summary () const
 {
        return String::compose ("audio: channels %1, length %2, raw rate %3, out rate %4", audio_channels(), audio_length(), content_audio_frame_rate(), output_audio_frame_rate());
 }
+
+/** Note: this is not particularly fast, as the FrameRateChange lookup
+ *  is not very intelligent.
+ *
+ *  @param t Some duration to convert.
+ *  @param at The time within the DCP to get the active frame rate change from; i.e. a point at which
+ *  the `controlling' video content is active.
+ */
+AudioContent::Frame
+AudioContent::time_to_content_audio_frames (Time t, Time at) const
+{
+       shared_ptr<const Film> film = _film.lock ();
+       assert (film);
+       
+       /* Consider the case where we're running a 25fps video at 24fps (i.e. slow)
+          Our audio is at 44.1kHz.  We will resample it to 48000 * 25 / 24 and then
+          run it at 48kHz (i.e. slow, to match).
+
+          After 1 second, we'll have run the equivalent of 44.1kHz * 24 / 25 samples
+          in the source.
+       */
+       
+       return rint (t * content_audio_frame_rate() * film->active_frame_rate_change(at).speed_up / TIME_HZ);
+}
index ca4a1f2348fea78b788632a38a6da5a058c091f1..10114e10d606e1f13f9920f83898ff59f51d50af 100644 (file)
@@ -74,6 +74,8 @@ public:
                return _audio_delay;
        }
 
+       Frame time_to_content_audio_frames (Time, Time) const;
+       
 private:
        /** Gain to apply to audio in dB */
        float _audio_gain;
index 226601e0ec61dffc92726bd8f98bbd976f21e9ad..6ad33fb37c898ad267083854fad35ecd3fcf961a 100644 (file)
@@ -97,9 +97,16 @@ public:
                if (_buffers->frames() == 0) {
                        return TimedAudioBuffers<T> ();
                }
-               
+
                return TimedAudioBuffers<T> (_buffers, _last_pull);
        }
+
+       void
+       clear (Time t)
+       {
+               _last_pull = t;
+               _buffers.reset (new AudioBuffers (_buffers->channels(), 0));
+       }
        
 private:
        boost::shared_ptr<AudioBuffers> _buffers;
index d67592ed812544c644b8766bcb1b1be1c03e84de..908d3aae51c3948803199af980ae4f7bb1e1f157 100644 (file)
@@ -27,6 +27,7 @@
 #include <boost/shared_ptr.hpp>
 #include <boost/weak_ptr.hpp>
 #include <boost/utility.hpp>
+#include "types.h"
 
 class Film;
 
@@ -43,6 +44,14 @@ public:
         *  cause the object to emit some data.
         */
        virtual void pass () = 0;
+
+       /** Seek so that the next pass() will yield the next thing
+        *  (video/sound frame, subtitle etc.) at or after the requested
+        *  time.  Pass accurate = true to try harder to get close to
+        *  the request.
+        */
+       virtual void seek (Time time, bool accurate) = 0;
+
        virtual bool done () const = 0;
 
 protected:
index e5e5f317a0efeea02dda41588685e1e2d58d3fe7..53d12419a6872f88a8bdb8b5f58658867620d236 100644 (file)
@@ -191,6 +191,10 @@ FFmpeg::video_codec_context () const
 AVCodecContext *
 FFmpeg::audio_codec_context () const
 {
+       if (!_ffmpeg_content->audio_stream ()) {
+               return 0;
+       }
+       
        return _ffmpeg_content->audio_stream()->stream(_format_context)->codec;
 }
 
index 9533315a517e2a0501796bfe3d6f1522f55ff27a..b6df2e9295ea68340b770f1edda7151168548514 100644 (file)
@@ -310,16 +310,15 @@ FFmpegContent::output_audio_frame_rate () const
        /* Resample to a DCI-approved sample rate */
        double t = dcp_audio_frame_rate (content_audio_frame_rate ());
 
-       FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
+       FrameRateChange frc (video_frame_rate(), film->video_frame_rate());
 
        /* Compensate if the DCP is being run at a different frame rate
           to the source; that is, if the video is run such that it will
           look different in the DCP compared to the source (slower or faster).
-          skip/repeat doesn't come into effect here.
        */
 
        if (frc.change_speed) {
-               t *= video_frame_rate() * frc.factor() / film->video_frame_rate();
+               t /= frc.speed_up;
        }
 
        return rint (t);
@@ -452,7 +451,7 @@ FFmpegContent::full_length () const
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
        
-       FrameRateConversion frc (video_frame_rate (), film->video_frame_rate ());
+       FrameRateChange frc (video_frame_rate (), film->video_frame_rate ());
        return video_length() * frc.factor() * TIME_HZ / film->video_frame_rate ();
 }
 
index 5a1b78762d92e6e30d55108dfae637f8b348e676..ed0574c6140ac24021b17b56e88a14a11f27b7de 100644 (file)
@@ -297,70 +297,132 @@ FFmpegDecoder::bytes_per_audio_sample () const
        return av_get_bytes_per_sample (audio_sample_format ());
 }
 
-void
-FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate)
+int
+FFmpegDecoder::minimal_run (boost::function<bool (int)> finished)
 {
-       double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base);
+       int frames_read = 0;
+       
+       while (!finished (frames_read)) {
+               int r = av_read_frame (_format_context, &_packet);
+               if (r < 0) {
+                       return -1;
+               }
 
-       /* If we are doing an accurate seek, our initial shot will be 5 frames (5 being
-          a number plucked from the air) earlier than we want to end up.  The loop below
-          will hopefully then step through to where we want to be.
-       */
-       int initial = frame;
+               ++frames_read;
 
-       if (accurate) {
-               initial -= 5;
-       }
+               double const time_base = av_q2d (_format_context->streams[_packet.stream_index]->time_base);
+
+               if (_packet.stream_index == _video_stream) {
+
+                       avcodec_get_frame_defaults (_frame);
+                       
+                       int finished = 0;
+                       r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
+                       if (r >= 0 && finished) {
+                               _video_position = rint (
+                                       (av_frame_get_best_effort_timestamp (_frame) * time_base + _video_pts_offset) * _ffmpeg_content->video_frame_rate()
+                                       );
+                       }
+
+               } else if (_ffmpeg_content->audio_stream() && _packet.stream_index == _ffmpeg_content->audio_stream()->index (_format_context)) {
+
+                       AVPacket copy_packet = _packet;
 
-       if (initial < 0) {
-               initial = 0;
+                       while (copy_packet.size > 0) {
+
+                               int finished;
+                               r = avcodec_decode_audio4 (audio_codec_context(), _frame, &finished, &copy_packet);
+                               if (r >= 0 && finished) {
+                                       _audio_position = rint (
+                                               (av_frame_get_best_effort_timestamp (_frame) * time_base + _audio_pts_offset) *
+                                               _ffmpeg_content->audio_stream()->frame_rate
+                                               );
+                               }
+
+                               copy_packet.data += r;
+                               copy_packet.size -= r;
+                       }
+               }
+
+               av_free_packet (&_packet);
        }
 
-       /* Initial seek time in the stream's timebase */
-       int64_t const initial_vt = ((initial / _ffmpeg_content->video_frame_rate()) - _video_pts_offset) / time_base;
+       return frames_read;
+}
+
+bool
+FFmpegDecoder::seek_overrun_finished (Time seek) const
+{
+       return (
+               _video_position >= _ffmpeg_content->time_to_content_video_frames (seek) ||
+               _audio_position >= _ffmpeg_content->time_to_content_audio_frames (seek, _ffmpeg_content->position())
+               );
+}
+
+bool
+FFmpegDecoder::seek_final_finished (int n, int done) const
+{
+       return n == done;
+}
+
+void
+FFmpegDecoder::seek_and_flush (Time t)
+{
+       int64_t const initial_v = ((_ffmpeg_content->time_to_content_video_frames (t) / _ffmpeg_content->video_frame_rate()) - _video_pts_offset) /
+               av_q2d (_format_context->streams[_video_stream]->time_base);
+
+       av_seek_frame (_format_context, _video_stream, initial_v, AVSEEK_FLAG_BACKWARD);
 
-       av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD);
+       shared_ptr<FFmpegAudioStream> as = _ffmpeg_content->audio_stream ();
+       if (as) {
+               int64_t initial_a = ((_ffmpeg_content->time_to_content_audio_frames (t, t) / as->frame_rate) - _audio_pts_offset) /
+                       av_q2d (as->stream(_format_context)->time_base);
+
+               av_seek_frame (_format_context, as->index (_format_context), initial_a, AVSEEK_FLAG_BACKWARD);
+       }
 
        avcodec_flush_buffers (video_codec_context());
+       if (audio_codec_context ()) {
+               avcodec_flush_buffers (audio_codec_context ());
+       }
        if (_subtitle_codec_context) {
                avcodec_flush_buffers (_subtitle_codec_context);
        }
 
+       _video_position = _ffmpeg_content->time_to_content_video_frames (t);
+       _audio_position = _ffmpeg_content->time_to_content_audio_frames (t, t);
+}
+
+void
+FFmpegDecoder::seek (Time time, bool accurate)
+{
+       /* If we are doing an accurate seek, our initial shot will be 200ms (200 being
+          a number plucked from the air) earlier than we want to end up.  The loop below
+          will hopefully then step through to where we want to be.
+       */
+
+       Time pre_roll = accurate ? (0.2 * TIME_HZ) : 0;
+       Time initial_seek = time - pre_roll;
+       if (initial_seek < 0) {
+               initial_seek = 0;
+       }
+       
+       /* Initial seek time in the video stream's timebase */
+
+       seek_and_flush (initial_seek);
+
        _just_sought = true;
-       _video_position = frame;
        
-       if (frame == 0 || !accurate) {
+       if (time == 0 || !accurate) {
                /* We're already there, or we're as close as we need to be */
                return;
        }
 
-       while (1) {
-               int r = av_read_frame (_format_context, &_packet);
-               if (r < 0) {
-                       return;
-               }
+       int const N = minimal_run (boost::bind (&FFmpegDecoder::seek_overrun_finished, this, time));
 
-               if (_packet.stream_index != _video_stream) {
-                       av_free_packet (&_packet);
-                       continue;
-               }
-               
-               avcodec_get_frame_defaults (_frame);
-               
-               int finished = 0;
-               r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet);
-               if (r >= 0 && finished) {
-                       _video_position = rint (
-                               (av_frame_get_best_effort_timestamp (_frame) * time_base + _video_pts_offset) * _ffmpeg_content->video_frame_rate()
-                               );
-
-                       if (_video_position >= (frame - 1)) {
-                               av_free_packet (&_packet);
-                               break;
-                       }
-               }
-               
-               av_free_packet (&_packet);
+       seek_and_flush (initial_seek);
+       if (N > 0) {
+               minimal_run (boost::bind (&FFmpegDecoder::seek_final_finished, this, N - 1, _1));
        }
 }
 
@@ -377,6 +439,7 @@ FFmpegDecoder::decode_audio_packet ()
 
                int frame_finished;
                int const decode_result = avcodec_decode_audio4 (audio_codec_context(), _frame, &frame_finished, &copy_packet);
+
                if (decode_result < 0) {
                        shared_ptr<const Film> film = _film.lock ();
                        assert (film);
index 11f83ed97c9808aeaff603f110814463b51234a6..f54ee2496fa0479fcdcea793e576568bd1452073 100644 (file)
@@ -52,7 +52,7 @@ public:
        ~FFmpegDecoder ();
 
        void pass ();
-       void seek (VideoContent::Frame, bool);
+       void seek (Time time, bool);
        bool done () const;
 
 private:
@@ -74,6 +74,11 @@ private:
        void maybe_add_subtitle ();
        boost::shared_ptr<AudioBuffers> deinterleave_audio (uint8_t** data, int size);
 
+       bool seek_overrun_finished (Time) const;
+       bool seek_final_finished (int, int) const;
+       int minimal_run (boost::function<bool (int)>);
+       void seek_and_flush (int64_t);
+
        AVCodecContext* _subtitle_codec_context; ///< may be 0 if there is no subtitle
        AVCodec* _subtitle_codec;                ///< may be 0 if there is no subtitle
        
index 5946d5bec8ce3db40f8c53d06259ae521f8dd170..b61991a45ca4a60dfc3bbcc1c2676b78611ebdf2 100644 (file)
@@ -866,6 +866,12 @@ Film::content_paths_valid () const
        return _playlist->content_paths_valid ();
 }
 
+FrameRateChange
+Film::active_frame_rate_change (Time t) const
+{
+       return _playlist->active_frame_rate_change (t, video_frame_rate ());
+}
+
 void
 Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
 {
index 4b07f84a928503befb9fd231ab2f4a0a8efd97e9..6573bd5b706369637914750685c7ba42afbcc898 100644 (file)
@@ -115,6 +115,7 @@ public:
        bool has_subtitles () const;
        OutputVideoFrame best_video_frame_rate () const;
        bool content_paths_valid () const;
+       FrameRateChange active_frame_rate_change (Time) const;
 
        libdcp::KDM
        make_kdm (
index b05fa6b8d69ce8599503e3d9b3c1e6c5179e6c9a..8eba33f3af20e2a780abac3f97b5ce24a8332992 100644 (file)
@@ -126,7 +126,7 @@ ImageContent::full_length () const
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
        
-       FrameRateConversion frc (video_frame_rate(), film->video_frame_rate ());
+       FrameRateChange frc (video_frame_rate(), film->video_frame_rate ());
        return video_length() * frc.factor() * TIME_HZ / video_frame_rate();
 }
 
index fb6053ae5226188d91f31809133df65aec47d011..bf3bc344b9e90b7e8b0d8d5413fd12d13b70722a 100644 (file)
@@ -77,9 +77,9 @@ ImageDecoder::pass ()
 }
 
 void
-ImageDecoder::seek (VideoContent::Frame frame, bool)
+ImageDecoder::seek (Time time, bool)
 {
-       _video_position = frame;
+       _video_position = _video_content->time_to_content_video_frames (time);
 }
 
 bool
index c7500243e08091ec3d6669f3c14273dfb93454e4..1f5f0b9d2310c463ea3f9bf4124b01a7b63c8d0c 100644 (file)
@@ -37,7 +37,7 @@ public:
        /* Decoder */
 
        void pass ();
-       void seek (VideoContent::Frame, bool);
+       void seek (Time, bool);
        bool done () const;
 
 private:
index 7f500b3d6f358a1e786b4ee61f5ddf281a7e9e71..184c9811f5f6be2f40ebd5e71dc4dc4a330533fc 100644 (file)
@@ -235,7 +235,7 @@ Player::pass ()
                        _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
                }
        }
-               
+
        return false;
 }
 
@@ -259,7 +259,7 @@ Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const Image> image
        shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
        assert (content);
 
-       FrameRateConversion frc (content->video_frame_rate(), _film->video_frame_rate());
+       FrameRateChange frc (content->video_frame_rate(), _film->video_frame_rate());
        if (frc.skip && (frame % 2) == 1) {
                return;
        }
@@ -420,16 +420,12 @@ Player::seek (Time t, bool accurate)
                (*i)->video_position = (*i)->audio_position = vc->position() + s;
 
                /* And seek the decoder */
-               dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
-                       vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
-                       );
-
+               dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (s + vc->trim_start (), accurate);
                (*i)->reset_repeat ();
        }
 
        _video_position = _audio_position = t;
-
-       /* XXX: don't seek audio because we don't need to... */
+       _audio_merger.clear (t);
 }
 
 void
@@ -456,7 +452,7 @@ Player::setup_pieces ()
                        fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
                        fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
 
-                       fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true);
+                       fd->seek (fc->trim_start (), true);
                        piece->decoder = fd;
                }
                
index 37b2902189f2b32d61a94a57bbc0baa36c7f0a28..581235adca4c9d7ebfe163863c586d5fda06715c 100644 (file)
@@ -292,6 +292,23 @@ Playlist::video_end () const
        return end;
 }
 
+FrameRateChange
+Playlist::active_frame_rate_change (Time t, int dcp_video_frame_rate) const
+{
+       for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
+               shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i);
+               if (!vc) {
+                       break;
+               }
+
+               if (vc->position() >= t && t < vc->end()) {
+                       return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate);
+               }
+       }
+
+       return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate);
+}
+
 void
 Playlist::set_sequence_video (bool s)
 {
index f87b3397b4b68e388f3f5e0f37b512c385c4bcf5..d3cf50a27b605c7b6cab2be544d45e1fbbb3a1db 100644 (file)
@@ -25,6 +25,7 @@
 #include <boost/enable_shared_from_this.hpp>
 #include "ffmpeg_content.h"
 #include "audio_mapping.h"
+#include "util.h"
 
 class Content;
 class FFmpegContent;
@@ -74,6 +75,7 @@ public:
        
        int best_dcp_frame_rate () const;
        Time video_end () const;
+       FrameRateChange active_frame_rate_change (Time, int dcp_frame_rate) const;
 
        void set_sequence_video (bool);
        void maybe_sequence_video ();
index e10f4f568430d08dd86d1d5db38d2116606fbbbb..0cca25257f538f0a25e650d054e0df3610671e62 100644 (file)
@@ -118,3 +118,9 @@ SndfileDecoder::done () const
 {
        return _audio_position >= _sndfile_content->audio_length ();
 }
+
+void
+SndfileDecoder::seek (Time t, bool accurate)
+{
+       /* XXX */
+}
index 77fa6d17734da4096757dae504a759c9925e0e8b..c3db1c294a6f805d97290aa2ccd83172d2a4e0ad 100644 (file)
@@ -30,6 +30,7 @@ public:
        ~SndfileDecoder ();
 
        void pass ();
+       void seek (Time, bool);
        bool done () const;
 
        int audio_channels () const;
index ddc0a297459cef504b49300b25f874752ec2ba64..d5a07192c9f2694842b37161a88a878e71e216c8 100644 (file)
@@ -772,7 +772,7 @@ audio_channel_name (int c)
        return channels[c];
 }
 
-FrameRateConversion::FrameRateConversion (float source, int dcp)
+FrameRateChange::FrameRateChange (float source, int dcp)
        : skip (false)
        , repeat (1)
        , change_speed (false)
@@ -790,7 +790,8 @@ FrameRateConversion::FrameRateConversion (float source, int dcp)
                repeat = round (dcp / source);
        }
 
-       change_speed = !about_equal (source * factor(), dcp);
+       speed_up = dcp / (source * factor());
+       change_speed = !about_equal (speed_up, 1.0);
 
        if (!skip && repeat == 1 && !change_speed) {
                description = _("Content and DCP have the same rate.\n");
index 7dcd920b7cac3d72974332eda264466faf0cc42a..9b201a50e7ab6cf92e59c2233809d1828d0b5ca7 100644 (file)
@@ -79,9 +79,9 @@ extern std::string tidy_for_filename (std::string);
 extern boost::shared_ptr<const libdcp::Signer> make_signer ();
 extern libdcp::Size fit_ratio_within (float ratio, libdcp::Size);
 
-struct FrameRateConversion
+struct FrameRateChange
 {
-       FrameRateConversion (float, int);
+       FrameRateChange (float, int);
 
        /** @return factor by which to multiply a source frame rate
            to get the effective rate after any skip or repeat has happened.
@@ -109,6 +109,11 @@ struct FrameRateConversion
         */
        bool change_speed;
 
+       /** Amount by which the video is being sped-up in the DCP; e.g. for a
+        *  24fps source in a 25fps DCP this would be 25/24.
+        */
+       float speed_up;
+
        std::string description;
 };
 
index 0a19ffd692463ac4f2923cd106ea54ddfccd4de1..c61a2455fcae9e8a8f7ed5545a6562e5a79308bf 100644 (file)
@@ -351,7 +351,8 @@ VideoContent::video_size_after_crop () const
 }
 
 /** @param t A time offset from the start of this piece of content.
- *  @return Corresponding frame index.
+ *  @return Corresponding frame index, rounded up so that the frame index
+ *  is that of the next complete frame which starts after `t'.
  */
 VideoContent::Frame
 VideoContent::time_to_content_video_frames (Time t) const
@@ -359,11 +360,12 @@ VideoContent::time_to_content_video_frames (Time t) const
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
        
-       FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
-
        /* Here we are converting from time (in the DCP) to a frame number in the content.
           Hence we need to use the DCP's frame rate and the double/skip correction, not
-          the source's rate.
+          the source's rate; source rate will be equal to DCP rate if we ignore
+          double/skip.  There's no need to call Film::active_frame_rate_change() here
+          as we know that we are it (since we're video).
        */
-       return t * film->video_frame_rate() / (frc.factor() * TIME_HZ);
+       FrameRateChange frc (video_frame_rate(), film->video_frame_rate());
+       return ceil (t * film->video_frame_rate() / (frc.factor() * TIME_HZ));
 }
index 142320a049c8e7a23bf7aff2d618149d6259941d..01319e48125a933bada580fd00ed3d559b46ae49 100644 (file)
@@ -34,11 +34,6 @@ class VideoDecoder : public virtual Decoder
 public:
        VideoDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const VideoContent>);
 
-       /** Seek so that the next pass() will yield (approximately) the requested frame.
-        *  Pass accurate = true to try harder to get close to the request.
-        */
-       virtual void seek (VideoContent::Frame frame, bool accurate) = 0;
-
        /** Emitted when a video frame is ready.
         *  First parameter is the video image.
         *  Second parameter is the eye(s) which should see this image.
index 5d841b0cc45d6c5b5c3481076e85c657febc16c4..4605abae72626f9b3cbcb013f03ac1e2a6bea8c1 100644 (file)
@@ -332,7 +332,7 @@ VideoPanel::setup_description ()
 
        d << wxString::Format (_("Content frame rate %.4f\n"), vcs->video_frame_rate ());
        ++lines;
-       FrameRateConversion frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ());
+       FrameRateChange frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ());
        d << frc.description << "\n";
        ++lines;
 
diff --git a/test/ffmpeg_seek_test.cc b/test/ffmpeg_seek_test.cc
new file mode 100644 (file)
index 0000000..7014bf1
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+    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.
+
+*/
+
+#include <boost/test/unit_test.hpp>
+#include "lib/player.h"
+#include "lib/ffmpeg_decoder.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "test.h"
+
+using std::cout;
+using std::string;
+using std::stringstream;
+using boost::shared_ptr;
+
+#define FFMPEG_SEEK_TEST_DEBUG 1
+
+boost::optional<Time> first_video;
+boost::optional<Time> first_audio;
+
+static void
+process_video (shared_ptr<PlayerImage>, Eyes, ColourConversion, bool, Time t)
+{
+       if (!first_video) {
+               first_video = t;
+       }
+}
+
+static void
+process_audio (shared_ptr<const AudioBuffers>, Time t)
+{
+       if (!first_audio) {
+               first_audio = t;
+       }
+}
+
+static string
+print_time (Time t, float fps)
+{
+       stringstream s;
+       s << t << " " << (float(t) / TIME_HZ) << "s " << (float(t) * fps / TIME_HZ) << "f";
+       return s.str ();
+}
+
+static void
+check (shared_ptr<Player> p, Time t)
+{
+       first_video.reset ();
+       first_audio.reset ();
+
+#if FFMPEG_SEEK_TEST_DEBUG == 1
+       cout << "\n-- Seek to " << print_time (t, 24) << "\n";
+#endif 
+       
+       p->seek (t, true);
+       while (!first_video || !first_audio) {
+               p->pass ();
+       }
+
+#if FFMPEG_SEEK_TEST_DEBUG == 1
+       cout << "First video " << print_time (first_video.get(), 24) << "\n";
+       cout << "First audio " << print_time (first_audio.get(), 24) << "\n";
+#endif 
+       
+       BOOST_CHECK (first_video.get() >= t);
+       BOOST_CHECK (first_audio.get() >= t);
+}
+
+BOOST_AUTO_TEST_CASE (ffmpeg_seek_test)
+{
+       shared_ptr<Film> film = new_test_film ("ffmpeg_audio_test");
+       film->set_name ("ffmpeg_audio_test");
+       film->set_container (Ratio::from_id ("185"));
+       shared_ptr<FFmpegContent> c (new FFmpegContent (film, "test/data/staircase.mov"));
+       c->set_ratio (Ratio::from_id ("185"));
+       film->examine_and_add_content (c);
+
+       wait_for_jobs ();
+
+       shared_ptr<Player> player = film->make_player ();
+       player->Video.connect (boost::bind (&process_video, _1, _2, _3, _4, _5));
+       player->Audio.connect (boost::bind (&process_audio, _1, _2));
+
+       check (player, 0);
+       check (player, 0.1 * TIME_HZ);
+       check (player, 0.2 * TIME_HZ);
+       check (player, 0.3 * TIME_HZ);
+}
+
+
index fdfdcf4529739f126ffab9da192aafeec44f8503..7d3904e740a8b05bf898a2d7cd0efa14e15f52e5 100644 (file)
@@ -47,91 +47,102 @@ BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test_single)
 
        content->_video_frame_rate = 60;
        int best = film->playlist()->best_dcp_frame_rate ();
-       FrameRateConversion frc = FrameRateConversion (60, best);
+       FrameRateChange frc = FrameRateChange (60, best);
        BOOST_CHECK_EQUAL (best, 30);
        BOOST_CHECK_EQUAL (frc.skip, true);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
        
        content->_video_frame_rate = 50;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (50, best);
+       frc = FrameRateChange (50, best);
        BOOST_CHECK_EQUAL (best, 25);
        BOOST_CHECK_EQUAL (frc.skip, true);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        content->_video_frame_rate = 48;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (48, best);
+       frc = FrameRateChange (48, best);
        BOOST_CHECK_EQUAL (best, 24);
        BOOST_CHECK_EQUAL (frc.skip, true);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        content->_video_frame_rate = 30;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (30, best);
+       frc = FrameRateChange (30, best);
        BOOST_CHECK_EQUAL (best, 30);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        content->_video_frame_rate = 29.97;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (29.97, best);
+       frc = FrameRateChange (29.97, best);
        BOOST_CHECK_EQUAL (best, 30);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, true);
+       BOOST_CHECK_CLOSE (frc.speed_up, 30 / 29.97, 0.1);
        
        content->_video_frame_rate = 25;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (25, best);
+       frc = FrameRateChange (25, best);
        BOOST_CHECK_EQUAL (best, 25);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        content->_video_frame_rate = 24;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (24, best);
+       frc = FrameRateChange (24, best);
        BOOST_CHECK_EQUAL (best, 24);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        content->_video_frame_rate = 14.5;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (14.5, best);
+       frc = FrameRateChange (14.5, best);
        BOOST_CHECK_EQUAL (best, 30);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 2);
        BOOST_CHECK_EQUAL (frc.change_speed, true);
+       BOOST_CHECK_CLOSE (frc.speed_up, 15 / 14.5, 0.1);
 
        content->_video_frame_rate = 12.6;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (12.6, best);
+       frc = FrameRateChange (12.6, best);
        BOOST_CHECK_EQUAL (best, 25);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 2);
        BOOST_CHECK_EQUAL (frc.change_speed, true);
+       BOOST_CHECK_CLOSE (frc.speed_up, 25 / 25.2, 0.1);
 
        content->_video_frame_rate = 12.4;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (12.4, best);
+       frc = FrameRateChange (12.4, best);
        BOOST_CHECK_EQUAL (best, 25);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 2);
        BOOST_CHECK_EQUAL (frc.change_speed, true);
+       BOOST_CHECK_CLOSE (frc.speed_up, 25 / 24.8, 0.1);
 
        content->_video_frame_rate = 12;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (12, best);
+       frc = FrameRateChange (12, best);
        BOOST_CHECK_EQUAL (best, 24);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 2);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        /* Now add some more rates and see if it will use them
           in preference to skip/repeat.
@@ -144,34 +155,38 @@ BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test_single)
 
        content->_video_frame_rate = 60;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (60, best);
+       frc = FrameRateChange (60, best);
        BOOST_CHECK_EQUAL (best, 60);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
        
        content->_video_frame_rate = 50;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (50, best);
+       frc = FrameRateChange (50, best);
        BOOST_CHECK_EQUAL (best, 50);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        content->_video_frame_rate = 48;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (48, best);
+       frc = FrameRateChange (48, best);
        BOOST_CHECK_EQUAL (best, 48);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, false);
+       BOOST_CHECK_CLOSE (frc.speed_up, 1, 0.1);
 
        /* Check some out-there conversions (not the best) */
        
-       frc = FrameRateConversion (14.99, 24);
+       frc = FrameRateChange (14.99, 24);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 2);
        BOOST_CHECK_EQUAL (frc.change_speed, true);
+       BOOST_CHECK_CLOSE (frc.speed_up, 2 * 14.99 / 24, 0.1);
 
        /* Check some conversions with limited DCP targets */
 
@@ -181,14 +196,15 @@ BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test_single)
 
        content->_video_frame_rate = 25;
        best = film->playlist()->best_dcp_frame_rate ();
-       frc = FrameRateConversion (25, best);
+       frc = FrameRateChange (25, best);
        BOOST_CHECK_EQUAL (best, 24);
        BOOST_CHECK_EQUAL (frc.skip, false);
        BOOST_CHECK_EQUAL (frc.repeat, 1);
        BOOST_CHECK_EQUAL (frc.change_speed, true);
+       BOOST_CHECK_CLOSE (frc.speed_up, 24.0 / 25, 0.1);
 }
 
-/* Test Playlist::best_dcp_frame_rate and FrameRateConversion
+/* Test Playlist::best_dcp_frame_rate and FrameRateChange
    with two pieces of content.
 */
 BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test_double)
@@ -266,7 +282,7 @@ BOOST_AUTO_TEST_CASE (audio_sampling_rate_test)
        content->_video_frame_rate = 14.99;
        film->set_video_frame_rate (25);
        content->set_audio_stream (shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream ("a", 42, 16000, 0)));
-       /* The FrameRateConversion within output_audio_frame_rate should choose to double-up
+       /* The FrameRateChange within output_audio_frame_rate should choose to double-up
           the 14.99 fps video to 30 and then run it slow at 25.
        */
        BOOST_CHECK_EQUAL (content->output_audio_frame_rate(), rint (48000 * 2 * 14.99 / 25));
diff --git a/test/long_ffmpeg_seek_test.cc b/test/long_ffmpeg_seek_test.cc
new file mode 100644 (file)
index 0000000..5f7f6c2
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+    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.
+
+*/
+
+#include <boost/test/unit_test.hpp>
+#include "lib/player.h"
+#include "lib/ffmpeg_decoder.h"
+#include "lib/film.h"
+#include "lib/ratio.h"
+#include "test.h"
+
+using std::cout;
+using std::string;
+using std::stringstream;
+using boost::shared_ptr;
+
+#define LONG_FFMPEG_SEEK_TEST_DEBUG 1
+
+boost::optional<Time> first_video;
+boost::optional<Time> first_audio;
+
+static void
+process_video (shared_ptr<PlayerImage>, Eyes, ColourConversion, bool, Time t)
+{
+       if (!first_video) {
+               first_video = t;
+       }
+}
+
+static void
+process_audio (shared_ptr<const AudioBuffers>, Time t)
+{
+       if (!first_audio) {
+               first_audio = t;
+       }
+}
+
+static string
+print_time (Time t, float fps)
+{
+       stringstream s;
+       s << t << " " << (float(t) / TIME_HZ) << "s " << (float(t) * fps / TIME_HZ) << "f";
+       return s.str ();
+}
+
+static void
+check (shared_ptr<Player> p, Time t)
+{
+       first_video.reset ();
+       first_audio.reset ();
+
+#if LONG_FFMPEG_SEEK_TEST_DEBUG == 1
+       cout << "\n-- Seek to " << print_time (t, 24) << "\n";
+#endif 
+       
+       p->seek (t, true);
+       while (!first_video || !first_audio) {
+               p->pass ();
+       }
+
+#if LONG_FFMPEG_SEEK_TEST_DEBUG == 1
+       cout << "First video " << print_time (first_video.get(), 24) << "\n";
+       cout << "First audio " << print_time (first_audio.get(), 24) << "\n";
+#endif 
+       
+       BOOST_CHECK (first_video.get() >= t);
+       BOOST_CHECK (first_audio.get() >= t);
+}
+
+BOOST_AUTO_TEST_CASE (long_ffmpeg_seek_test)
+{
+       shared_ptr<Film> film = new_test_film ("long_ffmpeg_audio_test");
+       film->set_name ("long_ffmpeg_audio_test");
+       film->set_container (Ratio::from_id ("185"));
+       shared_ptr<FFmpegContent> c (new FFmpegContent (film, "test/long_data/dolby_aurora.vob"));
+       c->set_ratio (Ratio::from_id ("185"));
+       film->examine_and_add_content (c);
+
+       wait_for_jobs ();
+
+       shared_ptr<Player> player = film->make_player ();
+       player->Video.connect (boost::bind (&process_video, _1, _2, _3, _4, _5));
+       player->Audio.connect (boost::bind (&process_audio, _1, _2));
+
+       for (float i = 0; i < 10; i += 0.1) {
+               check (player, i * TIME_HZ);
+       }
+}
+
+
index 22dea1fc4ea537b705d36084731305b808c63df3..92ce95656dfd4ba021fb83b8bf5acbd66a951953 100644 (file)
@@ -53,9 +53,9 @@ public:
 
 struct TestConfig
 {
-       TestConfig()
+       TestConfig ()
        {
-               dcpomatic_setup();
+               dcpomatic_setup ();
 
                Config::instance()->set_num_local_encoding_threads (1);
                Config::instance()->set_server_port_base (61920);
index 4ae49b087a05b32c3dfac078a4dbfaaddcb4896b..df9aa88d3d8d72ff420df856d69d1f7f9d9ab5c8 100644 (file)
@@ -10,7 +10,7 @@ def configure(conf):
                               """, msg = 'Checking for boost unit testing library', lib = 'boost_unit_test_framework%s' % boost_test_suffix, uselib_store = 'BOOST_TEST')
 
 def build(bld):
-    obj = bld(features = 'cxx cxxprogram')
+    obj = bld(features='cxx cxxprogram')
     obj.name   = 'unit-tests'
     obj.uselib = 'BOOST_TEST DCP OPENJPEG AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC CXML'
     obj.use    = 'libdcpomatic'
@@ -26,6 +26,7 @@ def build(bld):
                  ffmpeg_dcp_test.cc
                  ffmpeg_examiner_test.cc
                  ffmpeg_pts_offset.cc
+                 ffmpeg_seek_test.cc
                  file_group_test.cc
                  film_metadata_test.cc
                  frame_rate_test.cc
@@ -46,3 +47,15 @@ def build(bld):
 
     obj.target = 'unit-tests'
     obj.install_path = ''
+
+    obj = bld(features='cxx cxxprogram')
+    obj.name   = 'long-unit-tests'
+    obj.uselib = 'BOOST_TEST DCP OPENJPEG AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC CXML'
+    obj.use    = 'libdcpomatic'
+    obj.source = """
+                 test.cc
+                 long_ffmpeg_seek_test.cc
+                 """
+
+    obj.target = 'long-unit-tests'
+    obj.install_path = ''