X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=modules%2Fffmpeg%2Fconsumer%2Fffmpeg_consumer.cpp;h=0b38701253151cb00c20dd797d9284f0c5d01dae;hb=b17a5351a13075978b5e5d4c817fb683224d9d38;hp=4569723fe64292465a23779c80375a44a483d840;hpb=b0422bf99c1534c5518c9b3b91b4365a09883642;p=casparcg diff --git a/modules/ffmpeg/consumer/ffmpeg_consumer.cpp b/modules/ffmpeg/consumer/ffmpeg_consumer.cpp index 4569723fe..0b3870125 100644 --- a/modules/ffmpeg/consumer/ffmpeg_consumer.cpp +++ b/modules/ffmpeg/consumer/ffmpeg_consumer.cpp @@ -1,487 +1,1317 @@ -/* -* Copyright (c) 2011 Sveriges Television AB -* -* This file is part of CasparCG (www.casparcg.com). -* -* CasparCG 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 3 of the License, or -* (at your option) any later version. -* -* CasparCG 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 CasparCG. If not, see . -* -* Author: Robert Nagy, ronag89@gmail.com -*/ - -#include "../StdAfx.h" - -#include "../ffmpeg_error.h" - -#include "ffmpeg_consumer.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#if defined(_MSC_VER) -#pragma warning (push) -#pragma warning (disable : 4244) -#endif -extern "C" -{ - #define __STDC_CONSTANT_MACROS - #define __STDC_LIMIT_MACROS - #include - #include - #include -} -#if defined(_MSC_VER) -#pragma warning (pop) -#endif - -namespace caspar { namespace ffmpeg { - -struct ffmpeg_consumer : boost::noncopyable -{ - const std::wstring filename_; - - const std::shared_ptr oc_; - const core::video_format_desc format_desc_; - - const spl::shared_ptr graph_; - boost::timer frame_timer_; - boost::timer write_timer_; - - executor executor_; - executor file_write_executor_; - - // Audio - std::shared_ptr audio_st_; - - // Video - std::shared_ptr video_st_; - - std::vector video_outbuf_; - std::vector picture_buf_; - std::shared_ptr sws_; - - int64_t frame_number_; - -public: - ffmpeg_consumer(const std::wstring& filename, const core::video_format_desc& format_desc, const std::wstring& codec, const std::vector& options) - : filename_(filename) - , video_outbuf_(1920*1080*8) - , oc_(avformat_alloc_context(), av_free) - , format_desc_(format_desc) - , executor_(print()) - , file_write_executor_(print() + L"/output") - , frame_number_(0) - { - // TODO: Ask stakeholders about case where file already exists. - boost::filesystem::remove(boost::filesystem::path(env::media_folder() + filename)); // Delete the file if it exists - - graph_->set_color("frame-time", diagnostics::color(0.1f, 1.0f, 0.1f)); - graph_->set_color("write-time", diagnostics::color(0.5f, 0.5f, 0.1f)); - graph_->set_text(print()); - diagnostics::register_graph(graph_); - - executor_.set_capacity(8); - file_write_executor_.set_capacity(8); - - oc_->oformat = av_guess_format(nullptr, u8(filename_).c_str(), nullptr); - if (!oc_->oformat) - BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("Could not find suitable output format.")); - - THROW_ON_ERROR2(av_set_parameters(oc_.get(), nullptr), "[ffmpeg_consumer]"); - - strcpy_s(oc_->filename, u8(filename_).c_str()); - - auto video_codec = avcodec_find_encoder_by_name(u8(codec).c_str()); - if(video_codec == nullptr) - BOOST_THROW_EXCEPTION(invalid_argument() << arg_name_info(codec)); - - // Add the audio and video streams using the default format codecs and initialize the codecs . - video_st_ = add_video_stream(video_codec->id, options); - audio_st_ = add_audio_stream(); - - dump_format(oc_.get(), 0, u8(filename_).c_str(), 1); - - // Open the output ffmpeg, if needed. - if (!(oc_->oformat->flags & AVFMT_NOFILE)) - THROW_ON_ERROR2(avio_open(&oc_->pb, u8(filename_).c_str(), URL_WRONLY), "[ffmpeg_consumer]"); - - THROW_ON_ERROR2(av_write_header(oc_.get()), "[ffmpeg_consumer]"); - } - - ~ffmpeg_consumer() - { - executor_.wait(); - executor_.stop(); - executor_.join(); - - file_write_executor_.wait(); - file_write_executor_.stop(); - file_write_executor_.join(); - - LOG_ON_ERROR2(av_write_trailer(oc_.get()), "[ffmpeg_consumer]"); - - audio_st_.reset(); - video_st_.reset(); - - if (!(oc_->oformat->flags & AVFMT_NOFILE)) - LOG_ON_ERROR2(avio_close(oc_->pb), "[ffmpeg_consumer]"); // Close the output ffmpeg. - } - - std::wstring print() const - { - return L"ffmpeg[" + filename_ + L"]"; - } - - std::shared_ptr add_video_stream(enum CodecID codec_id, const std::vector& options) - { - auto st = av_new_stream(oc_.get(), 0); - if (!st) - BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("Could not allocate video-stream") << boost::errinfo_api_function("av_new_stream")); - - auto encoder = avcodec_find_encoder(codec_id); - if (!encoder) - BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("codec not found")); - - auto c = st->codec; - - avcodec_get_context_defaults3(c, encoder); - - c->codec_id = codec_id; - c->codec_type = AVMEDIA_TYPE_VIDEO; - c->width = format_desc_.width; - c->height = format_desc_.height; - c->time_base.den = format_desc_.time_scale; - c->time_base.num = format_desc_.duration; - c->gop_size = 25; - c->flags |= format_desc_.field_mode == core::field_mode::progressive ? 0 : (CODEC_FLAG_INTERLACED_ME | CODEC_FLAG_INTERLACED_DCT); - - if(c->codec_id == CODEC_ID_PRORES) - { - c->bit_rate = format_desc_.width < 1280 ? 63*1000000 : 220*1000000; - c->pix_fmt = PIX_FMT_YUV422P10; - } - else if(c->codec_id == CODEC_ID_DNXHD) - { - if(format_desc_.width < 1280 || format_desc_.height < 720) - BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("unsupported dimension")); - - c->bit_rate = 220*1000000; - c->pix_fmt = PIX_FMT_YUV422P; - } - else if(c->codec_id == CODEC_ID_DVVIDEO) - { - c->bit_rate = format_desc_.width < 1280 ? 50*1000000 : 100*1000000; - c->pix_fmt = PIX_FMT_YUV422P; - - c->width = format_desc_.height == 1280 ? 960 : c->width; - - if(format_desc_.duration == 1001) - c->width = format_desc_.height == 1080 ? 1280 : c->width; - else - c->width = format_desc_.height == 1080 ? 1440 : c->width; - } - else if(c->codec_id == CODEC_ID_H264) - { - c->pix_fmt = PIX_FMT_YUV420P; - if(options.empty()) - { - av_opt_set(c->priv_data, "preset", "ultrafast", 0); - av_opt_set(c->priv_data, "tune", "fastdecode", 0); - av_opt_set(c->priv_data, "crf", "5", 0); - } - } - else if(c->codec_id == CODEC_ID_QTRLE) - { - c->pix_fmt = PIX_FMT_ARGB; - } - else - { - BOOST_THROW_EXCEPTION(invalid_argument() << msg_info("Unsupported output parameters.")); - } - - c->max_b_frames = 0; // b-frames not supported. - - for(size_t n = 0; n < options.size()/2; ++n) - THROW_ON_ERROR2(av_opt_set(c, u8(options[n*2+0]).c_str(), u8(options[n*2+1]).c_str(), AV_OPT_SEARCH_CHILDREN), "[ffmpeg_consumer]"); - - if(oc_->oformat->flags & AVFMT_GLOBALHEADER) - c->flags |= CODEC_FLAG_GLOBAL_HEADER; - - c->thread_count = boost::thread::hardware_concurrency(); - THROW_ON_ERROR2(avcodec_open(c, encoder), "[ffmpeg_consumer]"); - - return std::shared_ptr(st, [](AVStream* st) - { - LOG_ON_ERROR2(avcodec_close(st->codec), "[ffmpeg_consumer]"); - av_freep(&st->codec); - av_freep(&st); - }); - } - - std::shared_ptr add_audio_stream() - { - auto st = av_new_stream(oc_.get(), 1); - if(!st) - BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("Could not allocate audio-stream") << boost::errinfo_api_function("av_new_stream")); - - st->codec->codec_id = CODEC_ID_PCM_S16LE; - st->codec->codec_type = AVMEDIA_TYPE_AUDIO; - st->codec->sample_rate = 48000; - st->codec->channels = 2; - st->codec->sample_fmt = SAMPLE_FMT_S16; - - if(oc_->oformat->flags & AVFMT_GLOBALHEADER) - st->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; - - auto codec = avcodec_find_encoder(st->codec->codec_id); - if (!codec) - BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("codec not found")); - - THROW_ON_ERROR2(avcodec_open(st->codec, codec), "[ffmpeg_consumer]"); - - return std::shared_ptr(st, [](AVStream* st) - { - LOG_ON_ERROR2(avcodec_close(st->codec), "[ffmpeg_consumer]");; - av_freep(&st->codec); - av_freep(&st); - }); - } - - std::shared_ptr convert_video_frame(const spl::shared_ptr& frame, AVCodecContext* c) - { - if(!sws_) - { - sws_.reset(sws_getContext(format_desc_.width, format_desc_.height, PIX_FMT_BGRA, c->width, c->height, c->pix_fmt, SWS_BICUBIC, nullptr, nullptr, nullptr), sws_freeContext); - if (sws_ == nullptr) - BOOST_THROW_EXCEPTION(caspar_exception() << msg_info("Cannot initialize the conversion context")); - } - - std::shared_ptr av_frame(avcodec_alloc_frame(), av_free); - avpicture_fill(reinterpret_cast(av_frame.get()), const_cast(frame->image_data().begin()), PIX_FMT_BGRA, format_desc_.width, format_desc_.height); - - std::shared_ptr local_av_frame(avcodec_alloc_frame(), av_free); - picture_buf_.resize(avpicture_get_size(c->pix_fmt, format_desc_.width, format_desc_.height)); - avpicture_fill(reinterpret_cast(local_av_frame.get()), picture_buf_.data(), c->pix_fmt, format_desc_.width, format_desc_.height); - - sws_scale(sws_.get(), av_frame->data, av_frame->linesize, 0, c->height, local_av_frame->data, local_av_frame->linesize); - - return local_av_frame; - } - - std::shared_ptr encode_video_frame(const spl::shared_ptr& frame) - { - auto c = video_st_->codec; - - auto av_frame = convert_video_frame(frame, c); - av_frame->interlaced_frame = format_desc_.field_mode != core::field_mode::progressive; - av_frame->top_field_first = format_desc_.field_mode == core::field_mode::upper; - av_frame->pts = frame_number_++; - - int out_size = THROW_ON_ERROR2(avcodec_encode_video(c, video_outbuf_.data(), static_cast(video_outbuf_.size()), av_frame.get()), "[ffmpeg_consumer]"); - if(out_size > 0) - { - spl::shared_ptr pkt(new AVPacket, [](AVPacket* p) - { - av_free_packet(p); - delete p; - }); - av_init_packet(pkt.get()); - - if (c->coded_frame->pts != AV_NOPTS_VALUE) - pkt->pts = av_rescale_q(c->coded_frame->pts, c->time_base, video_st_->time_base); - - if(c->coded_frame->key_frame) - pkt->flags |= AV_PKT_FLAG_KEY; - - pkt->stream_index = video_st_->index; - pkt->data = video_outbuf_.data(); - pkt->size = out_size; - - av_dup_packet(pkt.get()); - return pkt; - } - return nullptr; - } - - std::shared_ptr encode_audio_frame(const spl::shared_ptr& frame) - { - auto c = audio_st_->codec; - - auto audio_data = core::audio_32_to_16(frame->audio_data()); - - spl::shared_ptr pkt(new AVPacket, [](AVPacket* p) - { - av_free_packet(p); - delete p; - }); - av_init_packet(pkt.get()); - - if (c->coded_frame && c->coded_frame->pts != AV_NOPTS_VALUE) - pkt->pts = av_rescale_q(c->coded_frame->pts, c->time_base, audio_st_->time_base); - - pkt->flags |= AV_PKT_FLAG_KEY; - pkt->stream_index = audio_st_->index; - pkt->size = static_cast(audio_data.size()*2); - pkt->data = reinterpret_cast(audio_data.data()); - - av_dup_packet(pkt.get()); - return pkt; - } - - void send(const spl::shared_ptr& frame) - { - executor_.begin_invoke([=] - { - frame_timer_.restart(); - - auto video = encode_video_frame(frame); - auto audio = encode_audio_frame(frame); - - graph_->set_value("frame-time", frame_timer_.elapsed()*format_desc_.fps*0.5); - - file_write_executor_.begin_invoke([=] - { - write_timer_.restart(); - - if(video) - av_write_frame(oc_.get(), video.get()); - if(audio) - av_write_frame(oc_.get(), audio.get()); - - graph_->set_value("write-time", write_timer_.elapsed()*format_desc_.fps*0.5); - }); - }); - } -}; - -struct ffmpeg_consumer_proxy : public core::frame_consumer -{ - const std::wstring filename_; - const bool key_only_; - const std::wstring codec_; - const std::vector options_; - - std::unique_ptr consumer_; - -public: - - ffmpeg_consumer_proxy(const std::wstring& filename, bool key_only, const std::wstring& codec, const std::vector& options) - : filename_(filename) - , key_only_(key_only) - , codec_(std::move(codec)) - , options_(options) - { - } - - virtual void initialize(const core::video_format_desc& format_desc, int) - { - consumer_.reset(); - consumer_.reset(new ffmpeg_consumer(filename_, format_desc, codec_, options_)); - } - - virtual bool send(const spl::shared_ptr& frame) override - { - consumer_->send(frame); - return true; - } - - virtual std::wstring print() const override - { - return consumer_ ? consumer_->print() : L"[ffmpeg_consumer]"; - } - - virtual boost::property_tree::wptree info() const override - { - boost::property_tree::wptree info; - info.add(L"type", L"ffmpeg-consumer"); - info.add(L"key-only", key_only_); - info.add(L"filename", filename_); - info.add(L"vcodec", codec_); - return info; - } - - virtual bool has_synchronization_clock() const override - { - return false; - } - - virtual int buffer_depth() const override - { - return 1; - } - - virtual int index() const override - { - return 200; - } -}; - -spl::shared_ptr create_consumer(const std::vector& params) -{ - if(params.size() < 1 || params[0] != L"FILE") - return core::frame_consumer::empty(); - - auto filename = (params.size() > 1 ? params[1] : L""); - bool key_only = get_param(L"KEY_ONLY", params, false); - auto codec = get_param(L"-VCODEC", params, get_param(L"CODEC", params, L"libx264")); - - std::vector options; - auto opt_it = std::find(params.begin(), params.end(), L"-VCODEC"); - - if(std::distance(opt_it, params.end()) > 2) - { - std::advance(opt_it, 2); - while(opt_it != params.end()) - { - auto str = *opt_it++; - if(str.size() > 0 && str.at(0) == L'-') - str = str.substr(1); - options.push_back(boost::to_lower_copy(str)); - } - } - - if(codec == L"H264") - codec = L"libx264"; - - if(codec == L"DVCPRO") - codec = L"dvvideo"; - - codec = boost::to_lower_copy(codec); - - return spl::make_shared(env::media_folder() + filename, key_only, codec, options); -} - -spl::shared_ptr create_consumer(const boost::property_tree::wptree& ptree) -{ - auto filename = ptree.get(L"path"); - auto key_only = ptree.get(L"key-only", false); - auto codec = ptree.get(L"vcodec", L"libx264"); - - return spl::make_shared(env::media_folder() + filename, key_only, codec, std::vector()); -} - -}} +#include "../StdAfx.h" + +#include "ffmpeg_consumer.h" + +#include "../ffmpeg_error.h" +#include "../producer/util/util.h" +#include "../producer/filter/filter.h" +#include "../producer/filter/audio_filter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#pragma warning(push) +#pragma warning(disable: 4244) +#pragma warning(disable: 4245) +#include +#pragma warning(pop) + +#include +#include +#include +#include + +#include + +#pragma warning(push) +#pragma warning(disable: 4244) + +extern "C" +{ + #define __STDC_CONSTANT_MACROS + #define __STDC_LIMIT_MACROS + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +} + +#pragma warning(pop) + +namespace caspar { namespace ffmpeg { + +void set_pixel_format(AVFilterContext* sink, AVPixelFormat pix_fmt) +{ +#pragma warning (push) +#pragma warning (disable : 4245) + + FF(av_opt_set_int_list( + sink, + "pix_fmts", + std::vector({ pix_fmt, AVPixelFormat::AV_PIX_FMT_NONE }).data(), + -1, + AV_OPT_SEARCH_CHILDREN)); + +#pragma warning (pop) +} + +void adjust_video_filter(const AVCodec& codec, const core::video_format_desc& in_format, AVFilterContext* sink, std::string& filter) +{ + switch (codec.id) + { + case AV_CODEC_ID_DVVIDEO: + // Crop + if (in_format.format == core::video_format::ntsc) + filter = u8(append_filter(u16(filter), L"crop=720:480:0:2")); + + // Pixel format selection + if (in_format.format == core::video_format::ntsc) + set_pixel_format(sink, AVPixelFormat::AV_PIX_FMT_YUV411P); + else if (in_format.format == core::video_format::pal) + set_pixel_format(sink, AVPixelFormat::AV_PIX_FMT_YUV420P); + else + set_pixel_format(sink, AVPixelFormat::AV_PIX_FMT_YUV422P); + + // Scale + if (in_format.height == 1080) + filter = u8(append_filter(u16(filter), in_format.duration == 1001 + ? L"scale=1280:1080" + : L"scale=1440:1080")); + else if (in_format.height == 720) + filter = u8(append_filter(u16(filter), L"scale=960:720")); + + break; + } +} + +void setup_codec_defaults(AVCodecContext& encoder) +{ + static const int MEGABIT = 1000000; + + switch (encoder.codec_id) + { + case AV_CODEC_ID_DNXHD: + encoder.bit_rate = 220 * MEGABIT; + + break; + case AV_CODEC_ID_PRORES: + encoder.bit_rate = encoder.width < 1280 + ? 63 * MEGABIT + : 220 * MEGABIT; + + break; + case AV_CODEC_ID_H264: + av_opt_set(encoder.priv_data, "preset", "ultrafast", 0); + av_opt_set(encoder.priv_data, "tune", "fastdecode", 0); + av_opt_set(encoder.priv_data, "crf", "5", 0); + + break; + } +} + +bool is_pcm_s24le_not_supported(const AVFormatContext& container) +{ + auto name = std::string(container.oformat->name); + + if (name == "mp4" || name == "dv") + return true; + + return false; +} + +template +std::vector from_terminated_array(const In* array, In terminator) +{ + std::vector result; + + while (array != nullptr && *array != terminator) + { + In val = *array; + Out casted = static_cast(val); + + result.push_back(casted); + + ++array; + } + + return result; +} + +class ffmpeg_consumer +{ +private: + const spl::shared_ptr graph_; + core::monitor::subject subject_; + std::string path_; + boost::filesystem::path full_path_; + + std::map options_; + bool mono_streams_; + + core::video_format_desc in_video_format_; + core::audio_channel_layout in_channel_layout_ = core::audio_channel_layout::invalid(); + + std::shared_ptr oc_; + tbb::atomic abort_request_; + + std::shared_ptr video_st_; + std::vector> audio_sts_; + + std::int64_t video_pts_ = 0; + std::int64_t audio_pts_ = 0; + + std::unique_ptr audio_filter_; + + // TODO: make use of already existent avfilter abstraction for video also + AVFilterContext* video_graph_in_; + AVFilterContext* video_graph_out_; + std::shared_ptr video_graph_; + + executor video_encoder_executor_; + executor audio_encoder_executor_; + + semaphore tokens_ { 0 }; + + tbb::atomic current_encoding_delay_; + + executor write_executor_; + +public: + + ffmpeg_consumer( + std::string path, + std::string options, + bool mono_streams) + : path_(path) + , full_path_(path) + , mono_streams_(mono_streams) + , audio_encoder_executor_(print() + L" audio_encoder") + , video_encoder_executor_(print() + L" video_encoder") + , write_executor_(print() + L" io") + { + abort_request_ = false; + current_encoding_delay_ = 0; + + for(auto it = + boost::sregex_iterator( + options.begin(), + options.end(), + boost::regex("-(?[^-\\s]+)(\\s+(?[^\\s]+))?")); + it != boost::sregex_iterator(); + ++it) + { + options_[(*it)["NAME"].str()] = (*it)["VALUE"].matched ? (*it)["VALUE"].str() : ""; + } + + if (options_.find("threads") == options_.end()) + options_["threads"] = "auto"; + + tokens_.release( + std::max( + 1, + try_remove_arg( + options_, + boost::regex("tokens")).get_value_or(2))); + } + + ~ffmpeg_consumer() + { + if(oc_) + { + video_encoder_executor_.begin_invoke([&] { encode_video(core::const_frame::empty(), nullptr); }); + audio_encoder_executor_.begin_invoke([&] { encode_audio(core::const_frame::empty(), nullptr); }); + + video_encoder_executor_.stop(); + audio_encoder_executor_.stop(); + video_encoder_executor_.join(); + audio_encoder_executor_.join(); + + video_graph_.reset(); + audio_filter_.reset(); + video_st_.reset(); + audio_sts_.clear(); + + write_packet(nullptr, nullptr); + + write_executor_.stop(); + write_executor_.join(); + + FF(av_write_trailer(oc_.get())); + + if (!(oc_->oformat->flags & AVFMT_NOFILE) && oc_->pb) + avio_close(oc_->pb); + + oc_.reset(); + } + } + + void initialize( + const core::video_format_desc& format_desc, + const core::audio_channel_layout& channel_layout) + { + try + { + static boost::regex prot_exp("^.+:.*" ); + + if(!boost::regex_match( + path_, + prot_exp)) + { + if(!full_path_.is_complete()) + { + full_path_ = + u8( + env::media_folder()) + + path_; + } + + if(boost::filesystem::exists(full_path_)) + boost::filesystem::remove(full_path_); + + boost::filesystem::create_directories(full_path_.parent_path()); + } + + graph_->set_color("frame-time", diagnostics::color(0.1f, 1.0f, 0.1f)); + graph_->set_color("dropped-frame", diagnostics::color(0.3f, 0.6f, 0.3f)); + graph_->set_text(print()); + diagnostics::register_graph(graph_); + + const auto oformat_name = + try_remove_arg( + options_, + boost::regex("^f|format$")); + + AVFormatContext* oc; + + FF(avformat_alloc_output_context2( + &oc, + nullptr, + oformat_name && !oformat_name->empty() ? oformat_name->c_str() : nullptr, + full_path_.string().c_str())); + + oc_.reset( + oc, + avformat_free_context); + + CASPAR_VERIFY(oc_->oformat); + + oc_->interrupt_callback.callback = ffmpeg_consumer::interrupt_cb; + oc_->interrupt_callback.opaque = this; + + CASPAR_VERIFY(format_desc.format != core::video_format::invalid); + + in_video_format_ = format_desc; + in_channel_layout_ = channel_layout; + + CASPAR_VERIFY(oc_->oformat); + + const auto video_codec_name = + try_remove_arg( + options_, + boost::regex("^c:v|codec:v|vcodec$")); + + const auto video_codec = + video_codec_name + ? avcodec_find_encoder_by_name(video_codec_name->c_str()) + : avcodec_find_encoder(oc_->oformat->video_codec); + + const auto audio_codec_name = + try_remove_arg( + options_, + boost::regex("^c:a|codec:a|acodec$")); + + const auto audio_codec = + audio_codec_name + ? avcodec_find_encoder_by_name(audio_codec_name->c_str()) + : (is_pcm_s24le_not_supported(*oc_) + ? avcodec_find_encoder(oc_->oformat->audio_codec) + : avcodec_find_encoder_by_name("pcm_s24le")); + + if (!video_codec) + CASPAR_THROW_EXCEPTION(user_error() << msg_info( + "Failed to find video codec " + (video_codec_name + ? *video_codec_name + : "with id " + boost::lexical_cast( + oc_->oformat->video_codec)))); + if (!audio_codec) + CASPAR_THROW_EXCEPTION(user_error() << msg_info( + "Failed to find audio codec " + (audio_codec_name + ? *audio_codec_name + : "with id " + boost::lexical_cast( + oc_->oformat->audio_codec)))); + + // Filters + + { + configure_video_filters( + *video_codec, + try_remove_arg(options_, + boost::regex("vf|f:v|filter:v")).get_value_or("")); + + configure_audio_filters( + *audio_codec, + try_remove_arg(options_, + boost::regex("af|f:a|filter:a")).get_value_or("")); + } + + // Encoders + + { + auto video_options = options_; + auto audio_options = options_; + + video_st_ = open_encoder( + *video_codec, + video_options, + 0); + + for (int i = 0; i < audio_filter_->get_num_output_pads(); ++i) + audio_sts_.push_back(open_encoder( + *audio_codec, + audio_options, + i)); + + auto it = options_.begin(); + while(it != options_.end()) + { + if(video_options.find(it->first) == video_options.end() || audio_options.find(it->first) == audio_options.end()) + it = options_.erase(it); + else + ++it; + } + } + + // Output + { + AVDictionary* av_opts = nullptr; + + to_dict( + &av_opts, + std::move(options_)); + + CASPAR_SCOPE_EXIT + { + av_dict_free(&av_opts); + }; + + if (!(oc_->oformat->flags & AVFMT_NOFILE)) + { + FF(avio_open2( + &oc_->pb, + full_path_.string().c_str(), + AVIO_FLAG_WRITE, + &oc_->interrupt_callback, + &av_opts)); + } + + FF(avformat_write_header( + oc_.get(), + &av_opts)); + + options_ = to_map(av_opts); + } + + // Dump Info + + av_dump_format( + oc_.get(), + 0, + oc_->filename, + 1); + + for (const auto& option : options_) + { + CASPAR_LOG(warning) + << L"Invalid option: -" + << u16(option.first) + << L" " + << u16(option.second); + } + } + catch(...) + { + video_st_.reset(); + audio_sts_.clear(); + oc_.reset(); + throw; + } + } + + core::monitor::subject& monitor_output() + { + return subject_; + } + + void send(core::const_frame frame) + { + CASPAR_VERIFY(in_video_format_.format != core::video_format::invalid); + + auto frame_timer = spl::make_shared(); + + std::shared_ptr token( + nullptr, + [this, frame, frame_timer](void*) + { + tokens_.release(); + current_encoding_delay_ = frame.get_age_millis(); + graph_->set_value("frame-time", frame_timer->elapsed() * in_video_format_.fps * 0.5); + }); + tokens_.acquire(); + + video_encoder_executor_.begin_invoke([=]() mutable + { + encode_video( + frame, + token); + }); + + audio_encoder_executor_.begin_invoke([=]() mutable + { + encode_audio( + frame, + token); + }); + } + + bool ready_for_frame() const + { + return tokens_.permits() > 0; + } + + void mark_dropped() + { + graph_->set_tag(diagnostics::tag_severity::WARNING, "dropped-frame"); + } + + std::wstring print() const + { + return L"ffmpeg_consumer[" + u16(path_) + L"]"; + } + + int64_t presentation_frame_age_millis() const + { + return current_encoding_delay_; + } + +private: + + static int interrupt_cb(void* ctx) + { + CASPAR_ASSERT(ctx); + return reinterpret_cast(ctx)->abort_request_; + } + + std::shared_ptr open_encoder( + const AVCodec& codec, + std::map& options, + int stream_number_for_media_type) + { + auto st = + avformat_new_stream( + oc_.get(), + &codec); + + if (!st) + CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info("Could not allocate video-stream.") << boost::errinfo_api_function("avformat_new_stream")); + + auto enc = st->codec; + + CASPAR_VERIFY(enc); + + switch(enc->codec_type) + { + case AVMEDIA_TYPE_VIDEO: + { + enc->time_base = video_graph_out_->inputs[0]->time_base; + enc->pix_fmt = static_cast(video_graph_out_->inputs[0]->format); + enc->sample_aspect_ratio = st->sample_aspect_ratio = video_graph_out_->inputs[0]->sample_aspect_ratio; + enc->width = video_graph_out_->inputs[0]->w; + enc->height = video_graph_out_->inputs[0]->h; + enc->bit_rate_tolerance = 400 * 1000000; + + break; + } + case AVMEDIA_TYPE_AUDIO: + { + enc->time_base = audio_filter_->get_output_pad_info(stream_number_for_media_type).time_base; + enc->sample_fmt = static_cast(audio_filter_->get_output_pad_info(stream_number_for_media_type).format); + enc->sample_rate = audio_filter_->get_output_pad_info(stream_number_for_media_type).sample_rate; + enc->channel_layout = audio_filter_->get_output_pad_info(stream_number_for_media_type).channel_layout; + enc->channels = audio_filter_->get_output_pad_info(stream_number_for_media_type).channels; + + break; + } + } + + setup_codec_defaults(*enc); + + if(oc_->oformat->flags & AVFMT_GLOBALHEADER) + enc->flags |= CODEC_FLAG_GLOBAL_HEADER; + + static const std::array char_id_map = {{"v", "a", "d", "s"}}; + + const auto char_id = char_id_map.at(enc->codec_type); + + const auto codec_opts = + remove_options( + options, + boost::regex("^(" + char_id + "?[^:]+):" + char_id + "$")); + + AVDictionary* av_codec_opts = nullptr; + + to_dict( + &av_codec_opts, + options); + + to_dict( + &av_codec_opts, + codec_opts); + + options.clear(); + + FF(avcodec_open2( + enc, + &codec, + av_codec_opts ? &av_codec_opts : nullptr)); + + if(av_codec_opts) + { + auto t = + av_dict_get( + av_codec_opts, + "", + nullptr, + AV_DICT_IGNORE_SUFFIX); + + while(t) + { + options[t->key + (codec_opts.find(t->key) != codec_opts.end() ? ":" + char_id : "")] = t->value; + + t = av_dict_get( + av_codec_opts, + "", + t, + AV_DICT_IGNORE_SUFFIX); + } + + av_dict_free(&av_codec_opts); + } + + if(enc->codec_type == AVMEDIA_TYPE_AUDIO && !(codec.capabilities & CODEC_CAP_VARIABLE_FRAME_SIZE)) + { + CASPAR_ASSERT(enc->frame_size > 0); + audio_filter_->set_guaranteed_output_num_samples_per_frame( + stream_number_for_media_type, + enc->frame_size); + } + + return std::shared_ptr(st, [this](AVStream* st) + { + avcodec_close(st->codec); + }); + } + + void configure_video_filters( + const AVCodec& codec, + std::string filtergraph) + { + video_graph_.reset( + avfilter_graph_alloc(), + [](AVFilterGraph* p) + { + avfilter_graph_free(&p); + }); + + video_graph_->nb_threads = boost::thread::hardware_concurrency()/2; + video_graph_->thread_type = AVFILTER_THREAD_SLICE; + + const auto sample_aspect_ratio = + boost::rational( + in_video_format_.square_width, + in_video_format_.square_height) / + boost::rational( + in_video_format_.width, + in_video_format_.height); + + const auto vsrc_options = (boost::format("video_size=%1%x%2%:pix_fmt=%3%:time_base=%4%/%5%:pixel_aspect=%6%/%7%:frame_rate=%8%/%9%") + % in_video_format_.width % in_video_format_.height + % AVPixelFormat::AV_PIX_FMT_BGRA + % in_video_format_.duration % in_video_format_.time_scale + % sample_aspect_ratio.numerator() % sample_aspect_ratio.denominator() + % in_video_format_.time_scale % in_video_format_.duration).str(); + + AVFilterContext* filt_vsrc = nullptr; + FF(avfilter_graph_create_filter( + &filt_vsrc, + avfilter_get_by_name("buffer"), + "ffmpeg_consumer_buffer", + vsrc_options.c_str(), + nullptr, + video_graph_.get())); + + AVFilterContext* filt_vsink = nullptr; + FF(avfilter_graph_create_filter( + &filt_vsink, + avfilter_get_by_name("buffersink"), + "ffmpeg_consumer_buffersink", + nullptr, + nullptr, + video_graph_.get())); + +#pragma warning (push) +#pragma warning (disable : 4245) + + FF(av_opt_set_int_list( + filt_vsink, + "pix_fmts", + codec.pix_fmts, + -1, + AV_OPT_SEARCH_CHILDREN)); + +#pragma warning (pop) + + adjust_video_filter(codec, in_video_format_, filt_vsink, filtergraph); + + configure_filtergraph( + *video_graph_, + filtergraph, + *filt_vsrc, + *filt_vsink); + + video_graph_in_ = filt_vsrc; + video_graph_out_ = filt_vsink; + + CASPAR_LOG(info) + << u16(std::string("\n") + + avfilter_graph_dump( + video_graph_.get(), + nullptr)); + } + + void configure_audio_filters( + const AVCodec& codec, + std::string filtergraph) + { + int num_output_pads = 1; + + if (mono_streams_) + { + num_output_pads = in_channel_layout_.num_channels; + } + + if (num_output_pads > 1) + { + std::string splitfilter = "[a:0]channelsplit=channel_layout="; + + splitfilter += (boost::format("0x%|1$x|") % create_channel_layout_bitmask(in_channel_layout_.num_channels)).str(); + + for (int i = 0; i < num_output_pads; ++i) + splitfilter += "[aout:" + boost::lexical_cast(i) + "]"; + + filtergraph = u8(append_filter(u16(filtergraph), u16(splitfilter))); + } + + std::vector output_pads( + num_output_pads, + audio_output_pad( + from_terminated_array( codec.supported_samplerates, 0), + from_terminated_array( codec.sample_fmts, AVSampleFormat::AV_SAMPLE_FMT_NONE), + from_terminated_array( codec.channel_layouts, 0ull))); + + audio_filter_.reset(new audio_filter( + { audio_input_pad( + boost::rational(1, in_video_format_.audio_sample_rate), + in_video_format_.audio_sample_rate, + AVSampleFormat::AV_SAMPLE_FMT_S32, + create_channel_layout_bitmask(in_channel_layout_.num_channels)) }, + output_pads, + filtergraph)); + } + + void configure_filtergraph( + AVFilterGraph& graph, + const std::string& filtergraph, + AVFilterContext& source_ctx, + AVFilterContext& sink_ctx) + { + AVFilterInOut* outputs = nullptr; + AVFilterInOut* inputs = nullptr; + + if(!filtergraph.empty()) + { + outputs = avfilter_inout_alloc(); + inputs = avfilter_inout_alloc(); + + try + { + CASPAR_VERIFY(outputs && inputs); + + outputs->name = av_strdup("in"); + outputs->filter_ctx = &source_ctx; + outputs->pad_idx = 0; + outputs->next = nullptr; + + inputs->name = av_strdup("out"); + inputs->filter_ctx = &sink_ctx; + inputs->pad_idx = 0; + inputs->next = nullptr; + } + catch (...) + { + avfilter_inout_free(&outputs); + avfilter_inout_free(&inputs); + throw; + } + + FF(avfilter_graph_parse( + &graph, + filtergraph.c_str(), + inputs, + outputs, + nullptr)); + } + else + { + FF(avfilter_link( + &source_ctx, + 0, + &sink_ctx, + 0)); + } + + FF(avfilter_graph_config( + &graph, + nullptr)); + } + + void encode_video(core::const_frame frame_ptr, std::shared_ptr token) + { + if(!video_st_) + return; + + auto enc = video_st_->codec; + + if(frame_ptr != core::const_frame::empty()) + { + auto src_av_frame = create_frame(); + + const auto sample_aspect_ratio = + boost::rational( + in_video_format_.square_width, + in_video_format_.square_height) / + boost::rational( + in_video_format_.width, + in_video_format_.height); + + src_av_frame->format = AVPixelFormat::AV_PIX_FMT_BGRA; + src_av_frame->width = in_video_format_.width; + src_av_frame->height = in_video_format_.height; + src_av_frame->sample_aspect_ratio.num = sample_aspect_ratio.numerator(); + src_av_frame->sample_aspect_ratio.den = sample_aspect_ratio.denominator(); + src_av_frame->pts = video_pts_; + + video_pts_ += 1; + + subject_ + << core::monitor::message("/frame") % video_pts_ + << core::monitor::message("/path") % path_ + << core::monitor::message("/fps") % in_video_format_.fps; + + FF(av_image_fill_arrays( + src_av_frame->data, + src_av_frame->linesize, + frame_ptr.image_data().begin(), + static_cast(src_av_frame->format), + in_video_format_.width, + in_video_format_.height, + 1)); + + FF(av_buffersrc_add_frame( + video_graph_in_, + src_av_frame.get())); + } + + int ret = 0; + + while(ret >= 0) + { + auto filt_frame = create_frame(); + + ret = av_buffersink_get_frame( + video_graph_out_, + filt_frame.get()); + + video_encoder_executor_.begin_invoke([=] + { + if(ret == AVERROR_EOF) + { + if(enc->codec->capabilities & CODEC_CAP_DELAY) + { + while(encode_av_frame( + *video_st_, + avcodec_encode_video2, + nullptr, token)) + { + boost::this_thread::yield(); // TODO: + } + } + } + else if(ret != AVERROR(EAGAIN)) + { + FF_RET(ret, "av_buffersink_get_frame"); + + if (filt_frame->interlaced_frame) + { + if (enc->codec->id == AV_CODEC_ID_MJPEG) + enc->field_order = filt_frame->top_field_first ? AV_FIELD_TT : AV_FIELD_BB; + else + enc->field_order = filt_frame->top_field_first ? AV_FIELD_TB : AV_FIELD_BT; + } + else + enc->field_order = AV_FIELD_PROGRESSIVE; + + filt_frame->quality = enc->global_quality; + + if (!enc->me_threshold) + filt_frame->pict_type = AV_PICTURE_TYPE_NONE; + + encode_av_frame( + *video_st_, + avcodec_encode_video2, + filt_frame, + token); + + boost::this_thread::yield(); // TODO: + } + }); + } + } + + void encode_audio(core::const_frame frame_ptr, std::shared_ptr token) + { + if(audio_sts_.empty()) + return; + + if(frame_ptr != core::const_frame::empty()) + { + auto src_av_frame = create_frame(); + + src_av_frame->channels = in_channel_layout_.num_channels; + src_av_frame->channel_layout = create_channel_layout_bitmask(in_channel_layout_.num_channels); + src_av_frame->sample_rate = in_video_format_.audio_sample_rate; + src_av_frame->nb_samples = static_cast(frame_ptr.audio_data().size()) / src_av_frame->channels; + src_av_frame->format = AV_SAMPLE_FMT_S32; + src_av_frame->pts = audio_pts_; + + audio_pts_ += src_av_frame->nb_samples; + + FF(av_samples_fill_arrays( + src_av_frame->extended_data, + src_av_frame->linesize, + reinterpret_cast(&*frame_ptr.audio_data().begin()), + src_av_frame->channels, + src_av_frame->nb_samples, + static_cast(src_av_frame->format), + 16)); + + audio_filter_->push(0, src_av_frame); + } + + for (int pad_id = 0; pad_id < audio_filter_->get_num_output_pads(); ++pad_id) + { + for (auto filt_frame : audio_filter_->poll_all(pad_id)) + { + audio_encoder_executor_.begin_invoke([=] + { + encode_av_frame( + *audio_sts_.at(pad_id), + avcodec_encode_audio2, + filt_frame, + token); + + boost::this_thread::yield(); // TODO: + }); + } + } + + bool eof = frame_ptr == core::const_frame::empty(); + + if (eof) + { + audio_encoder_executor_.begin_invoke([=] + { + for (int pad_id = 0; pad_id < audio_filter_->get_num_output_pads(); ++pad_id) + { + auto enc = audio_sts_.at(pad_id)->codec; + + if (enc->codec->capabilities & CODEC_CAP_DELAY) + { + while (encode_av_frame( + *audio_sts_.at(pad_id), + avcodec_encode_audio2, + nullptr, + token)) + { + boost::this_thread::yield(); // TODO: + } + } + } + }); + } + } + + template + bool encode_av_frame( + AVStream& st, + const F& func, + const std::shared_ptr& src_av_frame, + std::shared_ptr token) + { + AVPacket pkt = {}; + av_init_packet(&pkt); + + int got_packet = 0; + + FF(func( + st.codec, + &pkt, + src_av_frame.get(), + &got_packet)); + + if(!got_packet || pkt.size <= 0) + return false; + + pkt.stream_index = st.index; + + if (pkt.pts != AV_NOPTS_VALUE) + { + pkt.pts = + av_rescale_q( + pkt.pts, + st.codec->time_base, + st.time_base); + } + + if (pkt.dts != AV_NOPTS_VALUE) + { + pkt.dts = + av_rescale_q( + pkt.dts, + st.codec->time_base, + st.time_base); + } + + pkt.duration = + static_cast( + av_rescale_q( + pkt.duration, + st.codec->time_base, st.time_base)); + + write_packet( + std::shared_ptr( + new AVPacket(pkt), + [](AVPacket* p) + { + av_free_packet(p); + delete p; + }), token); + + return true; + } + + void write_packet( + const std::shared_ptr& pkt_ptr, + std::shared_ptr token) + { + write_executor_.begin_invoke([this, pkt_ptr, token]() mutable + { + FF(av_interleaved_write_frame( + oc_.get(), + pkt_ptr.get())); + }); + } + + template + static boost::optional try_remove_arg( + std::map& options, + const boost::regex& expr) + { + for(auto it = options.begin(); it != options.end(); ++it) + { + if(boost::regex_search(it->first, expr)) + { + auto arg = it->second; + options.erase(it); + return boost::lexical_cast(arg); + } + } + + return boost::optional(); + } + + static std::map remove_options( + std::map& options, + const boost::regex& expr) + { + std::map result; + + auto it = options.begin(); + while(it != options.end()) + { + boost::smatch what; + if(boost::regex_search(it->first, what, expr)) + { + result[ + what.size() > 0 && what[1].matched + ? what[1].str() + : it->first] = it->second; + it = options.erase(it); + } + else + ++it; + } + + return result; + } + + static void to_dict(AVDictionary** dest, const std::map& c) + { + for (const auto& entry : c) + { + av_dict_set( + dest, + entry.first.c_str(), + entry.second.c_str(), 0); + } + } + + static std::map to_map(AVDictionary* dict) + { + std::map result; + + for(auto t = dict + ? av_dict_get( + dict, + "", + nullptr, + AV_DICT_IGNORE_SUFFIX) + : nullptr; + t; + t = av_dict_get( + dict, + "", + t, + AV_DICT_IGNORE_SUFFIX)) + { + result[t->key] = t->value; + } + + return result; + } +}; + +int crc16(const std::string& str) +{ + boost::crc_16_type result; + + result.process_bytes(str.data(), str.length()); + + return result.checksum(); +} + +struct ffmpeg_consumer_proxy : public core::frame_consumer +{ + const std::string path_; + const std::string options_; + const bool separate_key_; + const bool mono_streams_; + const bool compatibility_mode_; + int consumer_index_offset_; + + std::unique_ptr consumer_; + std::unique_ptr key_only_consumer_; + +public: + + ffmpeg_consumer_proxy(const std::string& path, const std::string& options, bool separate_key, bool mono_streams, bool compatibility_mode) + : path_(path) + , options_(options) + , separate_key_(separate_key) + , mono_streams_(mono_streams) + , compatibility_mode_(compatibility_mode) + , consumer_index_offset_(crc16(path)) + { + } + + void initialize(const core::video_format_desc& format_desc, const core::audio_channel_layout& channel_layout, int) override + { + if (consumer_) + CASPAR_THROW_EXCEPTION(invalid_operation() << msg_info("Cannot reinitialize ffmpeg-consumer.")); + + consumer_.reset(new ffmpeg_consumer(path_, options_, mono_streams_)); + consumer_->initialize(format_desc, channel_layout); + + if (separate_key_) + { + boost::filesystem::path fill_file(path_); + auto without_extension = u16(fill_file.parent_path().string() + "/" + fill_file.stem().string()); + auto key_file = without_extension + L"_A" + u16(fill_file.extension().string()); + + key_only_consumer_.reset(new ffmpeg_consumer(u8(key_file), options_, mono_streams_)); + key_only_consumer_->initialize(format_desc, channel_layout); + } + } + + int64_t presentation_frame_age_millis() const override + { + return consumer_ ? static_cast(consumer_->presentation_frame_age_millis()) : 0; + } + + std::future send(core::const_frame frame) override + { + bool ready_for_frame = consumer_->ready_for_frame(); + + if (ready_for_frame && separate_key_) + ready_for_frame = ready_for_frame && key_only_consumer_->ready_for_frame(); + + if (ready_for_frame) + { + consumer_->send(frame); + + if (separate_key_) + key_only_consumer_->send(frame.key_only()); + } + else + { + consumer_->mark_dropped(); + + if (separate_key_) + key_only_consumer_->mark_dropped(); + } + + return make_ready_future(true); + } + + std::wstring print() const override + { + return consumer_ ? consumer_->print() : L"[ffmpeg_consumer]"; + } + + std::wstring name() const override + { + return L"ffmpeg"; + } + + boost::property_tree::wptree info() const override + { + boost::property_tree::wptree info; + + info.add(L"type", L"ffmpeg"); + info.add(L"path", u16(path_)); + info.add(L"separate_key", separate_key_); + info.add(L"mono_streams", mono_streams_); + + return info; + } + + bool has_synchronization_clock() const override + { + return false; + } + + int buffer_depth() const override + { + return -1; + } + + int index() const override + { + return compatibility_mode_ ? 200 : 100000 + consumer_index_offset_; + } + + core::monitor::subject& monitor_output() override + { + return consumer_->monitor_output(); + } +}; + +void describe_ffmpeg_consumer(core::help_sink& sink, const core::help_repository& repo) +{ + sink.short_description(L"For streaming/recording the contents of a channel using FFmpeg."); + sink.syntax(L"FILE,STREAM [filename:string],[url:string] {-[ffmpeg_param1:string] [value1:string] {-[ffmpeg_param2:string] [value2:string] {...}}} {[separate_key:SEPARATE_KEY]} {[mono_streams:MONO_STREAMS]}"); + sink.para()->text(L"For recording or streaming the contents of a channel using FFmpeg"); + sink.definitions() + ->item(L"filename", L"The filename under the media folder including the extension (decides which kind of container format that will be used).") + ->item(L"url", L"If the filename is given in the form of an URL a network stream will be created instead of a file on disk.") + ->item(L"ffmpeg_paramX", L"A parameter supported by FFmpeg. For example vcodec or acodec etc.") + ->item(L"separate_key", L"If defined will create two files simultaneously -- One for fill and one for key (_A will be appended).") + ->item(L"mono_streams", L"If defined every audio channel will be written to its own audio stream."); + sink.para()->text(L"Examples:"); + sink.example(L">> ADD 1 FILE output.mov -vcodec dnxhd"); + sink.example(L">> ADD 1 FILE output.mov -vcodec prores"); + sink.example(L">> ADD 1 FILE output.mov -vcodec dvvideo"); + sink.example(L">> ADD 1 FILE output.mov -vcodec libx264 -preset ultrafast -tune fastdecode -crf 25"); + sink.example(L">> ADD 1 FILE output.mov -vcodec dnxhd SEPARATE_KEY", L"for creating output.mov with fill and output_A.mov with key/alpha"); + sink.example(L">> ADD 1 FILE output.mxf -vcodec dnxhd MONO_STREAMS", L"for creating output.mxf with every audio channel encoded in its own mono stream."); + sink.example(L">> ADD 1 STREAM udp://:9250 -format mpegts -vcodec libx264 -crf 25 -tune zerolatency -preset ultrafast", + L"for streaming over UDP instead of creating a local file."); +} + +spl::shared_ptr create_ffmpeg_consumer( + const std::vector& params, core::interaction_sink*, std::vector> channels) +{ + if (params.size() < 1 || (!boost::iequals(params.at(0), L"STREAM") && !boost::iequals(params.at(0), L"FILE"))) + return core::frame_consumer::empty(); + + auto params2 = params; + bool separate_key = get_and_consume_flag(L"SEPARATE_KEY", params2); + bool mono_streams = get_and_consume_flag(L"MONO_STREAMS", params2); + auto compatibility_mode = boost::iequals(params.at(0), L"FILE"); + auto path = u8(params2.size() > 1 ? params2.at(1) : L""); + auto args = u8(boost::join(params2, L" ")); + + return spl::make_shared(path, args, separate_key, mono_streams, compatibility_mode); +} + +spl::shared_ptr create_preconfigured_ffmpeg_consumer( + const boost::property_tree::wptree& ptree, core::interaction_sink*, std::vector> channels) +{ + return spl::make_shared( + u8(ptree_get(ptree, L"path")), + u8(ptree.get(L"args", L"")), + ptree.get(L"separate-key", false), + ptree.get(L"mono-streams", false), + false); +} + +}}