// Copyright (c) 2009 The Foundry Visionmongers Ltd. All Rights Reserved. #include "Build/fnBuild.h" #include "DDImage/DDString.h" #include "DDImage/Writer.h" #include "DDImage/Row.h" #include "DDImage/Knobs.h" extern "C" { #include #include #include } #if __WORDSIZE == 64 #define INT64_C(c) c ## L #else #define INT64_C(c) c ## LL #endif using namespace DD::Image; class ffmpegWriter : public Writer { private: enum WriterError { SUCCESS = 0, IGNORE_FINISH, CLEANUP }; public: explicit ffmpegWriter(Write* iop); ~ffmpegWriter(); virtual bool movie() const { return true; } void execute(); void finish(); void knobs(Knob_Callback f); static const Writer::Description d; private: void freeFormat(); bool isCodecBlacklisted(const char* name); private: AVCodecContext* avctxOptions_[CODEC_TYPE_NB]; AVFormatContext* avformatOptions_; AVStream* stream_; std::vector formatsLongNames_; std::vector formatsShortNames_; std::vector codecsLongNames_; std::vector codecsShortNames_; WriterError error_; // knobs variables float fps_; int format_; int codec_; int bitrate_; int bitrateTolerance_; int gopSize_; int bFrames_; int mbDecision_; }; static const char* codecBlacklist[] = { #ifdef FN_OS_WINDOWS "dv", "ogg", "mpjpeg", "ipod", "psp", "asf", "asf_stream", "dirac", "ffm", "h261", "h263", "h264", "image2", "3g2", "3gp", "RoQ", "swf", "rcv", "yuv4mpegpipe", #elif FN_OS_MAC "ogg", "mjpeg", "mpjpeg", "vc1", "dv", "ipod", "psp", "asf", "asf_stream", "h261", "h263", "h264", "image2", "RoQ", "swf", "3g2", "3gp", "rcv", "yuv4mpegpipe", #elif FN_OS_LINUX "ogg", "mjpeg", "mpjpeg", "vc1", "dv", "ipod", "psp", "asf", "asf_stream", "h261", "h263", "h264", "image2", "3g2", "3gp", "rcv", "yuv4mpegpipe", "RoQ", "swf", "dnxhd", #endif NULL }; ffmpegWriter::ffmpegWriter(Write* iop) : Writer(iop) , avformatOptions_(0) , stream_(0) , error_(IGNORE_FINISH) , fps_(25.0f) , format_(0) , codec_(0) , bitrate_(400000) , bitrateTolerance_(4000 * 10000) , gopSize_(12) , bFrames_(0) , mbDecision_(FF_MB_DECISION_SIMPLE) { av_log_set_level(AV_LOG_WARNING); av_register_all(); for (int i = 0; i < CODEC_TYPE_NB; ++i) avctxOptions_[i] = avcodec_alloc_context2(CodecType(i)); formatsLongNames_.push_back("default"); formatsShortNames_.push_back("default"); AVOutputFormat* fmt = av_oformat_next(NULL); while (fmt) { if (fmt->video_codec != CODEC_ID_NONE && !isCodecBlacklisted( fmt->name ) ) { if (fmt->long_name) { formatsLongNames_.push_back(std::string(fmt->long_name) + std::string(" (") + std::string(fmt->name) + std::string(")")); formatsShortNames_.push_back(fmt->name); } } fmt = av_oformat_next(fmt); } formatsShortNames_.push_back(0); codecsLongNames_.push_back("default"); codecsShortNames_.push_back("default"); AVCodec* c = av_codec_next(NULL); while (c) { if (c->type == CODEC_TYPE_VIDEO && c->encode && !isCodecBlacklisted( c->name ) ) { if (c->long_name) { codecsLongNames_.push_back(c->long_name); codecsShortNames_.push_back(c->name); } } c = av_codec_next(c); } codecsLongNames_.push_back(0); codecsShortNames_.push_back(0); } bool ffmpegWriter::isCodecBlacklisted(const char* name) { const char** iterator = codecBlacklist; while( *iterator != NULL ) { if (strncmp(name, *iterator, strlen(*iterator)) == 0) { return true; } ++iterator; } return false; } ffmpegWriter::~ffmpegWriter() { for (int i = 0; i < CODEC_TYPE_NB; ++i) av_free(avctxOptions_[i]); } void ffmpegWriter::execute() { error_ = IGNORE_FINISH; AVOutputFormat* fmt = 0; if (!format_) { fmt = guess_format(NULL, filename(), NULL); if (!fmt) { iop->critical("could not deduce output format from file extension"); return; } } else { fmt = guess_format(formatsShortNames_[format_], NULL, NULL); if (!fmt) { iop->critical("could not deduce output format"); return; } } if (!avformatOptions_) avformatOptions_ = av_alloc_format_context(); avformatOptions_->oformat = fmt; snprintf(avformatOptions_->filename, sizeof(avformatOptions_->filename), "%s", filename()); if (!stream_) { stream_ = av_new_stream(avformatOptions_, 0); if (!stream_) { iop->critical("out of memory"); return; } CodecID codecId = fmt->video_codec; if (codec_) { AVCodec* userCodec = avcodec_find_encoder_by_name(codecsShortNames_[codec_]); if (userCodec) codecId = userCodec->id; } AVCodec* videoCodec = avcodec_find_encoder(codecId); if (!videoCodec) { iop->critical("unable to find codec"); freeFormat(); return; } stream_->codec->codec_id = codecId; stream_->codec->codec_type = CODEC_TYPE_VIDEO; stream_->codec->bit_rate = bitrate_; stream_->codec->bit_rate_tolerance = bitrateTolerance_; stream_->codec->width = width(); stream_->codec->height = height(); // Bug 23953 // ffmpeg does a horrible job of converting floats to AVRational's // It adds 0.5 randomly and does some other stuff // To work around that, we just multiply the fps by what I think is a reasonable number to make it an int // and use the reasonable number as the numerator for the timebase. // Timebase is not the frame rate; it's the inverse of the framerate // So instead of doing 1/fps, we just set the numerator and denominator of the timebase directly. // The upshot is that this allows ffmpeg to properly do framerates of 23.78 (or 23.796, which is what the user really wants when they put that in). // // The code was this: //stream_->codec->time_base = av_d2q(1.0 / fps_, 100); const float CONVERSION_FACTOR = 1000.0f; stream_->codec->time_base.num = (int) CONVERSION_FACTOR; stream_->codec->time_base.den = (int) (fps_ * CONVERSION_FACTOR); // stream_->codec->gop_size = gopSize_; if (bFrames_) { stream_->codec->max_b_frames = bFrames_; stream_->codec->b_frame_strategy = 0; stream_->codec->b_quant_factor = 2.0; } stream_->codec->mb_decision = mbDecision_; PixelFormat pixFMT = PIX_FMT_YUV420P;; if( videoCodec->pix_fmts != NULL ) { pixFMT = *videoCodec->pix_fmts; } else { if( strcmp( fmt->name, "gif" ) == 0 ){ pixFMT = PIX_FMT_RGB24; } } stream_->codec->pix_fmt = pixFMT; // this is set to the first element of FMT a choice could be added if (!strcmp(avformatOptions_->oformat->name, "mp4") || !strcmp(avformatOptions_->oformat->name, "mov") || !strcmp(avformatOptions_->oformat->name, "3gp")) stream_->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; if (av_set_parameters(avformatOptions_, NULL) < 0) { iop->critical("unable to set parameters"); freeFormat(); return; } dump_format(avformatOptions_, 0, filename(), 1); if (avcodec_open(stream_->codec, videoCodec) < 0) { iop->critical("unable to open codec"); freeFormat(); return; } if (!(fmt->flags & AVFMT_NOFILE)) { if (url_fopen(&avformatOptions_->pb, filename(), URL_WRONLY) < 0) { iop->critical("unable to open file"); return; } } av_write_header(avformatOptions_); } error_ = CLEANUP; AVPicture picture; int picSize = avpicture_get_size(PIX_FMT_RGB24, width(), height()); uint8_t* buffer = (uint8_t*) av_malloc(picSize); avpicture_fill(&picture, buffer, PIX_FMT_RGB24, width(), height()); Row row(0, width()); input0().validate(); input0().request(0, 0, width(), height(), Mask_RGB, 1); for (int y = 0; y < height(); ++y) { get(y, 0, width(), Mask_RGB, row); if (iop->aborted()) return; for (Channel z = Chan_Red; z <= Chan_Blue; incr(z)) { const float* from = row[z]; to_byte(z - 1, picture.data[0] + (height() - y - 1) * picture.linesize[0] + z - 1, from, NULL, width(), 3); } } AVFrame* output = avcodec_alloc_frame(); avcodec_get_frame_defaults(output); picSize = avpicture_get_size(stream_->codec->pix_fmt, width(), height()); uint8_t* outBuffer = (uint8_t*) av_malloc(picSize); avpicture_fill((AVPicture*)output, outBuffer, stream_->codec->pix_fmt, width(), height()); img_convert((AVPicture*) output, stream_->codec->pix_fmt, &picture, PIX_FMT_RGB24, width(), height()); int ret = 0; if ((avformatOptions_->oformat->flags & AVFMT_RAWPICTURE) != 0) { AVPacket pkt; av_init_packet(&pkt); pkt.flags |= PKT_FLAG_KEY; pkt.stream_index = stream_->index; pkt.data = (uint8_t*) output; pkt.size = sizeof(AVPicture); ret = av_interleaved_write_frame(avformatOptions_, &pkt); } else { uint8_t* outbuf = (uint8_t*) av_malloc(picSize); ret = avcodec_encode_video(stream_->codec, outbuf, picSize, output); if (ret > 0) { AVPacket pkt; av_init_packet(&pkt); if (stream_->codec->coded_frame && static_cast(stream_->codec->coded_frame->pts) != AV_NOPTS_VALUE) pkt.pts = av_rescale_q(stream_->codec->coded_frame->pts, stream_->codec->time_base, stream_->time_base); if (stream_->codec->coded_frame && stream_->codec->coded_frame->key_frame) pkt.flags |= PKT_FLAG_KEY; pkt.stream_index = stream_->index; pkt.data = outbuf; pkt.size = ret; ret = av_interleaved_write_frame(avformatOptions_, &pkt); } av_free(outbuf); } av_free(outBuffer); av_free(buffer); av_free(output); if (ret) { iop->critical("error writing frame to file"); return; } error_ = SUCCESS; } void ffmpegWriter::finish() { if (error_ == IGNORE_FINISH) return; av_write_trailer(avformatOptions_); avcodec_close(stream_->codec); if (!(avformatOptions_->oformat->flags & AVFMT_NOFILE)) url_fclose(avformatOptions_->pb); freeFormat(); } void ffmpegWriter::knobs(Knob_Callback f) { static std::vector formatsAliases; formatsAliases.resize(formatsLongNames_.size()); for (int i = 0; i < static_cast(formatsLongNames_.size()); ++i) formatsAliases[i] = formatsLongNames_[i].c_str(); formatsAliases.push_back(0); Enumeration_knob(f, &format_, &formatsAliases[0], "format"); Float_knob(f, &fps_, IRange(0.0, 100.0f), "fps"); BeginClosedGroup(f, "Advanced"); Enumeration_knob(f, &codec_, &codecsLongNames_[0], "codec"); Int_knob(f, &bitrate_, IRange(0.0, 400000), "bitrate"); SetFlags(f, Knob::SLIDER | Knob::LOG_SLIDER); Int_knob(f, &bitrateTolerance_, IRange(0, 4000 * 10000), "bitrateTol", "bitrate tolerance"); SetFlags(f, Knob::SLIDER | Knob::LOG_SLIDER); Int_knob(f, &gopSize_, IRange(0, 30), "gopSize", "GOP size"); SetFlags(f, Knob::SLIDER | Knob::LOG_SLIDER); Int_knob(f, &bFrames_, IRange(0, 30), "bFrames", "B Frames"); SetFlags(f, Knob::SLIDER | Knob::LOG_SLIDER); static const char* mbDecisionTypes[] = { "FF_MB_DECISION_SIMPLE", "FF_MB_DECISION_BITS", "FF_MB_DECISION_RD", 0 }; Enumeration_knob(f, &mbDecision_, mbDecisionTypes, "mbDecision", "macro block decision mode"); EndGroup(f); } void ffmpegWriter::freeFormat() { for (int i = 0; i < static_cast(avformatOptions_->nb_streams); ++i) av_freep(&avformatOptions_->streams[i]); av_free(avformatOptions_); avformatOptions_ = 0; stream_ = 0; } static Writer* build(Write* iop) { return new ffmpegWriter(iop); } const Writer::Description ffmpegWriter::d("ffmpeg\0mov\0avi\0", build);