From 895f945a6bababdf1964b0522d591db96368db22 Mon Sep 17 00:00:00 2001 From: Carl Hetherington Date: Sat, 8 Sep 2018 01:26:21 +0100 Subject: [PATCH] Split parts of FFmpegEncoder into FFmpegFileEncoder. --- src/lib/ffmpeg_encoder.cc | 358 +++---------------------------- src/lib/ffmpeg_encoder.h | 53 +---- src/lib/ffmpeg_file_encoder.cc | 377 +++++++++++++++++++++++++++++++++ src/lib/ffmpeg_file_encoder.h | 93 ++++++++ src/lib/types.h | 6 + src/lib/wscript | 1 + src/wx/export_dialog.cc | 8 +- src/wx/export_dialog.h | 2 +- test/ffmpeg_encoder_test.cc | 30 +-- 9 files changed, 529 insertions(+), 399 deletions(-) create mode 100644 src/lib/ffmpeg_file_encoder.cc create mode 100644 src/lib/ffmpeg_file_encoder.h diff --git a/src/lib/ffmpeg_encoder.cc b/src/lib/ffmpeg_encoder.cc index 009a83def..2d7d41997 100644 --- a/src/lib/ffmpeg_encoder.cc +++ b/src/lib/ffmpeg_encoder.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2017 Carl Hetherington + Copyright (C) 2017-2018 Carl Hetherington This file is part of DCP-o-matic. @@ -40,39 +40,21 @@ using boost::shared_ptr; using boost::bind; using boost::weak_ptr; -int FFmpegEncoder::_video_stream_index = 0; -int FFmpegEncoder::_audio_stream_index = 1; -static AVPixelFormat -force_pixel_format (AVPixelFormat, AVPixelFormat out) -{ - return out; -} - -FFmpegEncoder::FFmpegEncoder (shared_ptr film, weak_ptr job, boost::filesystem::path output, Format format, bool mixdown_to_stereo, int x264_crf) +FFmpegEncoder::FFmpegEncoder (shared_ptr film, weak_ptr job, boost::filesystem::path output, ExportFormat format, bool mixdown_to_stereo, int x264_crf) : Encoder (film, job) - , _video_options (0) + , _file_encoder ( + _film->frame_size(), + _film->video_frame_rate(), + _film->audio_frame_rate(), + mixdown_to_stereo ? 2 : film->audio_channels(), + _film->log(), + format, + x264_crf, + output + ) , _history (1000) - , _output (output) { - switch (format) { - case FORMAT_PRORES: - _pixel_format = AV_PIX_FMT_YUV422P10; - _sample_format = AV_SAMPLE_FMT_S16; - _video_codec_name = "prores_ks"; - _audio_codec_name = "pcm_s16le"; - av_dict_set (&_video_options, "profile", "3", 0); - av_dict_set (&_video_options, "threads", "auto", 0); - break; - case FORMAT_H264: - _pixel_format = AV_PIX_FMT_YUV420P; - _sample_format = AV_SAMPLE_FMT_FLTP; - _video_codec_name = "libx264"; - _audio_codec_name = "aac"; - av_dict_set_int (&_video_options, "crf", x264_crf, 0); - break; - } - _player->set_always_burn_open_subtitles (); _player->set_play_referenced (); @@ -90,11 +72,9 @@ FFmpegEncoder::FFmpegEncoder (shared_ptr film, weak_ptr job, bo map.set (dcp::CENTRE, 1, overall_gain * minus_3dB); map.set (dcp::LS, 0, overall_gain); map.set (dcp::RS, 1, overall_gain); - _pending_audio.reset (new AudioBuffers (2, 0)); } else { _output_audio_channels = ch; map = AudioMapping (ch, ch); - _pending_audio.reset (new AudioBuffers (ch, 0)); for (int i = 0; i < ch; ++i) { map.set (i, i, 1); } @@ -103,99 +83,9 @@ FFmpegEncoder::FFmpegEncoder (shared_ptr film, weak_ptr job, bo _butler.reset (new Butler (_player, film->log(), map, _output_audio_channels)); } -void -FFmpegEncoder::setup_video () -{ - _video_codec = avcodec_find_encoder_by_name (_video_codec_name.c_str()); - if (!_video_codec) { - throw runtime_error (String::compose ("could not find FFmpeg encoder %1", _video_codec_name)); - } - - _video_codec_context = avcodec_alloc_context3 (_video_codec); - if (!_video_codec_context) { - throw runtime_error ("could not allocate FFmpeg video context"); - } - - avcodec_get_context_defaults3 (_video_codec_context, _video_codec); - - /* Variable quantisation */ - _video_codec_context->global_quality = 0; - _video_codec_context->width = _film->frame_size().width; - _video_codec_context->height = _film->frame_size().height; - _video_codec_context->time_base = (AVRational) { 1, _film->video_frame_rate() }; - _video_codec_context->pix_fmt = _pixel_format; - _video_codec_context->flags |= AV_CODEC_FLAG_QSCALE | AV_CODEC_FLAG_GLOBAL_HEADER; -} - -void -FFmpegEncoder::setup_audio () -{ - _audio_codec = avcodec_find_encoder_by_name (_audio_codec_name.c_str()); - if (!_audio_codec) { - throw runtime_error (String::compose ("could not find FFmpeg encoder %1", _audio_codec_name)); - } - - _audio_codec_context = avcodec_alloc_context3 (_audio_codec); - if (!_audio_codec_context) { - throw runtime_error ("could not allocate FFmpeg audio context"); - } - - avcodec_get_context_defaults3 (_audio_codec_context, _audio_codec); - - /* XXX: configurable */ - _audio_codec_context->bit_rate = 256 * 1024; - _audio_codec_context->sample_fmt = _sample_format; - _audio_codec_context->sample_rate = _film->audio_frame_rate (); - _audio_codec_context->channel_layout = av_get_default_channel_layout (_output_audio_channels); - _audio_codec_context->channels = _output_audio_channels; -} - void FFmpegEncoder::go () { - setup_video (); - setup_audio (); - - avformat_alloc_output_context2 (&_format_context, 0, 0, _output.string().c_str()); - if (!_format_context) { - throw runtime_error ("could not allocate FFmpeg format context"); - } - - _video_stream = avformat_new_stream (_format_context, _video_codec); - if (!_video_stream) { - throw runtime_error ("could not create FFmpeg output video stream"); - } - - _audio_stream = avformat_new_stream (_format_context, _audio_codec); - if (!_audio_stream) { - throw runtime_error ("could not create FFmpeg output audio stream"); - } - - _video_stream->id = _video_stream_index; - _video_stream->codec = _video_codec_context; - - _audio_stream->id = _audio_stream_index; - _audio_stream->codec = _audio_codec_context; - - if (avcodec_open2 (_video_codec_context, _video_codec, &_video_options) < 0) { - throw runtime_error ("could not open FFmpeg video codec"); - } - - int r = avcodec_open2 (_audio_codec_context, _audio_codec, 0); - if (r < 0) { - char buffer[256]; - av_strerror (r, buffer, sizeof(buffer)); - throw runtime_error (String::compose ("could not open FFmpeg audio codec (%1)", buffer)); - } - - if (avio_open_boost (&_format_context->pb, _output, AVIO_FLAG_WRITE) < 0) { - throw runtime_error ("could not open FFmpeg output file"); - } - - if (avformat_write_header (_format_context, 0) < 0) { - throw runtime_error ("could not write header to FFmpeg output file"); - } - { shared_ptr job = _job.lock (); DCPOMATIC_ASSERT (job); @@ -208,7 +98,20 @@ FFmpegEncoder::go () shared_ptr deinterleaved (new AudioBuffers (_output_audio_channels, audio_frames)); for (DCPTime i; i < _film->length(); i += video_frame) { pair, DCPTime> v = _butler->get_video (); - video (v.first, v.second); + _file_encoder.video (v.first, v.second); + + _history.event (); + + { + boost::mutex::scoped_lock lm (_mutex); + _last_time = i; + } + + shared_ptr job = _job.lock (); + if (job) { + job->set_progress (float(i.get()) / _film->length().get()); + } + _butler->get_audio (interleaved, audio_frames); /* XXX: inefficient; butler interleaves and we deinterleave again */ float* p = interleaved; @@ -217,204 +120,11 @@ FFmpegEncoder::go () deinterleaved->data(k)[j] = *p++; } } - audio (deinterleaved); + _file_encoder.audio (deinterleaved); } delete[] interleaved; - if (_pending_audio->frames() > 0) { - audio_frame (_pending_audio->frames ()); - } - - /* Flush */ - - bool flushed_video = false; - bool flushed_audio = false; - - while (!flushed_video || !flushed_audio) { - AVPacket packet; - av_init_packet (&packet); - packet.data = 0; - packet.size = 0; - - int got_packet; - avcodec_encode_video2 (_video_codec_context, &packet, 0, &got_packet); - if (got_packet) { - packet.stream_index = 0; - av_interleaved_write_frame (_format_context, &packet); - } else { - flushed_video = true; - } - av_packet_unref (&packet); - - av_init_packet (&packet); - packet.data = 0; - packet.size = 0; - - avcodec_encode_audio2 (_audio_codec_context, &packet, 0, &got_packet); - if (got_packet) { - packet.stream_index = 0; - av_interleaved_write_frame (_format_context, &packet); - } else { - flushed_audio = true; - } - av_packet_unref (&packet); - } - - av_write_trailer (_format_context); - - avcodec_close (_video_codec_context); - avcodec_close (_audio_codec_context); - avio_close (_format_context->pb); - avformat_free_context (_format_context); -} - -void -FFmpegEncoder::video (shared_ptr video, DCPTime time) -{ - shared_ptr image = video->image ( - bind (&Log::dcp_log, _film->log().get(), _1, _2), - bind (&force_pixel_format, _1, _pixel_format), - true, - false - ); - - AVFrame* frame = av_frame_alloc (); - DCPOMATIC_ASSERT (frame); - - _pending_images[image->data()[0]] = image; - for (int i = 0; i < 3; ++i) { - AVBufferRef* buffer = av_buffer_create(image->data()[i], image->stride()[i] * image->size().height, &buffer_free, this, 0); - frame->buf[i] = av_buffer_ref (buffer); - frame->data[i] = buffer->data; - frame->linesize[i] = image->stride()[i]; - av_buffer_unref (&buffer); - } - - frame->width = image->size().width; - frame->height = image->size().height; - frame->format = _pixel_format; - frame->pts = time.seconds() / av_q2d (_video_stream->time_base); - - AVPacket packet; - av_init_packet (&packet); - packet.data = 0; - packet.size = 0; - - int got_packet; - if (avcodec_encode_video2 (_video_codec_context, &packet, frame, &got_packet) < 0) { - throw EncodeError ("FFmpeg video encode failed"); - } - - if (got_packet && packet.size) { - packet.stream_index = _video_stream_index; - av_interleaved_write_frame (_format_context, &packet); - av_packet_unref (&packet); - } - - av_frame_free (&frame); - - _history.event (); - - { - boost::mutex::scoped_lock lm (_mutex); - _last_time = time; - } - - shared_ptr job = _job.lock (); - if (job) { - job->set_progress (float(time.get()) / _film->length().get()); - } -} - -/** Called when the player gives us some audio */ -void -FFmpegEncoder::audio (shared_ptr audio) -{ - _pending_audio->append (audio); - - int frame_size = _audio_codec_context->frame_size; - if (frame_size == 0) { - /* codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE */ - frame_size = _film->audio_frame_rate() / _film->video_frame_rate(); - } - - while (_pending_audio->frames() >= frame_size) { - audio_frame (frame_size); - } -} - -void -FFmpegEncoder::audio_frame (int size) -{ - DCPOMATIC_ASSERT (size); - - AVFrame* frame = av_frame_alloc (); - DCPOMATIC_ASSERT (frame); - - int const channels = _pending_audio->channels(); - DCPOMATIC_ASSERT (channels); - - int const buffer_size = av_samples_get_buffer_size (0, channels, size, _audio_codec_context->sample_fmt, 0); - DCPOMATIC_ASSERT (buffer_size >= 0); - - void* samples = av_malloc (buffer_size); - DCPOMATIC_ASSERT (samples); - - frame->nb_samples = size; - int r = avcodec_fill_audio_frame (frame, channels, _audio_codec_context->sample_fmt, (const uint8_t *) samples, buffer_size, 0); - DCPOMATIC_ASSERT (r >= 0); - - float** p = _pending_audio->data (); - switch (_audio_codec_context->sample_fmt) { - case AV_SAMPLE_FMT_S16: - { - int16_t* q = reinterpret_cast (samples); - for (int i = 0; i < size; ++i) { - for (int j = 0; j < channels; ++j) { - *q++ = p[j][i] * 32767; - } - } - break; - } - case AV_SAMPLE_FMT_FLTP: - { - float* q = reinterpret_cast (samples); - for (int i = 0; i < channels; ++i) { - memcpy (q, p[i], sizeof(float) * size); - q += size; - } - break; - } - default: - DCPOMATIC_ASSERT (false); - } - - AVPacket packet; - av_init_packet (&packet); - packet.data = 0; - packet.size = 0; - - int got_packet; - if (avcodec_encode_audio2 (_audio_codec_context, &packet, frame, &got_packet) < 0) { - throw EncodeError ("FFmpeg audio encode failed"); - } - - if (got_packet && packet.size) { - packet.stream_index = _audio_stream_index; - av_interleaved_write_frame (_format_context, &packet); - av_packet_unref (&packet); - } - - av_free (samples); - av_frame_free (&frame); - - _pending_audio->trim_start (size); -} - -void -FFmpegEncoder::subtitle (PlayerText, DCPTimePeriod) -{ - + _file_encoder.flush (); } float @@ -429,15 +139,3 @@ FFmpegEncoder::frames_done () const boost::mutex::scoped_lock lm (_mutex); return _last_time.frames_round (_film->video_frame_rate ()); } - -void -FFmpegEncoder::buffer_free (void* opaque, uint8_t* data) -{ - reinterpret_cast(opaque)->buffer_free2(data); -} - -void -FFmpegEncoder::buffer_free2 (uint8_t* data) -{ - _pending_images.erase (data); -} diff --git a/src/lib/ffmpeg_encoder.h b/src/lib/ffmpeg_encoder.h index 37e45a888..98c4704e2 100644 --- a/src/lib/ffmpeg_encoder.h +++ b/src/lib/ffmpeg_encoder.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2017 Carl Hetherington + Copyright (C) 2017-2018 Carl Hetherington This file is part of DCP-o-matic. @@ -24,24 +24,14 @@ #include "encoder.h" #include "event_history.h" #include "audio_mapping.h" -extern "C" { -#include -#include -} -#include +#include "ffmpeg_file_encoder.h" class Butler; class FFmpegEncoder : public Encoder { public: - enum Format - { - FORMAT_PRORES, - FORMAT_H264 - }; - - FFmpegEncoder (boost::shared_ptr film, boost::weak_ptr job, boost::filesystem::path output, Format format, bool mixdown_to_stereo, int x264_crf); + FFmpegEncoder (boost::shared_ptr film, boost::weak_ptr job, boost::filesystem::path output, ExportFormat format, bool mixdown_to_stereo, int x264_crf); void go (); @@ -52,30 +42,7 @@ public: } private: - void video (boost::shared_ptr, DCPTime); - void audio (boost::shared_ptr); - void subtitle (PlayerText, DCPTimePeriod); - - void setup_video (); - void setup_audio (); - - void audio_frame (int size); - - static void buffer_free(void* opaque, uint8_t* data); - void buffer_free2(uint8_t* data); - - AVCodec* _video_codec; - AVCodecContext* _video_codec_context; - AVCodec* _audio_codec; - AVCodecContext* _audio_codec_context; - AVFormatContext* _format_context; - AVStream* _video_stream; - AVStream* _audio_stream; - AVPixelFormat _pixel_format; - AVSampleFormat _sample_format; - AVDictionary* _video_options; - std::string _video_codec_name; - std::string _audio_codec_name; + FFmpegFileEncoder _file_encoder; int _output_audio_channels; mutable boost::mutex _mutex; @@ -83,19 +50,7 @@ private: EventHistory _history; - boost::filesystem::path _output; - - boost::shared_ptr _pending_audio; - boost::shared_ptr _butler; - - /** Store of shared_ptr to keep them alive whilst raw pointers into - their data have been passed to FFmpeg. - */ - std::map > _pending_images; - - static int _video_stream_index; - static int _audio_stream_index; }; #endif diff --git a/src/lib/ffmpeg_file_encoder.cc b/src/lib/ffmpeg_file_encoder.cc new file mode 100644 index 000000000..97a342e29 --- /dev/null +++ b/src/lib/ffmpeg_file_encoder.cc @@ -0,0 +1,377 @@ +/* + Copyright (C) 2017-2018 Carl Hetherington + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see . + +*/ + +#include "ffmpeg_encoder.h" +#include "film.h" +#include "job.h" +#include "player.h" +#include "player_video.h" +#include "log.h" +#include "image.h" +#include "cross.h" +#include "butler.h" +#include "compose.hpp" +#include + +#include "i18n.h" + +using std::string; +using std::runtime_error; +using std::cout; +using std::pair; +using boost::shared_ptr; +using boost::bind; +using boost::weak_ptr; + +int FFmpegFileEncoder::_video_stream_index = 0; +int FFmpegFileEncoder::_audio_stream_index = 1; + +static AVPixelFormat +force_pixel_format (AVPixelFormat, AVPixelFormat out) +{ + return out; +} + +FFmpegFileEncoder::FFmpegFileEncoder ( + dcp::Size video_frame_size, + int video_frame_rate, + int audio_frame_rate, + int channels, + shared_ptr log, + ExportFormat format, + int x264_crf, + boost::filesystem::path output + ) + : _video_options (0) + , _audio_channels (channels) + , _output (output) + , _video_frame_size (video_frame_size) + , _video_frame_rate (video_frame_rate) + , _audio_frame_rate (audio_frame_rate) + , _log (log) +{ + switch (format) { + case EXPORT_FORMAT_PRORES: + _pixel_format = AV_PIX_FMT_YUV422P10; + _sample_format = AV_SAMPLE_FMT_S16; + _video_codec_name = "prores_ks"; + _audio_codec_name = "pcm_s16le"; + av_dict_set (&_video_options, "profile", "3", 0); + av_dict_set (&_video_options, "threads", "auto", 0); + break; + case EXPORT_FORMAT_H264: + _pixel_format = AV_PIX_FMT_YUV420P; + _sample_format = AV_SAMPLE_FMT_FLTP; + _video_codec_name = "libx264"; + _audio_codec_name = "aac"; + av_dict_set_int (&_video_options, "crf", x264_crf, 0); + break; + } + + setup_video (); + setup_audio (); + + avformat_alloc_output_context2 (&_format_context, 0, 0, _output.string().c_str()); + if (!_format_context) { + throw runtime_error ("could not allocate FFmpeg format context"); + } + + _video_stream = avformat_new_stream (_format_context, _video_codec); + if (!_video_stream) { + throw runtime_error ("could not create FFmpeg output video stream"); + } + + _audio_stream = avformat_new_stream (_format_context, _audio_codec); + if (!_audio_stream) { + throw runtime_error ("could not create FFmpeg output audio stream"); + } + + _video_stream->id = _video_stream_index; + _video_stream->codec = _video_codec_context; + + _audio_stream->id = _audio_stream_index; + _audio_stream->codec = _audio_codec_context; + + if (avcodec_open2 (_video_codec_context, _video_codec, &_video_options) < 0) { + throw runtime_error ("could not open FFmpeg video codec"); + } + + int r = avcodec_open2 (_audio_codec_context, _audio_codec, 0); + if (r < 0) { + char buffer[256]; + av_strerror (r, buffer, sizeof(buffer)); + throw runtime_error (String::compose ("could not open FFmpeg audio codec (%1)", buffer)); + } + + if (avio_open_boost (&_format_context->pb, _output, AVIO_FLAG_WRITE) < 0) { + throw runtime_error ("could not open FFmpeg output file"); + } + + if (avformat_write_header (_format_context, 0) < 0) { + throw runtime_error ("could not write header to FFmpeg output file"); + } + + _pending_audio.reset (new AudioBuffers(channels, 0)); +} + +void +FFmpegFileEncoder::setup_video () +{ + _video_codec = avcodec_find_encoder_by_name (_video_codec_name.c_str()); + if (!_video_codec) { + throw runtime_error (String::compose ("could not find FFmpeg encoder %1", _video_codec_name)); + } + + _video_codec_context = avcodec_alloc_context3 (_video_codec); + if (!_video_codec_context) { + throw runtime_error ("could not allocate FFmpeg video context"); + } + + avcodec_get_context_defaults3 (_video_codec_context, _video_codec); + + /* Variable quantisation */ + _video_codec_context->global_quality = 0; + _video_codec_context->width = _video_frame_size.width; + _video_codec_context->height = _video_frame_size.height; + _video_codec_context->time_base = (AVRational) { 1, _video_frame_rate }; + _video_codec_context->pix_fmt = _pixel_format; + _video_codec_context->flags |= AV_CODEC_FLAG_QSCALE | AV_CODEC_FLAG_GLOBAL_HEADER; +} + +void +FFmpegFileEncoder::setup_audio () +{ + _audio_codec = avcodec_find_encoder_by_name (_audio_codec_name.c_str()); + if (!_audio_codec) { + throw runtime_error (String::compose ("could not find FFmpeg encoder %1", _audio_codec_name)); + } + + _audio_codec_context = avcodec_alloc_context3 (_audio_codec); + if (!_audio_codec_context) { + throw runtime_error ("could not allocate FFmpeg audio context"); + } + + avcodec_get_context_defaults3 (_audio_codec_context, _audio_codec); + + /* XXX: configurable */ + _audio_codec_context->bit_rate = 256 * 1024; + _audio_codec_context->sample_fmt = _sample_format; + _audio_codec_context->sample_rate = _audio_frame_rate; + _audio_codec_context->channel_layout = av_get_default_channel_layout (_audio_channels); + _audio_codec_context->channels = _audio_channels; +} + +void +FFmpegFileEncoder::flush () +{ + if (_pending_audio->frames() > 0) { + audio_frame (_pending_audio->frames ()); + } + + bool flushed_video = false; + bool flushed_audio = false; + + while (!flushed_video || !flushed_audio) { + AVPacket packet; + av_init_packet (&packet); + packet.data = 0; + packet.size = 0; + + int got_packet; + avcodec_encode_video2 (_video_codec_context, &packet, 0, &got_packet); + if (got_packet) { + packet.stream_index = 0; + av_interleaved_write_frame (_format_context, &packet); + } else { + flushed_video = true; + } + av_packet_unref (&packet); + + av_init_packet (&packet); + packet.data = 0; + packet.size = 0; + + avcodec_encode_audio2 (_audio_codec_context, &packet, 0, &got_packet); + if (got_packet) { + packet.stream_index = 0; + av_interleaved_write_frame (_format_context, &packet); + } else { + flushed_audio = true; + } + av_packet_unref (&packet); + } + + av_write_trailer (_format_context); + + avcodec_close (_video_codec_context); + avcodec_close (_audio_codec_context); + avio_close (_format_context->pb); + avformat_free_context (_format_context); +} + +void +FFmpegFileEncoder::video (shared_ptr video, DCPTime time) +{ + shared_ptr image = video->image ( + bind (&Log::dcp_log, _log.get(), _1, _2), + bind (&force_pixel_format, _1, _pixel_format), + true, + false + ); + + AVFrame* frame = av_frame_alloc (); + DCPOMATIC_ASSERT (frame); + + _pending_images[image->data()[0]] = image; + for (int i = 0; i < 3; ++i) { + AVBufferRef* buffer = av_buffer_create(image->data()[i], image->stride()[i] * image->size().height, &buffer_free, this, 0); + frame->buf[i] = av_buffer_ref (buffer); + frame->data[i] = buffer->data; + frame->linesize[i] = image->stride()[i]; + av_buffer_unref (&buffer); + } + + frame->width = image->size().width; + frame->height = image->size().height; + frame->format = _pixel_format; + frame->pts = time.seconds() / av_q2d (_video_stream->time_base); + + AVPacket packet; + av_init_packet (&packet); + packet.data = 0; + packet.size = 0; + + int got_packet; + if (avcodec_encode_video2 (_video_codec_context, &packet, frame, &got_packet) < 0) { + throw EncodeError ("FFmpeg video encode failed"); + } + + if (got_packet && packet.size) { + packet.stream_index = _video_stream_index; + av_interleaved_write_frame (_format_context, &packet); + av_packet_unref (&packet); + } + + av_frame_free (&frame); + +} + +/** Called when the player gives us some audio */ +void +FFmpegFileEncoder::audio (shared_ptr audio) +{ + _pending_audio->append (audio); + + int frame_size = _audio_codec_context->frame_size; + if (frame_size == 0) { + /* codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE */ + frame_size = _audio_frame_rate / _video_frame_rate; + } + + while (_pending_audio->frames() >= frame_size) { + audio_frame (frame_size); + } +} + +void +FFmpegFileEncoder::audio_frame (int size) +{ + DCPOMATIC_ASSERT (size); + + AVFrame* frame = av_frame_alloc (); + DCPOMATIC_ASSERT (frame); + + int const channels = _pending_audio->channels(); + DCPOMATIC_ASSERT (channels); + + int const buffer_size = av_samples_get_buffer_size (0, channels, size, _audio_codec_context->sample_fmt, 0); + DCPOMATIC_ASSERT (buffer_size >= 0); + + void* samples = av_malloc (buffer_size); + DCPOMATIC_ASSERT (samples); + + frame->nb_samples = size; + int r = avcodec_fill_audio_frame (frame, channels, _audio_codec_context->sample_fmt, (const uint8_t *) samples, buffer_size, 0); + DCPOMATIC_ASSERT (r >= 0); + + float** p = _pending_audio->data (); + switch (_audio_codec_context->sample_fmt) { + case AV_SAMPLE_FMT_S16: + { + int16_t* q = reinterpret_cast (samples); + for (int i = 0; i < size; ++i) { + for (int j = 0; j < channels; ++j) { + *q++ = p[j][i] * 32767; + } + } + break; + } + case AV_SAMPLE_FMT_FLTP: + { + float* q = reinterpret_cast (samples); + for (int i = 0; i < channels; ++i) { + memcpy (q, p[i], sizeof(float) * size); + q += size; + } + break; + } + default: + DCPOMATIC_ASSERT (false); + } + + AVPacket packet; + av_init_packet (&packet); + packet.data = 0; + packet.size = 0; + + int got_packet; + if (avcodec_encode_audio2 (_audio_codec_context, &packet, frame, &got_packet) < 0) { + throw EncodeError ("FFmpeg audio encode failed"); + } + + if (got_packet && packet.size) { + packet.stream_index = _audio_stream_index; + av_interleaved_write_frame (_format_context, &packet); + av_packet_unref (&packet); + } + + av_free (samples); + av_frame_free (&frame); + + _pending_audio->trim_start (size); +} + +void +FFmpegFileEncoder::subtitle (PlayerText, DCPTimePeriod) +{ + +} + +void +FFmpegFileEncoder::buffer_free (void* opaque, uint8_t* data) +{ + reinterpret_cast(opaque)->buffer_free2(data); +} + +void +FFmpegFileEncoder::buffer_free2 (uint8_t* data) +{ + _pending_images.erase (data); +} diff --git a/src/lib/ffmpeg_file_encoder.h b/src/lib/ffmpeg_file_encoder.h new file mode 100644 index 000000000..803125bd3 --- /dev/null +++ b/src/lib/ffmpeg_file_encoder.h @@ -0,0 +1,93 @@ +/* + Copyright (C) 2017-2018 Carl Hetherington + + This file is part of DCP-o-matic. + + DCP-o-matic 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. + + DCP-o-matic 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 DCP-o-matic. If not, see . + +*/ + +#ifndef DCPOMATIC_FFMPEG_FILE_ENCODER_H +#define DCPOMATIC_FFMPEG_FILE_ENCODER_H + +#include "encoder.h" +#include "event_history.h" +#include "audio_mapping.h" +#include "log.h" +extern "C" { +#include +#include +} + +class FFmpegFileEncoder +{ +public: + FFmpegFileEncoder ( + dcp::Size video_frame_size, + int video_frame_rate, + int audio_frame_rate, + int channels, + boost::shared_ptr log, + ExportFormat, + int x264_crf, + boost::filesystem::path output + ); + + void video (boost::shared_ptr, DCPTime); + void audio (boost::shared_ptr); + void subtitle (PlayerText, DCPTimePeriod); + + void flush (); + +private: + void setup_video (); + void setup_audio (); + + void audio_frame (int size); + + static void buffer_free(void* opaque, uint8_t* data); + void buffer_free2(uint8_t* data); + + AVCodec* _video_codec; + AVCodecContext* _video_codec_context; + AVCodec* _audio_codec; + AVCodecContext* _audio_codec_context; + AVFormatContext* _format_context; + AVStream* _video_stream; + AVStream* _audio_stream; + AVPixelFormat _pixel_format; + AVSampleFormat _sample_format; + AVDictionary* _video_options; + std::string _video_codec_name; + std::string _audio_codec_name; + int _audio_channels; + + boost::filesystem::path _output; + dcp::Size _video_frame_size; + int _video_frame_rate; + int _audio_frame_rate; + boost::shared_ptr _log; + + boost::shared_ptr _pending_audio; + + /** Store of shared_ptr to keep them alive whilst raw pointers into + their data have been passed to FFmpeg. + */ + std::map > _pending_images; + + static int _video_stream_index; + static int _audio_stream_index; +}; + +#endif diff --git a/src/lib/types.h b/src/lib/types.h index 22d652b3f..607c9e275 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -161,6 +161,12 @@ extern std::string text_type_to_string (TextType t); extern std::string text_type_to_name (TextType t); extern TextType string_to_text_type (std::string s); +enum ExportFormat +{ + EXPORT_FORMAT_PRORES, + EXPORT_FORMAT_H264 +}; + /** @struct Crop * @brief A description of the crop of an image or video. */ diff --git a/src/lib/wscript b/src/lib/wscript index 5c1da8a5f..609ddb322 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -88,6 +88,7 @@ sources = """ ffmpeg_content.cc ffmpeg_decoder.cc ffmpeg_encoder.cc + ffmpeg_file_encoder.cc ffmpeg_examiner.cc ffmpeg_stream.cc ffmpeg_subtitle_stream.cc diff --git a/src/wx/export_dialog.cc b/src/wx/export_dialog.cc index 481408578..f75150e45 100644 --- a/src/wx/export_dialog.cc +++ b/src/wx/export_dialog.cc @@ -43,9 +43,9 @@ wxString format_extensions[] = { "mp4" }; -FFmpegEncoder::Format formats[] = { - FFmpegEncoder::FORMAT_PRORES, - FFmpegEncoder::FORMAT_H264, +ExportFormat formats[] = { + EXPORT_FORMAT_PRORES, + EXPORT_FORMAT_H264, }; ExportDialog::ExportDialog (wxWindow* parent) @@ -110,7 +110,7 @@ ExportDialog::path () const return wx_to_std (fn.GetFullPath()); } -FFmpegEncoder::Format +ExportFormat ExportDialog::format () const { DCPOMATIC_ASSERT (_format->GetSelection() >= 0 && _format->GetSelection() < FORMATS); diff --git a/src/wx/export_dialog.h b/src/wx/export_dialog.h index c6ccc875e..d6e3bdbb6 100644 --- a/src/wx/export_dialog.h +++ b/src/wx/export_dialog.h @@ -31,7 +31,7 @@ public: explicit ExportDialog (wxWindow* parent); boost::filesystem::path path () const; - FFmpegEncoder::Format format () const; + ExportFormat format () const; bool mixdown_to_stereo () const; int x264_crf () const; diff --git a/test/ffmpeg_encoder_test.cc b/test/ffmpeg_encoder_test.cc index 8bb813713..d7bb6c29b 100644 --- a/test/ffmpeg_encoder_test.cc +++ b/test/ffmpeg_encoder_test.cc @@ -37,16 +37,16 @@ using std::string; using boost::shared_ptr; static void -ffmpeg_content_test (int number, boost::filesystem::path content, FFmpegEncoder::Format format) +ffmpeg_content_test (int number, boost::filesystem::path content, ExportFormat format) { string name = "ffmpeg_encoder_"; string extension; switch (format) { - case FFmpegEncoder::FORMAT_H264: + case EXPORT_FORMAT_H264: name += "h264"; extension = "mp4"; break; - case FFmpegEncoder::FORMAT_PRORES: + case EXPORT_FORMAT_PRORES: name += "prores"; extension = "mov"; break; @@ -72,25 +72,25 @@ ffmpeg_content_test (int number, boost::filesystem::path content, FFmpegEncoder: /** Red / green / blue MP4 -> Prores */ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test1) { - ffmpeg_content_test (1, "test/data/test.mp4", FFmpegEncoder::FORMAT_PRORES); + ffmpeg_content_test (1, "test/data/test.mp4", EXPORT_FORMAT_PRORES); } /** Dolby Aurora trailer VOB -> Prores */ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test2) { - ffmpeg_content_test (2, private_data / "dolby_aurora.vob", FFmpegEncoder::FORMAT_PRORES); + ffmpeg_content_test (2, private_data / "dolby_aurora.vob", EXPORT_FORMAT_PRORES); } /** Sintel trailer -> Prores */ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test3) { - ffmpeg_content_test (3, private_data / "Sintel_Trailer1.480p.DivX_Plus_HD.mkv", FFmpegEncoder::FORMAT_PRORES); + ffmpeg_content_test (3, private_data / "Sintel_Trailer1.480p.DivX_Plus_HD.mkv", EXPORT_FORMAT_PRORES); } /** Big Buck Bunny trailer -> Prores */ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test4) { - ffmpeg_content_test (4, private_data / "big_buck_bunny_trailer_480p.mov", FFmpegEncoder::FORMAT_PRORES); + ffmpeg_content_test (4, private_data / "big_buck_bunny_trailer_480p.mov", EXPORT_FORMAT_PRORES); } /** Still image -> Prores */ @@ -109,7 +109,7 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test5) film->write_metadata (); shared_ptr job (new TranscodeJob (film)); - FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_test5.mov", FFmpegEncoder::FORMAT_PRORES, false, 23); + FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_test5.mov", EXPORT_FORMAT_PRORES, false, 23); encoder.go (); } @@ -130,7 +130,7 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test6) film->write_metadata(); shared_ptr job (new TranscodeJob (film)); - FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_test6.mov", FFmpegEncoder::FORMAT_PRORES, false, 23); + FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_test6.mov", EXPORT_FORMAT_PRORES, false, 23); encoder.go (); } @@ -154,14 +154,14 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_prores_test7) s->only_text()->set_effect_colour (dcp::Colour (0, 255, 255)); shared_ptr job (new TranscodeJob (film)); - FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_test7.mov", FFmpegEncoder::FORMAT_PRORES, false, 23); + FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_prores_test7.mov", EXPORT_FORMAT_PRORES, false, 23); encoder.go (); } /** Red / green / blue MP4 -> H264 */ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test1) { - ffmpeg_content_test(1, "test/data/test.mp4", FFmpegEncoder::FORMAT_H264); + ffmpeg_content_test(1, "test/data/test.mp4", EXPORT_FORMAT_H264); } /** Just subtitles -> H264 */ @@ -181,7 +181,7 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test2) film->write_metadata(); shared_ptr job (new TranscodeJob (film)); - FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_h264_test2.mp4", FFmpegEncoder::FORMAT_H264, false, 23); + FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_h264_test2.mp4", EXPORT_FORMAT_H264, false, 23); encoder.go (); } @@ -206,7 +206,7 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test3) film->write_metadata(); shared_ptr job (new TranscodeJob (film)); - FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_h264_test3.mp4", FFmpegEncoder::FORMAT_H264, false, 23); + FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_h264_test3.mp4", EXPORT_FORMAT_H264, false, 23); encoder.go (); } @@ -220,7 +220,7 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test4) film->set_container(Ratio::from_id("185")); shared_ptr job(new TranscodeJob(film)); - FFmpegEncoder encoder(film, job, "build/test/ffmpeg_encoder_h264_test4.mp4", FFmpegEncoder::FORMAT_H264, false, 23); + FFmpegEncoder encoder(film, job, "build/test/ffmpeg_encoder_h264_test4.mp4", EXPORT_FORMAT_H264, false, 23); encoder.go(); } @@ -274,7 +274,7 @@ BOOST_AUTO_TEST_CASE (ffmpeg_encoder_h264_test5) Rs->audio->set_mapping (map); shared_ptr job (new TranscodeJob (film)); - FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_h264_test5.mp4", FFmpegEncoder::FORMAT_H264, true, 23); + FFmpegEncoder encoder (film, job, "build/test/ffmpeg_encoder_h264_test5.mp4", EXPORT_FORMAT_H264, true, 23); encoder.go (); check_ffmpeg ("build/test/ffmpeg_encoder_h264_test5.mp4", "test/data/ffmpeg_encoder_h264_test5.mp4", 1); -- 2.30.2