// Copyright (c) 2009 The Foundry Visionmongers Ltd. All Rights Reserved. #include "Build/fnBuild.h" #include "DDImage/DDString.h" #include "DDImage/Reader.h" #include "DDImage/Row.h" #ifdef _WIN32 #include #endif 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; namespace { const char* ffmpegError(int error) { switch (error) { case AVERROR_IO: return "I/O error"; case AVERROR_NUMEXPECTED: return "number syntax expected in filename"; case AVERROR_INVALIDDATA: return "invalid data found"; case AVERROR_NOMEM: return "not enough memory"; case AVERROR_NOFMT: return "unknown format"; case AVERROR_NOTSUPP: return "operation not supported"; case AVERROR_NOENT: return "no such file or directory"; case AVERROR_PATCHWELCOME: default: return "unknown error"; } } } class ffmpegReader : public Reader { public: explicit ffmpegReader(Read* iop); ~ffmpegReader(); virtual bool videosequence() const { return true; } void engine(int y, int x, int r, ChannelMask channels, Row& row); void open(); private: MetaData::Bundle meta; static const Reader::Description d; public: const MetaData::Bundle& fetchMetaData(const char* key) { return meta; } private: bool findStreamInfo(); bool hasVideo() const { return !videoIdx_.empty(); } AVStream* getVideoStream() { return currVideoIdx_ >= 0 ? context_->streams[currVideoIdx_] : NULL; } double fps() const; void openVideoCodec(); void closeVideoCodec(); int64_t getTimeStamp(int pos) const; bool seek(int pos); bool decodeImage(); int width() const { return width_; } int height() const { return height_; } // use internal frame query function, as ffmpeg uses 0-based frames for most codecs // so they might not be the same as Nuke's frames int ffmpegFrame() const; private: AVFormatContext* context_; AVInputFormat* format_; AVFormatParameters* params_; AVFrame* avFrame_; AVCodec* videoCodec_; AVPacket pkt_; AVCodecContext* avctxOptions[CODEC_TYPE_NB]; AVFormatContext* avformatOptions; std::vector videoIdx_; int fpsNum_; int fpsDen_; int currVideoIdx_; uint64_t frames_; int width_; int height_; double aspect_; std::vector data_; bool offsetTime_; int lastSearchPos_; int lastDecodedPos_; bool codecZeroBasedStart_; }; ffmpegReader::ffmpegReader(Read* iop) : Reader(iop) , context_(0) , format_(0) , params_(0) , avFrame_(0) , videoCodec_(0) , avformatOptions(0) , fpsNum_(0) , fpsDen_(0) , currVideoIdx_(-1) , frames_(0) , width_(720) , height_(576) , aspect_(1.0f) , offsetTime_(true) , lastSearchPos_(-1) , lastDecodedPos_(-1) , codecZeroBasedStart_(true) { 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)); avformatOptions = av_alloc_format_context(); avFrame_ = avcodec_alloc_frame(); // FIXME_GC: shouldn't the plugin be passed the filename without the prefix? int offset = 0; std::string filename(iop->filename()); if (filename.find("ffmpeg:") != std::string::npos) offset = 7; int error = av_open_input_file(&context_, iop->filename() + offset, format_, 0, params_); if (error < 0) { iop->error(ffmpegError(error)); } else { // FIXME_GC: needs to know if it's streamable. error = av_find_stream_info(context_); if (error < 0) { iop->error(ffmpegError(error)); } else { if (findStreamInfo()) { AVCodecContext* codecContext = getVideoStream()->codec; if (getVideoStream()->sample_aspect_ratio.num) aspect_ = av_q2d(getVideoStream()->sample_aspect_ratio); else if (codecContext->sample_aspect_ratio.num) aspect_ = av_q2d(codecContext->sample_aspect_ratio); info_.channels(Mask_RGBA); set_info(width_, height_, 3, aspect_); info_.first_frame(1); info_.last_frame(frames_); data_.resize(width() * height() * 3); // hack so seeking works from our intended position. if (!strcmp(codecContext->codec->name, "mjpeg") || !strcmp(codecContext->codec->name, "dvvideo")) offsetTime_ = false; // H264 codec uses a different initial frame than the other codecs if (!strcmp(codecContext->codec->name, "h264")) codecZeroBasedStart_ = false; meta.setData(MetaData::CREATOR, context_->author); meta.setData(MetaData::COPYRIGHT, context_->copyright); meta.setData(MetaData::COMMENT, context_->comment); meta.setData(MetaData::PROJECT, context_->album); meta.setData(MetaData::FILE_CREATION_TIME, double(context_->timestamp)); meta.setData("ffmpeg/num_streams", context_->nb_streams); meta.setData(MetaData::FRAME_RATE, fps()); meta.setData("ffmpeg/codec/codecName", codecContext->codec->name); } else { iop->error("unable to find codec"); } } } } ffmpegReader::~ffmpegReader() { closeVideoCodec(); if (context_) av_close_input_file(context_); av_free(avFrame_); for (int i = 0; i < CODEC_TYPE_NB; ++i) av_free(avctxOptions[i]); av_free(avformatOptions); } void ffmpegReader::engine(int y, int x, int rx, ChannelMask channels, Row& out) { foreach ( z, channels ) { float* TO = out.writable(z) + x; unsigned char* FROM = &data_[0]; FROM += (height() - y - 1) * width() * 3; FROM += x * 3; from_byte(z, TO, FROM + z - 1, NULL, rx - x, 3); } } void ffmpegReader::open() { int internalFrame = ffmpegFrame(); if (lastDecodedPos_ + 1 != internalFrame) { seek(0); seek(internalFrame); } av_init_packet(&pkt_); bool hasPicture = false; int error = 0; while (error >= 0 && !hasPicture) { error = av_read_frame(context_, &pkt_); if (error < 0) break; if (error >= 0 && videoIdx_.size() && currVideoIdx_ != -1 && pkt_.stream_index == videoIdx_[currVideoIdx_]) hasPicture = decodeImage(); av_free_packet(&pkt_); } } bool ffmpegReader::findStreamInfo() { for (int i = 0; i < static_cast(context_->nb_streams); ++i) { AVCodecContext* codecContext = context_->streams[i]->codec; if (avcodec_find_decoder(codecContext->codec_id) == NULL) continue; switch (codecContext->codec_type) { case CODEC_TYPE_VIDEO: videoIdx_.push_back(i); if (currVideoIdx_ < 0) currVideoIdx_ = 0; width_ = codecContext->width; height_ = codecContext->height; break; // ignore all audio streams case CODEC_TYPE_AUDIO: default: break; } } if (!hasVideo()) return false; AVStream* stream = getVideoStream(); if (stream->r_frame_rate.num != 0 && stream->r_frame_rate.den != 0) { fpsNum_ = stream->r_frame_rate.num; fpsDen_ = stream->r_frame_rate.den; } openVideoCodec(); // Set the duration if ((uint64_t)context_->duration != AV_NOPTS_VALUE) frames_ = uint64_t((fps() * (double)context_->duration / (double)AV_TIME_BASE)); else frames_ = 1 << 29; // try to calculate the number of frames if (!frames_) { seek(0); av_init_packet(&pkt_); av_read_frame(context_, &pkt_); uint64_t firstPts = pkt_.pts; uint64_t maxPts = firstPts; seek(1 << 29); av_init_packet(&pkt_); while (stream && av_read_frame(context_, &pkt_) >= 0) { uint64_t currPts = (uint64_t)(av_q2d(getVideoStream()->time_base) * (pkt_.pts - firstPts) * fps()); if (currPts > maxPts) maxPts = currPts; } frames_ = maxPts; } return true; } double ffmpegReader::fps() const { if (fpsDen_) return fpsNum_ / (double)fpsDen_; return 1.0f; } void ffmpegReader::openVideoCodec() { AVStream* stream = getVideoStream(); AVCodecContext* codecContext = stream->codec; videoCodec_ = avcodec_find_decoder(codecContext->codec_id); if (videoCodec_ == NULL || avcodec_open(codecContext, videoCodec_) < 0) currVideoIdx_ = -1; } void ffmpegReader::closeVideoCodec() { AVStream* stream = getVideoStream(); if (stream && stream->codec) avcodec_close(stream->codec); } int64_t ffmpegReader::getTimeStamp(int pos) const { int64_t timestamp = (int64_t)(((double) pos / fps()) * AV_TIME_BASE); if ((uint64_t) context_->start_time != AV_NOPTS_VALUE) timestamp += context_->start_time; return timestamp; } bool ffmpegReader::seek(int pos) { int64_t offset = getTimeStamp(pos); if (offsetTime_) { offset -= AV_TIME_BASE; if (offset < context_->start_time) offset = 0; } avcodec_flush_buffers(getVideoStream()->codec); if (av_seek_frame(context_, -1, offset, AVSEEK_FLAG_BACKWARD) < 0) return false; return true; } bool ffmpegReader::decodeImage() { // search for our picture. double pts = 0; if ((uint64_t) pkt_.dts != AV_NOPTS_VALUE) pts = av_q2d(getVideoStream()->time_base) * pkt_.dts; int curPos = int(pts * fps() + 0.5f); if (curPos == lastSearchPos_) curPos = lastSearchPos_ + 1; lastSearchPos_ = curPos; if ((uint64_t)context_->start_time != AV_NOPTS_VALUE) curPos -= int(context_->start_time * fps() / AV_TIME_BASE); int hasPicture = 0; int curSearch = 0; AVCodecContext* codecContext = getVideoStream()->codec; if (curPos >= ffmpegFrame()) avcodec_decode_video(codecContext, avFrame_, &hasPicture, pkt_.data, pkt_.size); else if (offsetTime_) avcodec_decode_video(codecContext, avFrame_, &curSearch, pkt_.data, pkt_.size); if (!hasPicture) return false; lastDecodedPos_ = lastSearchPos_; AVPicture output; avpicture_fill(&output, &data_[0], PIX_FMT_RGB24, width_, height_); img_convert(&output, PIX_FMT_RGB24, (AVPicture*) avFrame_, codecContext->pix_fmt, width_, height_); return true; } int ffmpegReader::ffmpegFrame() const { int internalFrame = frame(); // ffmpeg uses 0-based frames for most codecs, so we need to convert Nuke's 1-based frames // H264 seems to still use 1-based, however if (codecZeroBasedStart_) internalFrame --; return internalFrame; } static Reader* build(Read* iop, int fd, const unsigned char* b, int n) { ::close(fd); return new ffmpegReader(iop); } static bool test(int fd, const unsigned char* block, int length) { return true; } const Reader::Description ffmpegReader::d("ffmpeg\0", build, test);