diff options
| author | Carl Hetherington <cth@carlh.net> | 2013-12-11 10:31:18 +0000 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2013-12-11 10:31:18 +0000 |
| commit | ea910e250a0fb3b0ad3ce0cf32dd27b24c17cd1d (patch) | |
| tree | 32119d2a04532d3162c656df7c7e715b289c6e61 /src | |
| parent | 3e5df964ada49e10a880a200c985cc309ccecb64 (diff) | |
Various work on better seeking (and seeking of audio).
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/audio_content.cc | 24 | ||||
| -rw-r--r-- | src/lib/audio_content.h | 2 | ||||
| -rw-r--r-- | src/lib/audio_merger.h | 9 | ||||
| -rw-r--r-- | src/lib/decoder.h | 9 | ||||
| -rw-r--r-- | src/lib/ffmpeg.cc | 4 | ||||
| -rw-r--r-- | src/lib/ffmpeg_content.cc | 7 | ||||
| -rw-r--r-- | src/lib/ffmpeg_decoder.cc | 151 | ||||
| -rw-r--r-- | src/lib/ffmpeg_decoder.h | 7 | ||||
| -rw-r--r-- | src/lib/film.cc | 6 | ||||
| -rw-r--r-- | src/lib/film.h | 1 | ||||
| -rw-r--r-- | src/lib/image_content.cc | 2 | ||||
| -rw-r--r-- | src/lib/image_decoder.cc | 4 | ||||
| -rw-r--r-- | src/lib/image_decoder.h | 2 | ||||
| -rw-r--r-- | src/lib/player.cc | 14 | ||||
| -rw-r--r-- | src/lib/playlist.cc | 17 | ||||
| -rw-r--r-- | src/lib/playlist.h | 2 | ||||
| -rw-r--r-- | src/lib/sndfile_decoder.cc | 6 | ||||
| -rw-r--r-- | src/lib/sndfile_decoder.h | 1 | ||||
| -rw-r--r-- | src/lib/util.cc | 5 | ||||
| -rw-r--r-- | src/lib/util.h | 9 | ||||
| -rw-r--r-- | src/lib/video_content.cc | 12 | ||||
| -rw-r--r-- | src/lib/video_decoder.h | 5 | ||||
| -rw-r--r-- | src/wx/video_panel.cc | 2 |
23 files changed, 223 insertions, 78 deletions
diff --git a/src/lib/audio_content.cc b/src/lib/audio_content.cc index 97372b962..0c4586681 100644 --- a/src/lib/audio_content.cc +++ b/src/lib/audio_content.cc @@ -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); +} diff --git a/src/lib/audio_content.h b/src/lib/audio_content.h index ca4a1f234..10114e10d 100644 --- a/src/lib/audio_content.h +++ b/src/lib/audio_content.h @@ -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; diff --git a/src/lib/audio_merger.h b/src/lib/audio_merger.h index 226601e0e..6ad33fb37 100644 --- a/src/lib/audio_merger.h +++ b/src/lib/audio_merger.h @@ -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; diff --git a/src/lib/decoder.h b/src/lib/decoder.h index d67592ed8..908d3aae5 100644 --- a/src/lib/decoder.h +++ b/src/lib/decoder.h @@ -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: diff --git a/src/lib/ffmpeg.cc b/src/lib/ffmpeg.cc index e5e5f317a..53d12419a 100644 --- a/src/lib/ffmpeg.cc +++ b/src/lib/ffmpeg.cc @@ -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; } diff --git a/src/lib/ffmpeg_content.cc b/src/lib/ffmpeg_content.cc index 9533315a5..b6df2e929 100644 --- a/src/lib/ffmpeg_content.cc +++ b/src/lib/ffmpeg_content.cc @@ -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 (); } diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc index 5a1b78762..ed0574c61 100644 --- a/src/lib/ffmpeg_decoder.cc +++ b/src/lib/ffmpeg_decoder.cc @@ -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, ©_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, ©_packet); + if (decode_result < 0) { shared_ptr<const Film> film = _film.lock (); assert (film); diff --git a/src/lib/ffmpeg_decoder.h b/src/lib/ffmpeg_decoder.h index 11f83ed97..f54ee2496 100644 --- a/src/lib/ffmpeg_decoder.h +++ b/src/lib/ffmpeg_decoder.h @@ -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 diff --git a/src/lib/film.cc b/src/lib/film.cc index 5946d5bec..b61991a45 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -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) { diff --git a/src/lib/film.h b/src/lib/film.h index 4b07f84a9..6573bd5b7 100644 --- a/src/lib/film.h +++ b/src/lib/film.h @@ -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 ( diff --git a/src/lib/image_content.cc b/src/lib/image_content.cc index b05fa6b8d..8eba33f3a 100644 --- a/src/lib/image_content.cc +++ b/src/lib/image_content.cc @@ -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(); } diff --git a/src/lib/image_decoder.cc b/src/lib/image_decoder.cc index fb6053ae5..bf3bc344b 100644 --- a/src/lib/image_decoder.cc +++ b/src/lib/image_decoder.cc @@ -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 diff --git a/src/lib/image_decoder.h b/src/lib/image_decoder.h index c7500243e..1f5f0b9d2 100644 --- a/src/lib/image_decoder.h +++ b/src/lib/image_decoder.h @@ -37,7 +37,7 @@ public: /* Decoder */ void pass (); - void seek (VideoContent::Frame, bool); + void seek (Time, bool); bool done () const; private: diff --git a/src/lib/player.cc b/src/lib/player.cc index 7f500b3d6..184c9811f 100644 --- a/src/lib/player.cc +++ b/src/lib/player.cc @@ -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; } diff --git a/src/lib/playlist.cc b/src/lib/playlist.cc index 37b290218..581235adc 100644 --- a/src/lib/playlist.cc +++ b/src/lib/playlist.cc @@ -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) { diff --git a/src/lib/playlist.h b/src/lib/playlist.h index f87b3397b..d3cf50a27 100644 --- a/src/lib/playlist.h +++ b/src/lib/playlist.h @@ -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 (); diff --git a/src/lib/sndfile_decoder.cc b/src/lib/sndfile_decoder.cc index e10f4f568..0cca25257 100644 --- a/src/lib/sndfile_decoder.cc +++ b/src/lib/sndfile_decoder.cc @@ -118,3 +118,9 @@ SndfileDecoder::done () const { return _audio_position >= _sndfile_content->audio_length (); } + +void +SndfileDecoder::seek (Time t, bool accurate) +{ + /* XXX */ +} diff --git a/src/lib/sndfile_decoder.h b/src/lib/sndfile_decoder.h index 77fa6d177..c3db1c294 100644 --- a/src/lib/sndfile_decoder.h +++ b/src/lib/sndfile_decoder.h @@ -30,6 +30,7 @@ public: ~SndfileDecoder (); void pass (); + void seek (Time, bool); bool done () const; int audio_channels () const; diff --git a/src/lib/util.cc b/src/lib/util.cc index ddc0a2974..d5a07192c 100644 --- a/src/lib/util.cc +++ b/src/lib/util.cc @@ -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"); diff --git a/src/lib/util.h b/src/lib/util.h index 7dcd920b7..9b201a50e 100644 --- a/src/lib/util.h +++ b/src/lib/util.h @@ -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; }; diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc index 0a19ffd69..c61a2455f 100644 --- a/src/lib/video_content.cc +++ b/src/lib/video_content.cc @@ -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)); } diff --git a/src/lib/video_decoder.h b/src/lib/video_decoder.h index 142320a04..01319e481 100644 --- a/src/lib/video_decoder.h +++ b/src/lib/video_decoder.h @@ -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. diff --git a/src/wx/video_panel.cc b/src/wx/video_panel.cc index 5d841b0cc..4605abae7 100644 --- a/src/wx/video_panel.cc +++ b/src/wx/video_panel.cc @@ -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; |
