// Copyright (c) 2014 The Foundry Visionmongers Ltd. All Rights Reserved. #include "mov64Reader.h" #include "movReaderFormat.h" #include #include using namespace DD::Image; static const char* const kDefaultColorMatrixLabel = "default (ycbcr_matrix)"; #define CHECK(x) \ {\ int error = x;\ if (error<0) {\ setInternalError(error);\ return;\ }\ }\ typedef std::vector MetaPrefixStripList; MetaPrefixStripList CreateMetaPrefixStripList() { MetaPrefixStripList prefixStripList; prefixStripList.push_back(std::string("com.")); prefixStripList.push_back(std::string("uk.")); prefixStripList.push_back(std::string("co.")); //Note order to allow uk.co. to be fully stripped. prefixStripList.push_back(std::string("de.")); prefixStripList.push_back(std::string("org.")); prefixStripList.push_back(std::string("net.")); prefixStripList.push_back(std::string("gov.")); return prefixStripList; } static const MetaPrefixStripList kMetaPrefixStripList = CreateMetaPrefixStripList(); //Reformat meta key parses the incoming string to optionally strip inverse //DNS notation, as well as replace subdomain .'s with /s to match Nuke //standard metadata formatting. static std::string ReformatMetaKey(const std::string& origKey, const bool doNotAttachPrefix, const bool matchMetaFormat) { std::string key = origKey; if(matchMetaFormat) { //Strip leading reverse dns for those we can identify, otherwise leave. for(MetaPrefixStripList::const_iterator it = kMetaPrefixStripList.begin(); it != kMetaPrefixStripList.end(); it++) { if(((*it).size() < key.size()) && (key.substr(0, (*it).size()) == (*it))) key = key.substr((*it).size(), key.size()); } std::replace(key.begin(), key.end(), '.', '/'); } if(doNotAttachPrefix) return key; return std::string(MetaData::QuickTime::QUICKTIME_PREFIX).append(key); } typedef std::map MetaKeyMap; MetaKeyMap CreateMetaKeyMap() { MetaKeyMap m; m["author"] = MetaData::CREATOR; m["comment"] = MetaData::COMMENT; m["album"] = MetaData::PROJECT; m["copyright"] = MetaData::COPYRIGHT; return m; } static const MetaKeyMap kMetaKeyMap = CreateMetaKeyMap(); //Remap deals with special cased keys going into shared meta namespaces static std::string RemapMetaKey(const std::string& origKey, const bool doNotAttachPrefix, const bool matchMetaFormat) { MetaKeyMap::const_iterator it = kMetaKeyMap.find(origKey); if (it != kMetaKeyMap.end()) return ReformatMetaKey(it->second, doNotAttachPrefix, matchMetaFormat); //It's not in the direct mappings list, but we still need the prefix. std::string newKey = ReformatMetaKey(origKey, doNotAttachPrefix, matchMetaFormat); return newKey; } //Set metadata item is the entry point for special case metadata items that don't need //to be checked against an exclude list. static void SetMetaDataItem(MetaData::Bundle& metadata, const std::string& key, const std::string& value, const bool doNotAttachPrefix, const bool matchMetaFormat) { std::string newKey = RemapMetaKey(key, doNotAttachPrefix, matchMetaFormat); metadata.setData(newKey, value); } //The MetaKeyIgnoreList deals with stripping metadata inserted by ffmpeg, but which //we wish to suppress from the meta list inserted in the stream, either because it's //inconsistent with the standard mov metadata, or because it's values used to convey //internal TF data that's interpreted separately (eg the NCLC atom keys). typedef std::vector MetaKeyList; MetaKeyList CreateMetaKeyIgnoreList() { MetaKeyList l; l.push_back("creation_time"); l.push_back("language"); l.push_back("handler_name"); l.push_back("compatible_brands"); l.push_back("major_brand"); l.push_back("minor_version"); l.push_back("timecode"); //This is specifically searched out and push_backed separately. l.push_back("com.arri.camera.LookFileXml"); //This formats terribly, since its a long xml string. l.push_back(kNCLCPrimariesKey); l.push_back(kNCLCMatrixKey); l.push_back(kNCLCTransferKey); std::sort(l.begin(), l.end()); return l; } static const MetaKeyList kMetaKeyIgnoreList = CreateMetaKeyIgnoreList(); static bool IsMetaKeyIgnored(const std::string& key) { return (std::find(kMetaKeyIgnoreList.begin(), kMetaKeyIgnoreList.end(), key) != kMetaKeyIgnoreList.end()); } //Validate and set metadata item is the generic entry point for adding general metadata items //that need to be checked against an exclude list. static void ValidateAndSetMetaDataItem(MetaData::Bundle& metadata, const std::string& key, const std::string& value, const bool doNotAttachPrefix, const bool matchMetaFormat) { if(!IsMetaKeyIgnored(key)) SetMetaDataItem(metadata, key, value, doNotAttachPrefix, matchMetaFormat); } SwsContext* FFmpegFile::Stream::getConvertCtx(AVPixelFormat srcPixelFormat, int srcWidth, int srcHeight, int srcColorRange, AVPixelFormat dstPixelFormat, int dstWidth, int dstHeight) { // Reset is flagged when the UI colour matrix selection is // modified. This causes a new convert context to be created // that reflects the UI selection. if (_resetConvertCtx) { _resetConvertCtx = false; if (_convertCtx) { sws_freeContext(_convertCtx); _convertCtx = NULL; } } if (!_convertCtx) { //Preventing deprecated pixel format used error messages, see: //https://libav.org/doxygen/master/pixfmt_8h.html#a9a8e335cf3be472042bc9f0cf80cd4c5 //This manually sets them to the new versions of equivalent types. switch (srcPixelFormat) { case AV_PIX_FMT_YUVJ420P : srcPixelFormat = AV_PIX_FMT_YUV420P; break; case AV_PIX_FMT_YUVJ422P : srcPixelFormat = AV_PIX_FMT_YUV422P; break; case AV_PIX_FMT_YUVJ444P : srcPixelFormat = AV_PIX_FMT_YUV444P; break; case AV_PIX_FMT_YUVJ440P : srcPixelFormat = AV_PIX_FMT_YUV440P; default: break; } _convertCtx = sws_getContext(srcWidth, srcHeight, srcPixelFormat, // src format dstWidth, dstHeight, dstPixelFormat, // dest format SWS_BICUBIC, NULL, NULL, NULL); // Set up the SoftWareScaler to convert colorspaces correctly. // Colorspace conversion makes no sense for RGB->RGB conversions if (!isYUV()) { return _convertCtx; } int colorspace = isRec709Format() ? SWS_CS_ITU709 : SWS_CS_ITU601; // Optional color space override if (_colorMatrixTypeOverride > 0) { if (_colorMatrixTypeOverride == 1) { colorspace = SWS_CS_ITU709; } else { colorspace = SWS_CS_ITU601; } } // sws_setColorspaceDetails takes a flag indicating the white-black range of the input: // 0 - mpeg, 16..235 // 1 - jpeg, 0..255 int srcRange; // Set this flag according to the color_range reported by the codec context. switch (srcColorRange) { case AVCOL_RANGE_MPEG: srcRange = 0; break; case AVCOL_RANGE_JPEG: srcRange = 1; break; case AVCOL_RANGE_UNSPECIFIED: default: // If the colour range wasn't specified, set the flag according to // whether the data is YUV or not. srcRange = isYUV() ? 0 : 1; break; } int result = sws_setColorspaceDetails(_convertCtx, sws_getCoefficients(colorspace), // inv_table srcRange, // srcRange -flag indicating the white-black range of the input (1=jpeg / 0=mpeg) 0 = 16..235, 1 = 0..255 sws_getCoefficients(SWS_CS_DEFAULT), // table 1, // dstRange - 0 = 16..235, 1 = 0..255 0, // brightness fixed point, with 0 meaning no change, 1 << 16, // contrast fixed point, with 1<<16 meaning no change, 1 << 16); // saturation fixed point, with 1<<16 meaning no change); assert(result != -1); } return _convertCtx; } /*static*/ float FFmpegFile::Stream::GetStreamAspectRatio(Stream* stream) { if (stream->_avstream->sample_aspect_ratio.num) { #if TRACE_FILE_OPEN std::cout << " Aspect ratio (from stream)=" << av_q2d(stream->_avstream->sample_aspect_ratio) << std::endl; #endif return av_q2d(stream->_avstream->sample_aspect_ratio); } else if (stream->_codecContext->sample_aspect_ratio.num) { #if TRACE_FILE_OPEN std::cout << " Aspect ratio (from codec)=" << av_q2d(stream->_codecContext->sample_aspect_ratio) << std::endl; #endif return av_q2d(stream->_codecContext->sample_aspect_ratio); } #if TRACE_FILE_OPEN else { std::cout << " Aspect ratio unspecified, assuming " << stream->_aspect << std::endl; } #endif return stream->_aspect; } // get stream start time int64_t FFmpegFile::getStreamStartTime(Stream& stream) { #if TRACE_FILE_OPEN std::cout << " Determining stream start PTS:" << std::endl; #endif // Read from stream. If the value read isn't valid, get it from the first frame in the stream that provides such a // value. int64_t startPTS = stream._avstream->start_time; #if TRACE_FILE_OPEN if (startPTS != int64_t(AV_NOPTS_VALUE)) std::cout << " Obtained from AVStream::start_time="; #endif if (startPTS == int64_t(AV_NOPTS_VALUE)) { #if TRACE_FILE_OPEN std::cout << " Not specified by AVStream::start_time, searching frames..." << std::endl; #endif // Seek 1st key-frame in video stream. avcodec_flush_buffers(stream._codecContext); if (av_seek_frame(_context, stream._idx, 0, 0) >= 0) { av_init_packet(&_avPacket); // Read frames until we get one for the video stream that contains a valid PTS. do { if (av_read_frame(_context, &_avPacket) < 0) { // Read error or EOF. Abort search for PTS. #if TRACE_FILE_OPEN std::cout << " Read error, aborted search" << std::endl; #endif break; } if (_avPacket.stream_index == stream._idx) { // Packet read for video stream. Get its PTS. Loop will continue if the PTS is AV_NOPTS_VALUE. startPTS = _avPacket.pts; } av_free_packet(&_avPacket); } while (startPTS == int64_t(AV_NOPTS_VALUE)); } #if TRACE_FILE_OPEN else std::cout << " Seek error, aborted search" << std::endl; #endif #if TRACE_FILE_OPEN if (startPTS != int64_t(AV_NOPTS_VALUE)) std::cout << " Found by searching frames="; #endif } // If we still don't have a valid initial PTS, assume 0. (This really shouldn't happen for any real media file, as // it would make meaningful playback presentation timing and seeking impossible.) if (startPTS == int64_t(AV_NOPTS_VALUE)) { #if TRACE_FILE_OPEN std::cout << " Not found by searching frames, assuming "; #endif startPTS = 0; } #if TRACE_FILE_OPEN std::cout << startPTS << " ticks, " << double(startPTS) * double(stream._avstream->time_base.num) / double(stream._avstream->time_base.den) << " s" << std::endl; #endif return startPTS; } // Get the video stream duration in frames... int64_t FFmpegFile::getStreamFrames(Stream& stream) { #if TRACE_FILE_OPEN std::cout << " Determining stream frame count:" << std::endl; #endif int64_t frames = 0; // Obtain from movie duration if specified. This is preferred since mov/mp4 formats allow the media in // tracks (=streams) to be remapped in time to the final movie presentation without needing to recode the // underlying tracks content; the movie duration thus correctly describes the final presentation. if (_context->duration != 0) { // Annoyingly, FFmpeg exposes the movie duration converted (with round-to-nearest semantics) to units of // AV_TIME_BASE (microseconds in practice) and does not expose the original rational number duration // from a mov/mp4 file's "mvhd" atom/box. Accuracy may be lost in this conversion; a duration that was // an exact number of frames as a rational may end up as a duration slightly over or under that number // of frames in units of AV_TIME_BASE. // Conversion to whole frames rounds up the resulting number of frames because a partial frame is still // a frame. However, in an attempt to compensate for AVFormatContext's inaccurate representation of // duration, with unknown rounding direction, the conversion to frames subtracts 1 unit (microsecond) // from that duration. The rationale for this is thus: // * If the stored duration exactly represents an exact number of frames, then that duration minus 1 // will result in that same number of frames once rounded up. // * If the stored duration is for an exact number of frames that was rounded down, then that duration // minus 1 will result in that number of frames once rounded up. // * If the stored duration is for an exact number of frames that was rounded up, then that duration // minus 1 will result in that number of frames once rounded up, while that duration unchanged would // result in 1 more frame being counted after rounding up. // * If the original duration in the file was not for an exact number of frames, then the movie timebase // would have to be >= 10^6 for there to be any chance of this calculation resulting in the wrong // number of frames. This isn't a case that I've seen. Even if that were to be the case, the original // duration would have to be <= 1 microsecond greater than an exact number of frames in order to // result in the wrong number of frames, which is highly improbable. int64_t divisor = int64_t(AV_TIME_BASE) * stream._fpsDen; frames = ((_context->duration - 1) * stream._fpsNum + divisor - 1) / divisor; // The above calculation is not reliable, because it seems in some situations (such as rendering out a mov // with 5 frames at 24 fps from Nuke) the duration has been rounded up to the nearest millisecond, which // leads to an extra frame being reported. To attempt to work around this, compare against the number of // frames in the stream, and if they differ by one, use that value instead. int64_t streamFrames = stream._avstream->nb_frames; if( streamFrames > 0 && std::abs(frames - streamFrames) <= 1 ) { frames = streamFrames; } #if TRACE_FILE_OPEN std::cout << " Obtained from AVFormatContext::duration & framerate="; #endif } // If number of frames still unknown, obtain from stream's number of frames if specified. Will be 0 if // unknown. if (!frames) { #if TRACE_FILE_OPEN std::cout << " Not specified by AVFormatContext::duration, obtaining from AVStream::nb_frames..." << std::endl; #endif frames = stream._avstream->nb_frames; #if TRACE_FILE_OPEN if (frames) std::cout << " Obtained from AVStream::nb_frames="; #endif } // If number of frames still unknown, attempt to calculate from stream's duration, fps and timebase. if (!frames) { #if TRACE_FILE_OPEN std::cout << " Not specified by AVStream::nb_frames, calculating from duration & framerate..." << std::endl; #endif frames = (int64_t(stream._avstream->duration) * stream._avstream->time_base.num * stream._fpsNum) / (int64_t(stream._avstream->time_base.den) * stream._fpsDen); #if TRACE_FILE_OPEN if (frames) std::cout << " Calculated from duration & framerate="; #endif } // If the number of frames is still unknown, attempt to measure it from the last frame PTS for the stream in the // file relative to first (which we know from earlier). if (!frames) { #if TRACE_FILE_OPEN std::cout << " Not specified by duration & framerate, searching frames for last PTS..." << std::endl; #endif int64_t maxPts = stream._startPTS; // Seek last key-frame. avcodec_flush_buffers(stream._codecContext); av_seek_frame(_context, stream._idx, stream.frameToPts(1<<29), AVSEEK_FLAG_BACKWARD); // Read up to last frame, extending max PTS for every valid PTS value found for the video stream. av_init_packet(&_avPacket); while (av_read_frame(_context, &_avPacket) >= 0) { if (_avPacket.stream_index == stream._idx && _avPacket.pts != int64_t(AV_NOPTS_VALUE) && _avPacket.pts > maxPts) maxPts = _avPacket.pts; av_free_packet(&_avPacket); } #if TRACE_FILE_OPEN std::cout << " Start PTS=" << stream._startPTS << ", Max PTS found=" << maxPts << std::endl; #endif // Compute frame range from min to max PTS. Need to add 1 as both min and max are at starts of frames, so stream // extends for 1 frame beyond this. frames = 1 + stream.ptsToFrame(maxPts); #if TRACE_FILE_OPEN std::cout << " Calculated from frame PTS range="; #endif } #if TRACE_FILE_OPEN std::cout << frames << std::endl; #endif return frames; } // constructor FFmpegFile::FFmpegFile(const char* filename) : _context(NULL) , _format(NULL) , _invalidState(false) #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) , _decoder(0) #endif { // FIXME_GC: shouldn't the plugin be passed the filename without the prefix? int offset = 0; if (std::string(filename).find("ffmpeg:") != std::string::npos) offset = 7; if (std::string(filename).find("mov64:") != std::string::npos) offset = 6; #if TRACE_FILE_OPEN std::cout << "mov64Reader=" << this << "::c'tor(): filename=" << filename + offset << std::endl; #endif CHECK( avformat_open_input(&_context, filename + offset, _format, NULL) ); CHECK( avformat_find_stream_info(_context, NULL) ); #if TRACE_FILE_OPEN std::cout << " " << _context->nb_streams << " streams:" << std::endl; #endif // fill the array with all available video streams bool unsuported_codec = false; // find all streams that the library is able to decode for (unsigned i = 0; i < _context->nb_streams; ++i) { #if TRACE_FILE_OPEN std::cout << " FFmpeg stream index " << i << ": "; #endif AVStream* avstream = _context->streams[i]; // be sure to have a valid stream if (!avstream || !avstream->codec) { #if TRACE_FILE_OPEN std::cout << "No valid stream or codec, skipping..." << std::endl; #endif continue; } // considering only video streams, skipping audio if (avstream->codec->codec_type != AVMEDIA_TYPE_VIDEO) { #if TRACE_FILE_OPEN std::cout << "Not a video stream, skipping..." << std::endl; #endif continue; } // find the codec AVCodec* videoCodec = avcodec_find_decoder(avstream->codec->codec_id); if (videoCodec == NULL) { #if TRACE_FILE_OPEN std::cout << "Decoder not found, skipping..." << std::endl; #endif continue; } // skip codecs not in the white list if (!Foundry::Nuke::isCodecWhitelistedForReading(videoCodec->name)) { #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) // Apple ProRes and Avid DNxHD specific. // ProRes and DNxHD decoding is supported by // this reader as the official Apple and Avid libraries have // been licensed. So check whether the black listed codec is // indeed ProRes or DNxHD and permit it to pass for this // reader. // // DNxHD currently has some roundtrip colour issues so this // is not included unless the environment variable is set. // if ((!strcmp(videoCodec->name, "dnxhd") && getenv("NUKE_EXPERIMENTAL_DNXHD_SUPPORT")) || !strcmp(videoCodec->name, "prores")) { } else { # if TRACE_FILE_OPEN std::cout << "Decoder \"" << videoCodec->name << "\" disallowed, skipping..." << std::endl; # endif unsuported_codec = true; continue; } #else # if TRACE_FILE_OPEN std::cout << "Decoder \"" << videoCodec->name << "\" disallowed, skipping..." << std::endl; # endif unsuported_codec = true; continue; #endif } // Some codecs support multi-threaded decoding (eg mpeg). Its fast but causes problems when opening many readers // simultaneously since each opens as many threads as you have cores. This leads to resource starvation and failed reads. // For now, revert to the previous ffmpeg behaviour (single-threaded decode) unless overridden by env var. const char* multiThreadedString = getenv("NUKE_MULTI_THREADED_CODEC_SUPPORT"); if (multiThreadedString && strcmp(multiThreadedString, "1") == 0) { # if TRACE_FILE_OPEN std::cout << "mov64Reader: Multi-threaded decode" << std::endl; # endif } else { # if TRACE_FILE_OPEN std::cout << "mov64Reader: Single-threaded decode" << std::endl; # endif // Default behaviour avstream->codec->thread_count = 1; avstream->codec->thread_type = FF_THREAD_FRAME; } // skip if the codec can't be open if (avcodec_open2(avstream->codec, videoCodec, NULL) < 0) { #if TRACE_FILE_OPEN std::cout << "Decoder \"" << videoCodec->name << "\" failed to open, skipping..." << std::endl; #endif continue; } #if TRACE_FILE_OPEN std::cout << "Video decoder \"" << videoCodec->name << "\" opened ok, getting stream properties:" << std::endl; #endif Stream* stream = new Stream(); stream->_idx = i; stream->_avstream = avstream; stream->_codecContext = avstream->codec; stream->_videoCodec = videoCodec; stream->_avFrame = avcodec_alloc_frame(); #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) if (AV_CODEC_ID_PRORES == avstream->codec->codec_id) { // Always assume there is alpha. // This is done so that if alpha is present in the encoded // bitstream, then it will not be discarded. If there is // no alpha in the encoded bitstream, then the decoder // returns opaque values for alpha. stream->_bitDepth = (NukeCodecs::CodecFactory::eProResCodec4444XQ == SWAP32(stream->_codecContext->codec_tag)) ? 12 : 10; stream->_numberOfComponents = 4; } else if (AV_CODEC_ID_DNXHD == avstream->codec->codec_id) { // Avid DNxHD specific. stream->_bitDepth = avstream->codec->bits_per_raw_sample; stream->_numberOfComponents = 4; // Always assume there is alpha. // Use the RGBA component re-order swizzle. This avoids having // to use another buffer to convert from BGRA to RGBA. stream->_componentPosition[0] = 2; // Red position. stream->_componentPosition[1] = 1; // Green position. stream->_componentPosition[2] = 0; // Blue position. stream->_componentPosition[3] = 3; // Alpha position. } else #endif { stream->_bitDepth = avstream->codec->bits_per_raw_sample; const AVPixFmtDescriptor* avPixFmtDescriptor = av_pix_fmt_desc_get(stream->_codecContext->pix_fmt); // Sanity check the number of components. // Only 3 or 4 components are supported by |engine|, that is // Nuke/NukeStudio will only accept 3 or 4 component data. // For a monochrome image (single channel) promote to 3 // channels. This is in keeping with all the assumptions // throughout the code that if it is not 4 channels data // then it must be three channel data. This ensures that // all the buffer size calculations are correct. stream->_numberOfComponents = avPixFmtDescriptor->nb_components; if (3 > stream->_numberOfComponents) stream->_numberOfComponents = 3; // AVCodecContext::bits_pre_raw_sample may not be set, if // it's not set, try with the following utility function. if (0 == stream->_bitDepth) stream->_bitDepth = av_get_bits_per_pixel(avPixFmtDescriptor) / stream->_numberOfComponents; } if (stream->_bitDepth > 8) stream->_outputPixelFormat = (4 == stream->_numberOfComponents) ? AV_PIX_FMT_RGBA64LE : AV_PIX_FMT_RGB48LE; // 16-bit. else stream->_outputPixelFormat = (4 == stream->_numberOfComponents) ? AV_PIX_FMT_RGBA : AV_PIX_FMT_RGB24; // 8-bit #if TRACE_FILE_OPEN std::cout << " Timebase=" << avstream->time_base.num << "/" << avstream->time_base.den << " s/tick" << std::endl; std::cout << " Duration=" << avstream->duration << " ticks, " << double(avstream->duration) * double(avstream->time_base.num) / double(avstream->time_base.den) << " s" << std::endl; std::cout << " BitDepth=" << stream->_bitDepth << std::endl; std::cout << " NumberOfComponents=" << stream->_numberOfComponents << std::endl; #endif // If FPS is specified, record it. // Otherwise assume 1 fps (default value). if ( avstream->r_frame_rate.num != 0 && avstream->r_frame_rate.den != 0 ) { stream->_fpsNum = avstream->r_frame_rate.num; stream->_fpsDen = avstream->r_frame_rate.den; #if TRACE_FILE_OPEN std::cout << " Framerate=" << stream->_fpsNum << "/" << stream->_fpsDen << ", " << double(stream->_fpsNum) / double(stream->_fpsDen) << " fps" << std::endl; #endif } #if TRACE_FILE_OPEN else std::cout << " Framerate unspecified, assuming 1 fps" << std::endl; #endif stream->_width = avstream->codec->width; stream->_height = avstream->codec->height; #if TRACE_FILE_OPEN std::cout << " Image size=" << stream->_width << "x" << stream->_height << std::endl; #endif // set aspect ratio stream->_aspect = Stream::GetStreamAspectRatio(stream); // set stream start time and numbers of frames stream->_startPTS = getStreamStartTime(*stream); stream->_frames = getStreamFrames(*stream); // save the stream _streams.push_back(stream); } if (_streams.empty()) setError( unsuported_codec ? "unsupported codec..." : "unable to find video stream" ); } // destructor FFmpegFile::~FFmpegFile() { // force to close all resources needed for all streams std::for_each(_streams.begin(), _streams.end(), Stream::destroy); if (_context) avformat_close_input(&_context); } const char* FFmpegFile::getColorspace() const { //The preferred colorspace is figured out from a number of sources - initially we look for a number //of different metadata sources that may be present in the file. If these fail we then fall back //to using the codec's underlying storage mechanism - if RGB we default to gamma 1.8, if YCbCr we //default to gamma 2.2 (note prores special case). Note we also ignore the NCLC atom for reading //purposes, as in practise it tends to be incorrect. //First look for the meta keys that (recent) Nukes would've written, or special cases in Arri meta. //Doubles up searching for lower case keys as the ffmpeg searches are case sensitive, and the keys //have been seen to be lower cased (particularly in old Arri movs). if(_context && _context->metadata) { AVDictionaryEntry* t; t = av_dict_get(_context->metadata, "uk.co.thefoundry.Colorspace", NULL, AV_DICT_IGNORE_SUFFIX); if (!t) av_dict_get(_context->metadata, "uk.co.thefoundry.colorspace", NULL, AV_DICT_IGNORE_SUFFIX); if (t) { //Validate t->value against root list, to make sure it's been written with a LUT //we have a matching conversion for. bool found = false; int i = 0; while (!found && LUT::builtin_names[i] != NULL) { found = !strcasecmp(t->value, LUT::builtin_names[i++]); } if (found) { return t->value; } } t = av_dict_get(_context->metadata, "com.arri.camera.ColorGammaSxS", NULL, AV_DICT_IGNORE_SUFFIX); if (!t) av_dict_get(_context->metadata, "com.arri.camera.colorgammasxs", NULL, AV_DICT_IGNORE_SUFFIX); if (t && !strncasecmp(t->value, "LOG-C", 5)) { return "AlexaV3LogC"; } if (t && !strncasecmp(t->value, "REC-709", 7)) { return "rec709"; } } #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) //Special case for prores - the util YUV will report RGB, due to pixel format support, but for //compatibility and consistency with official quicktime, we need to be using 2.2 for 422 material //and 1.8 for 4444. Protected to deal with ffmpeg vagaries. if (!_streams.empty() && _streams[0]->_codecContext && _streams[0]->_codecContext->codec_id) { if (_streams[0]->_codecContext->codec_id == AV_CODEC_ID_PRORES) { if (SWAP32(_streams[0]->_codecContext->codec_tag) == NukeCodecs::CodecFactory::eProResCodec4444 || SWAP32(_streams[0]->_codecContext->codec_tag) == NukeCodecs::CodecFactory::eProResCodec4444XQ) { return "Gamma1.8"; } else { return "Gamma2.2"; } } } #endif return (isYUV() ? "Gamma2.2" : "Gamma1.8"); } // decode a single frame into the buffer. Thread safe bool FFmpegFile::decode(unsigned char* buffer, unsigned frame, size_t streamIdx) { Guard guard(_lock); if (streamIdx >= _streams.size()) return false; mFnAssertMsg(streamIdx == 0, "FFmpegFile functions always assume only the first stream is in use"); // get the stream Stream* stream = _streams[streamIdx]; // Early-out if out-of-range frame requested. if (frame >= stream->_frames) return false; #if TRACE_DECODE_PROCESS std::cout << "mov64Reader=" << this << "::decode(): frame=" << frame << ", videoStream=" << streamIdx << ", streamIdx=" << stream->_idx << std::endl; #endif // Number of read retries remaining when decode stall is detected before we give up (in the case of post-seek stalls, // such retries are applied only after we've searched all the way back to the start of the file and failed to find a // successful start point for playback).. // // We have a rather annoying case with a small subset of media files in which decode latency (between input and output // frames) will exceed the maximum above which we detect decode stall at certain frames on the first pass through the // file but those same frames will decode succesfully on a second attempt. The root cause of this is not understood but // it appears to be some oddity of FFmpeg. While I don't really like it, retrying decode enables us to successfully // decode those files rather than having to fail the read. int retriesRemaining = 1; // Whether we have just performed a seek and are still awaiting the first decoded frame after that seek. This controls // how we respond when a decode stall is detected. // // One cause of such stalls is when a file contains incorrect information indicating that a frame is a key-frame when it // is not; a seek may land at such a frame but the decoder will not be able to start decoding until a real key-frame is // reached, which may be a long way in the future. Once a frame has been decoded, we will expect it to be the first frame // input to decode but it will actually be the next real key-frame found, leading to subsequent frames appearing as // earlier frame numbers and the movie ending earlier than it should. To handle such cases, when a stall is detected // immediately after a seek, we seek to the frame before the previous seek's landing frame, allowing us to search back // through the movie for a valid key frame from which decode commences correctly; if this search reaches the beginning of // the movie, we give up and fail the read, thus ensuring that this method will exit at some point. // // Stalls once seeking is complete and frames are being decoded are handled differently; these result in immediate read // failure. bool awaitingFirstDecodeAfterSeek = false; // If the frame we want is not the next one to be decoded, seek to the keyframe before/at our desired frame. Set the last // seeked frame to indicate that we need to synchronise frame indices once we've read the first frame of the video stream, // since we don't yet know which frame number the seek will land at. Also invalidate current indices, reset accumulated // decode latency and record that we're awaiting the first decoded frame after a seek. int lastSeekedFrame = -1; // 0-based index of the last frame to which we seeked when seek in progress / negative when no // seek in progress, if (frame != stream->_decodeNextFrameOut) { #if TRACE_DECODE_PROCESS std::cout << " Next frame expected out=" << stream->_decodeNextFrameOut << ", Seeking to desired frame" << std::endl; #endif lastSeekedFrame = frame; stream->_decodeNextFrameIn = -1; stream->_decodeNextFrameOut = -1; stream->_accumDecodeLatency = 0; awaitingFirstDecodeAfterSeek = true; avcodec_flush_buffers(stream->_codecContext); int error = av_seek_frame(_context, stream->_idx, stream->frameToPts(frame), AVSEEK_FLAG_BACKWARD); if (error < 0) { // Seek error. Abort attempt to read and decode frames. setInternalError(error, "FFmpeg Reader failed to seek frame: "); return false; } } #if TRACE_DECODE_PROCESS else std::cout << " Next frame expected out=" << stream->_decodeNextFrameOut << ", No seek required" << std::endl; #endif av_init_packet(&_avPacket); // Loop until the desired frame has been decoded. May also break from within loop on failure conditions where the // desired frame will never be decoded. bool hasPicture = false; do { bool decodeAttempted = false; int frameDecoded = 0; int srcColourRange = stream->_codecContext->color_range; #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) // Apple ProRes and Avid DNxHD specific. // Licensed SDKs are used for decoding ProRes or DNxHD. // This means that a buffer must be allocated to received // the decoded frame when decoding ProRes or DNxHD. // We need to handle our own buffer management. if ((AV_CODEC_ID_PRORES == stream->_codecContext->codec_id) || (AV_CODEC_ID_DNXHD == stream->_codecContext->codec_id)) { srcColourRange = AVCOL_RANGE_JPEG; // Default to full scale. // Allocate a buffer to receive the decoded frame. // Create an instance of a decoder if one does not // already exist. NukeCodecs::CodecFactory::CodecType codecType = NukeCodecs::CodecFactory::eUnknownCodec; if (AV_CODEC_ID_PRORES == stream->_codecContext->codec_id) codecType = static_cast(SWAP32(stream->_codecContext->codec_tag)); else { // The four character code 'AVdn' aliases all DNxHD // compression IDs. The 'ARES' atom contains the actual // compression ID. The 'ACLR' atom contains a parameter // that indicates if the data range of the component // data is full range (0-255) or legal (16-240). The // FFmpeg library was modified to provide the 'ARES' and // 'ACLR' atoms in metadata for DNxHD, so attempt to // locate and use that information here. // AVDictionaryEntry* t = av_dict_get(stream->_avstream->metadata, kARESCompressionID, NULL, AV_DICT_IGNORE_SUFFIX); if (t) { codecType = static_cast(strtoul(t->value, NULL, 10)); t = NULL; } t = av_dict_get(stream->_avstream->metadata, kACLRYuvRange, NULL, AV_DICT_IGNORE_SUFFIX); if (t) { // 'ACLR' defines a range field as 1=full range, 2=legal range. // That is mapped here to AVCOL_RANGE_JPEG=full range, AVCOL_RANGE_MPEG=legal range. // R.e. |getConvertCtx| above. srcColourRange = (2 == strtoul(t->value, NULL, 10)) ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG; t = NULL; } } if (!_decoder.get()) _decoder.reset(NukeCodecs::CodecFactory::makeDecoder(codecType)); if (_decoder.get()) { _decoder->GetDecoderInfo(codecType, stream->_codecContext->width, stream->_codecContext->height, _codecInfo); // Only ProRes requires an intermediate buffer to convert // from the codec output pixel format to the Nuke pixel // format. The DNxHD decoder outputs in the Nuke pixel // format so no additional conversions are required. if (AV_CODEC_ID_PRORES == stream->_codecContext->codec_id) stream->_codecContext->pix_fmt = AV_PIX_FMT_BGRA64LE; // Currently the ProRes decoder output 10-12 bits. else { if ((NukeCodecs::CodecFactory::eDNxHDCodec1235 == codecType) || (NukeCodecs::CodecFactory::eDNxHDCodec1241 == codecType) || (NukeCodecs::CodecFactory::eDNxHDCodec1250 == codecType) || (NukeCodecs::CodecFactory::eDNxHDCodec1256 == codecType)) { stream->_codecContext->pix_fmt = AV_PIX_FMT_BGRA64LE; // 10-bit DNxHD. } else stream->_codecContext->pix_fmt = AV_PIX_FMT_RGBA; // 8-bit DNxHD. } if (!stream->_avFrame->linesize[0]) { // Allocate a buffer if one has not already been allocated. av_image_alloc(stream->_avFrame->data, stream->_avFrame->linesize, _codecInfo._width, _codecInfo._height, stream->_codecContext->pix_fmt, 1); } } else { setError("failed to create decoder"); break; } } #endif // If the next frame to decode is within range of frames (or negative implying invalid; we've just seeked), read // a new frame from the source file and feed it to the decoder if it's for the video stream. if (stream->_decodeNextFrameIn < stream->_frames) { #if TRACE_DECODE_PROCESS std::cout << " Next frame expected in="; if (stream->_decodeNextFrameIn >= 0) std::cout << stream->_decodeNextFrameIn; else std::cout << "unknown"; #endif int error = av_read_frame(_context, &_avPacket); if (error < 0) { // Read error. Abort attempt to read and decode frames. #if TRACE_DECODE_PROCESS std::cout << ", Read failed" << std::endl; #endif setInternalError(error, "FFmpeg Reader failed to read frame: "); break; } #if TRACE_DECODE_PROCESS std::cout << ", Read OK, Packet data:" << std::endl; std::cout << " PTS=" << _avPacket.pts << ", DTS=" << _avPacket.dts << ", Duration=" << _avPacket.duration << ", KeyFrame=" << ((_avPacket.flags & AV_PKT_FLAG_KEY) ? 1 : 0) << ", Corrupt=" << ((_avPacket.flags & AV_PKT_FLAG_CORRUPT) ? 1 : 0) << ", StreamIdx=" << _avPacket.stream_index << ", PktSize=" << _avPacket.size; #endif // If the packet read belongs to the video stream, synchronise frame indices from it if required and feed it // into the decoder. if (_avPacket.stream_index == stream->_idx) { #if TRACE_DECODE_PROCESS std::cout << ", Relevant stream" << std::endl; #endif // If the packet read has a valid PTS, record that we have seen a PTS for this stream. if (_avPacket.pts != int64_t(AV_NOPTS_VALUE)) stream->_ptsSeen = true; // If a seek is in progress, we need to synchronise frame indices if we can... if (lastSeekedFrame >= 0) { #if TRACE_DECODE_PROCESS std::cout << " In seek (" << lastSeekedFrame << ")"; #endif // Determine which frame the seek landed at, using whichever kind of timestamp is currently selected for this // stream. If there's no timestamp available at that frame, we can't synchronise frame indices to know which // frame we're first going to decode, so we need to seek back to an earlier frame in hope of obtaining a // timestamp. Likewise, if the landing frame is after the seek target frame (this can happen, presumably a bug // in FFmpeg seeking), we need to seek back to an earlier frame so that we can start decoding at or before the // desired frame. int landingFrame; if (_avPacket.*stream->_timestampField == int64_t(AV_NOPTS_VALUE) || (landingFrame = stream->ptsToFrame(_avPacket.*stream->_timestampField)) > lastSeekedFrame) { #if TRACE_DECODE_PROCESS std::cout << ", landing frame not found"; if (_avPacket.*stream->_timestampField == int64_t(AV_NOPTS_VALUE)) std::cout << " (no timestamp)"; else std::cout << " (landed after target at " << landingFrame << ")"; #endif // Wind back 1 frame from last seeked frame. If that takes us to before frame 0, we're never going to be // able to synchronise using the current timestamp source... if (--lastSeekedFrame < 0) { #if TRACE_DECODE_PROCESS std::cout << ", can't seek before start"; #endif // If we're currently using PTSs to determine the landing frame and we've never seen a valid PTS for any // frame from this stream, switch to using DTSs and retry the read from the initial desired frame. if (stream->_timestampField == &AVPacket::pts && !stream->_ptsSeen) { stream->_timestampField = &AVPacket::dts; lastSeekedFrame = frame; #if TRACE_DECODE_PROCESS std::cout << ", PTSs absent, switching to use DTSs"; #endif } // Otherwise, failure to find a landing point isn't caused by an absence of PTSs from the file or isn't // recovered by using DTSs instead. Something is wrong with the file. Abort attempt to read and decode frames. else { #if TRACE_DECODE_PROCESS if (stream->_timestampField == &AVPacket::dts) std::cout << ", search using DTSs failed"; else std::cout << ", PTSs present"; std::cout << ", giving up" << std::endl; #endif setError("FFmpeg Reader failed to find timing reference frame, possible file corruption"); break; } } // Seek to the new frame. By leaving the seek in progress, we will seek backwards frame by frame until we // either successfully synchronise frame indices or give up having reached the beginning of the stream. #if TRACE_DECODE_PROCESS std::cout << ", seeking to " << lastSeekedFrame << std::endl; #endif avcodec_flush_buffers(stream->_codecContext); error = av_seek_frame(_context, stream->_idx, stream->frameToPts(lastSeekedFrame), AVSEEK_FLAG_BACKWARD); if (error < 0) { // Seek error. Abort attempt to read and decode frames. setInternalError(error, "FFmpeg Reader failed to seek frame: "); break; } } // Otherwise, we have a valid landing frame, so set that as the next frame into and out of decode and set // no seek in progress. else { #if TRACE_DECODE_PROCESS std::cout << ", landed at " << landingFrame << std::endl; #endif stream->_decodeNextFrameOut = stream->_decodeNextFrameIn = landingFrame; lastSeekedFrame = -1; } } // If there's no seek in progress, feed this frame into the decoder. if (lastSeekedFrame < 0) { #if TRACE_DECODE_BITSTREAM // H.264 ONLY std::cout << " Decoding input frame " << stream->_decodeNextFrameIn << " bitstream:" << std::endl; uint8_t *data = _avPacket.data; uint32_t remain = _avPacket.size; while (remain > 0) { if (remain < 4) { std::cout << " Insufficient remaining bytes (" << remain << ") for block size at BlockOffset=" << (data - _avPacket.data) << std::endl; remain = 0; } else { uint32_t blockSize = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; data += 4; remain -= 4; std::cout << " BlockOffset=" << (data - _avPacket.data) << ", Size=" << blockSize; if (remain < blockSize) { std::cout << ", Insufficient remaining bytes (" << remain << ")" << std::endl; remain = 0; } else { std::cout << ", Bytes:"; int count = (blockSize > 16 ? 16 : blockSize); for (int offset = 0; offset < count; offset++) { static const char hexTable[] = "0123456789ABCDEF"; uint8_t byte = data[offset]; std::cout << ' ' << hexTable[byte >> 4] << hexTable[byte & 0xF]; } std::cout << std::endl; data += blockSize; remain -= blockSize; } } } #elif TRACE_DECODE_PROCESS std::cout << " Decoding input frame " << stream->_decodeNextFrameIn << std::endl; #endif // Advance next frame to input. ++stream->_decodeNextFrameIn; // Decode the frame just read. frameDecoded indicates whether a decoded frame was output. decodeAttempted = true; #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) if ((AV_CODEC_ID_PRORES == stream->_codecContext->codec_id) || (AV_CODEC_ID_DNXHD == stream->_codecContext->codec_id)) { // Apple ProRes and Avid DNxHD specific. // Decode the frame. NukeCodecs::DecoderParams params; params.in = _avPacket.data; params.inSize = _avPacket.size; params.width = _codecInfo._width; params.height = _codecInfo._height; params.out = stream->_avFrame->data[0]; params.outRowBytes = _codecInfo._rowBytes; params.codecType = _codecInfo._codecType; error = _decoder->Decode(params); frameDecoded = (error <= 0) ? 0 : 1; // Mimic the behaviour of |avcodec_decode_video2| on |frameDecoded|. mFnAssert(frameDecoded); } else #endif { error = avcodec_decode_video2(stream->_codecContext, stream->_avFrame, &frameDecoded, &_avPacket); } if (error < 0) { // Decode error. Abort attempt to read and decode frames. setInternalError(error, "FFmpeg Reader failed to decode frame: "); break; } } } #if TRACE_DECODE_PROCESS else std::cout << ", Irrelevant stream" << std::endl; #endif } // If the next frame to decode is out of frame range, there's nothing more to read and the decoder will be fed // null input frames in order to obtain any remaining output. else { #if TRACE_DECODE_PROCESS std::cout << " No more frames to read, pumping remaining decoder output" << std::endl; #endif // Obtain remaining frames from the decoder. pkt_ contains NULL packet data pointer and size at this point, // required to pump out remaining frames with no more input. frameDecoded indicates whether a decoded frame // was output. decodeAttempted = true; int error = 0; #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) if ((AV_CODEC_ID_PRORES == stream->_codecContext->codec_id) || (AV_CODEC_ID_DNXHD == stream->_codecContext->codec_id)) { // Apple ProRes specific. // The ProRes codec is I-frame only so will not have any // remaining frames. } else #endif { error = avcodec_decode_video2(stream->_codecContext, stream->_avFrame, &frameDecoded, &_avPacket); } if (error < 0) { // Decode error. Abort attempt to read and decode frames. setInternalError(error, "FFmpeg Reader failed to decode frame: "); break; } } // If a frame was decoded, ... if (frameDecoded) { #if TRACE_DECODE_PROCESS std::cout << " Frame decoded=" << stream->_decodeNextFrameOut; #endif // Now that we have had a frame decoded, we know that seek landed at a valid place to start decode. Any decode // stalls detected after this point will result in immediate decode failure. awaitingFirstDecodeAfterSeek = false; // If the frame just output from decode is the desired one, get the decoded picture from it and set that we // have a picture. if (stream->_decodeNextFrameOut == frame) { #if TRACE_DECODE_PROCESS std::cout << ", is desired frame" << std::endl; #endif AVPicture output; avpicture_fill(&output, buffer, stream->_outputPixelFormat, stream->_width, stream->_height); SwsContext* context = NULL; #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) if (AV_CODEC_ID_PRORES == stream->_codecContext->codec_id) { // Apple ProRes specific. // Convert the big endian format to little endian. // For speed, divide the conversion between the physical // CPUs by creating a worker thread to run on each CPU. // Initialise the payload for each thread and then // spawn the worker thread. convertBuffer(eb64a_to_RGBA64LE, stream->_avFrame->data[0], _codecInfo._rowBytes, reinterpret_cast(output.data[0]), output.linesize[0], stream->_width, stream->_height); } else if (AV_CODEC_ID_DNXHD == stream->_codecContext->codec_id) { // Avid DNxHD specific. // Convert from the pixel format provided by the decoder to the // desired pixel format for Nuke/NukeStudio. There are two // possible options, the 'thin raster' option where the DNxHD // compression ID is 1440x1080 or 960x720 which needs to be // resized to 1920x1080 and 1280x720 respectively. Fortunately // these are 8-bit formats so the rescale is supported. The other // option is Avid specific, where the range of component values // could be 'legal' range, i.e. NOT full scale. In this case a // conversion is needed to rescale the component values from // (16-240) to (0-255). This includes the RGB from DNxHD 4:4:4. // However this is only supported for 8-bit pixel formats in // FFmpeg, so the 10-bit rescale has to be done here. if ((NukeCodecs::CodecFactory::eDNxHDCodec1244 == _codecInfo._codecType) || (NukeCodecs::CodecFactory::eDNxHDCodec1258 == _codecInfo._codecType) || (NukeCodecs::CodecFactory::eDNxHDCodec1259 == _codecInfo._codecType) || (NukeCodecs::CodecFactory::eDNxHDCodec1260 == _codecInfo._codecType)) { // Rescale the 'thin raster' compression IDs to full raster. context = stream->getConvertCtx(stream->_codecContext->pix_fmt, _codecInfo._width, _codecInfo._height, srcColourRange, stream->_outputPixelFormat, stream->_width, stream->_height); } else memcpy(output.data[0], stream->_avFrame->data[0], stream->_height * stream->_avFrame->linesize[0]); } else #endif { context = stream->getConvertCtx(stream->_codecContext->pix_fmt, stream->_width, stream->_height, srcColourRange, stream->_outputPixelFormat, stream->_width, stream->_height); } // Scale if any of the decoding path has provided a convert // context. Otherwise, no scaling/conversion is required after // decoding the frame. if (context) { sws_scale(context, stream->_avFrame->data, stream->_avFrame->linesize, 0, stream->_height, output.data, output.linesize); } hasPicture = true; } #if TRACE_DECODE_PROCESS else std::cout << ", is not desired frame (" << frame << ")" << std::endl; #endif // Advance next output frame expected from decode. ++stream->_decodeNextFrameOut; } // If no frame was decoded but decode was attempted, determine whether this constitutes a decode stall and handle if so. else if (decodeAttempted) { // Failure to get an output frame for an input frame increases the accumulated decode latency for this stream. ++stream->_accumDecodeLatency; #if TRACE_DECODE_PROCESS std::cout << " No frame decoded, accumulated decode latency=" << stream->_accumDecodeLatency << ", max allowed latency=" << stream->getCodecDelay() << std::endl; #endif // If the accumulated decode latency exceeds the maximum delay permitted for this codec at this time (the delay can // change dynamically if the codec discovers B-frames mid-stream), we've detected a decode stall. if (stream->_accumDecodeLatency > stream->getCodecDelay()) { int seekTargetFrame; // Target frame for any seek we might perform to attempt decode stall recovery. // Handle a post-seek decode stall. if (awaitingFirstDecodeAfterSeek) { // If there's anywhere in the file to seek back to before the last seek's landing frame (which can be found in // stream->_decodeNextFrameOut, since we know we've not decoded any frames since landing), then set up a seek to // the frame before that landing point to try to find a valid decode start frame earlier in the file. if (stream->_decodeNextFrameOut > 0) { seekTargetFrame = stream->_decodeNextFrameOut - 1; #if TRACE_DECODE_PROCESS std::cout << " Post-seek stall detected, trying earlier decode start, seeking frame " << seekTargetFrame << std::endl; #endif } // Otherwise, there's nowhere to seek back. If we have any retries remaining, use one to attempt the read again, // starting from the desired frame. else if (retriesRemaining > 0) { --retriesRemaining; seekTargetFrame = frame; #if TRACE_DECODE_PROCESS std::cout << " Post-seek stall detected, at start of file, retrying from desired frame " << seekTargetFrame << std::endl; #endif } // Otherwise, all we can do is to fail the read so that this method exits safely. else { #if TRACE_DECODE_PROCESS std::cout << " Post-seek STALL DETECTED, at start of file, no more retries, failed read" << std::endl; #endif setError("FFmpeg Reader failed to find decode reference frame, possible file corruption"); break; } } // Handle a mid-decode stall. All we can do is to fail the read so that this method exits safely. else { // If we have any retries remaining, use one to attempt the read again, starting from the desired frame. if (retriesRemaining > 0) { --retriesRemaining; seekTargetFrame = frame; #if TRACE_DECODE_PROCESS std::cout << " Mid-decode stall detected, retrying from desired frame " << seekTargetFrame << std::endl; #endif } // Otherwise, all we can do is to fail the read so that this method exits safely. else { #if TRACE_DECODE_PROCESS std::cout << " Mid-decode STALL DETECTED, no more retries, failed read" << std::endl; #endif setError("FFmpeg Reader detected decoding stall, possible file corruption"); break; } } // If we reach here, seek to the target frame chosen above in an attempt to recover from the decode stall. lastSeekedFrame = seekTargetFrame; stream->_decodeNextFrameIn = -1; stream->_decodeNextFrameOut = -1; stream->_accumDecodeLatency = 0; awaitingFirstDecodeAfterSeek = true; avcodec_flush_buffers(stream->_codecContext); int error = av_seek_frame(_context, stream->_idx, stream->frameToPts(seekTargetFrame), AVSEEK_FLAG_BACKWARD); if (error < 0) { // Seek error. Abort attempt to read and decode frames. setInternalError(error, "FFmpeg Reader failed to seek frame: "); break; } } } av_free_packet(&_avPacket); } while (!hasPicture); #if TRACE_DECODE_PROCESS std::cout << "<-validPicture=" << hasPicture << " for frame " << frame << std::endl; #endif // If read failed, reset the next frame expected out so that we seek and restart decode on next read attempt. Also free // the AVPacket, since it won't have been freed at the end of the above loop (we reach here by a break from the main // loop when hasPicture is false). if (!hasPicture) { if (_avPacket.size > 0) av_free_packet(&_avPacket); stream->_decodeNextFrameOut = -1; } return hasPicture; } // fill the metada information of a particular stream bool FFmpegFile::metadata(DD::Image::MetaData::Bundle& metadata, size_t streamIdx) { Guard guard(_lock); if (streamIdx >= _streams.size()) return false; // get the stream Stream* stream = _streams[streamIdx]; int bitsPerComponent = 8; // 8 bits per channel (Fix for Bug 29986. data_ is unsigned char) const char* codecName = 0; #if defined(FN_LICENSED_PRORES_DNXHD_CODEC) if (AV_CODEC_ID_PRORES == stream->_codecContext->codec_id) { // Set the full (and correct) ProRes codec profile name. // The |codec_tag| member variable is the four character // code (FCC) of the codec profile. Use this to identify // the ProRes encoding profile and set the appropriate // codec profile name. switch (SWAP32(stream->_codecContext->codec_tag)) { case NukeCodecs::CodecFactory::eProResCodec4444XQ: codecName = APPLE_PRORES_4444XQ_NAME; bitsPerComponent = 12; break; case NukeCodecs::CodecFactory::eProResCodec4444: codecName = APPLE_PRORES_4444_NAME; bitsPerComponent = 10; break; case NukeCodecs::CodecFactory::eProResCodec422HQ: codecName = APPLE_PRORES_422HQ_NAME; bitsPerComponent = 10; break; case NukeCodecs::CodecFactory::eProResCodec422: codecName = APPLE_PRORES_422_NAME; bitsPerComponent = 10; break; case NukeCodecs::CodecFactory::eProResCodec422LT: codecName = APPLE_PRORES_422LT_NAME; bitsPerComponent = 10; break; case NukeCodecs::CodecFactory::eProResCodec422Proxy: codecName = APPLE_PRORES_422PROXY_NAME; bitsPerComponent = 10; break; default: break; } } else if (AV_CODEC_ID_DNXHD == stream->_codecContext->codec_id) { codecName = AVID_DNXHD_NAME; // |bitsPerComponent| is just for display purposes so // report the data from the movie. Note that for DNxHD // the actual bits per sample is forced to 10 for // reasons described above. The following will ensure // that the correct bit depth is displayed irrespective // of the internal shennigans of this reader. bitsPerComponent = stream->_codecContext->bits_per_raw_sample; } else #endif //Add metadata sourced from non-meta sources in ffmpeg. { codecName = Foundry::Nuke::getCodecOverridenLongName( stream->_videoCodec->name ); if (0 == strcmp(codecName, "")) { codecName = stream->_videoCodec->name; } } if (codecName) metadata.setData(MetaData::QuickTime::CODEC_NAME, codecName); if (stream->_codecContext->codec_tag) { char codecID[32]; av_get_codec_tag_string(codecID, sizeof(codecID), stream->_codecContext->codec_tag); if (0 != strcmp(codecID, "")) { metadata.setData(MetaData::QuickTime::CODEC_ID, codecID); } } metadata.setData(MetaData::DEPTH, MetaData::DEPTH_FIXED(bitsPerComponent)); metadata.setData(MetaData::FRAME_RATE, double(stream->_fpsNum) / double(stream->_fpsDen)); //trim to a couple of degrees of precision. float ar = ((int)(Stream::GetStreamAspectRatio(stream) * 100 + .5) / 100.0); metadata.setData(MetaData::PIXEL_ASPECT, ar); //Add special cased metadata that we can't add as part of the subsequent main loop. // The predefined 'MetaData::TIMECODE' tag will be picked up by // Nuke/NukeStudio as the timecode for the clip. // There are a number of methods to retrieve the timecode using // the FFmpeg libraries and are listed here in case the current // method does not cover all cases. // * Use av_dict_get() on AVFormatContext::metadata (current/preferred method). // * Use av_dict_get() on video stream AVStream::metadata // * Use av_dict_get() on data stream AVStream::metadata // * Read the timecode sample from the data stream using // av_read_frame, check the AVPacket::stream_index matches // the data stream and decode/translate the value to a // time code value. // // First attempt to extract timecode from the format context. If // this does not yield any timecode information, try the video // stream. // // The official reader produces per frame TC, rather than first frame only. // for consistency we need to think about doing the same thing here. AVDictionaryEntry* avDictionaryEntry = av_dict_get(_context->metadata, "timecode", NULL, 0); if (!avDictionaryEntry) avDictionaryEntry = av_dict_get(stream->_avstream->metadata, "timecode", NULL, 0); if (avDictionaryEntry) metadata.setData(MetaData::TIMECODE, avDictionaryEntry->value); // The following has been removed as |timecode_frame_start| is unreliable. // This has been replaced by search for a 'timecode' key in the format // metadata, r.e. SetMetaDataItem() with MeteData::TIMECODE above. //metadata.setData("mov64/codec/timecodeFrameStart", (unsigned int)stream->_codecContext->timecode_frame_start); // Similarly startTime has been removed as mismatches with official quicktime. //metadata.setData("mov64/codec/startTime", (unsigned int)_context->start_time); //In practise official quicktime may add a com.apple.quicktime.starttime meta key. //We'd need to convert the TC source above with the fps to give us the equivalent //as both timecode_frame_start and start_time are unreliable in testing. AVDictionaryEntry* t; t = av_dict_get(stream->_avstream->metadata, kNCLCPrimariesKey, NULL, AV_DICT_IGNORE_SUFFIX); if (t) { metadata.setData(MetaData::QuickTime::NCLC_PRIMARIES,GetNCLCPrimariesLabelFromIndex(atoi(t->value))); t = NULL; } t = av_dict_get(stream->_avstream->metadata, kNCLCTransferKey, NULL, AV_DICT_IGNORE_SUFFIX); if (t) { metadata.setData(MetaData::QuickTime::NCLC_TRANSFER,GetNCLCTransferLabelFromIndex(atoi(t->value))); t = NULL; } t = av_dict_get(stream->_avstream->metadata, kNCLCMatrixKey, NULL, AV_DICT_IGNORE_SUFFIX); if (t) { metadata.setData(MetaData::QuickTime::NCLC_MATRIX,GetNCLCMatrixLabelFromIndex(atoi(t->value))); t = NULL; } //Add optional top level meta when found that'll also be duplicated to a general place in the subsequent loop. t = av_dict_get(_context->metadata, "com.arri.camera.ReelName", NULL, AV_DICT_IGNORE_SUFFIX); if (t) { metadata.setData(MetaData::QuickTime::REEL, t->value); t = NULL; } //Add in all arbitrary metadata from the stream context, renaming/formatting special cases. while ((t = av_dict_get(stream->_avstream->metadata, "", t, AV_DICT_IGNORE_SUFFIX)) != NULL) { ValidateAndSetMetaDataItem(metadata, t->key, t->value, stream->_doNotAttachPrefix, stream->_matchMetaFormat); } //Add in all arbitrary metadata from the format context, renaming/formatting special cases. while ((t = av_dict_get(_context->metadata, "", t, AV_DICT_IGNORE_SUFFIX)) != NULL) { ValidateAndSetMetaDataItem(metadata, t->key, t->value, stream->_doNotAttachPrefix, stream->_matchMetaFormat); } return true; } // get stream information bool FFmpegFile::info( int& width, int& height, double& aspect, int& frames, size_t streamIdx) { Guard guard(_lock); if (streamIdx >= _streams.size()) return false; // get the stream Stream* stream = _streams[streamIdx]; width = stream->_width; height = stream->_height; aspect = stream->_aspect; frames = stream->_frames; return true; } int FFmpegFile::getRowSize() const { // returns 0 if no stream const int bitDepth = getBitDepth(); if (bitDepth > 8) { return getNumberOfComponents() * getWidth() * sizeof(U16); } return getNumberOfComponents() * getWidth(); } int FFmpegFile::getBufferSize() const { return getRowSize() * getHeight(); } //// FFmpegFileManager //// Keeps track of all FFmpegFile mapped against file name. // A lock manager function for FFmpeg, enabling it to use mutexes managed by this reader. Pass to av_lockmgr_register(). int FFmpegFileManager::FFmpegLockManager(void** mutex, enum AVLockOp op) { switch (op) { case AV_LOCK_CREATE: // Create a mutex. try { *mutex = static_cast< void* >(new DD::Image::Lock()); return 0; } catch(...) { // Return error if mutex creation failed. return 1; } case AV_LOCK_OBTAIN: // Lock the specified mutex. try { static_cast< DD::Image::Lock* >(*mutex)->lock(); return 0; } catch(...) { // Return error if mutex lock failed. return 1; } case AV_LOCK_RELEASE: // Unlock the specified mutex. // Mutex unlock can't fail. static_cast< DD::Image::Lock* >(*mutex)->unlock(); return 0; case AV_LOCK_DESTROY: // Destroy the specified mutex. // Mutex destruction can't fail. delete static_cast< DD::Image::Lock* >(*mutex); *mutex = 0; return 0; default: // Unknown operation. mFnAssert(false); return 1; } } // singleton static FFmpegFileManager s_readerManager; // constructor FFmpegFileManager::FFmpegFileManager() { av_log_set_level(AV_LOG_ERROR); av_register_all(); // Register a lock manager callback with FFmpeg, providing it the ability to use mutex locking around // otherwise non-thread-safe calls. av_lockmgr_register(FFmpegLockManager); } // get a specific reader FFmpegFile::Ptr FFmpegFileManager::get(const char* filename) { // For performance reason and for different use cases Hero prefer to decode multiple time the same file. // That means allocating more resources by gurantee best performance when the application needs // to seek in consecutive way from different position in the same video sequence. #if defined(FN_HIERO) FFmpegFile::Ptr retVal; retVal.allocate(filename); return retVal; #else Guard guard(_lock); FFmpegFile::Ptr retVal; ReaderMap::iterator it = _readers.find(filename); if (it == _readers.end()) { retVal.allocate(filename); _readers.insert(std::make_pair(std::string(filename), retVal)); } else { retVal = it->second; } return retVal; #endif } // release a specific reader void FFmpegFileManager::release(const char* filename) { #if !defined(FN_HIERO) Guard guard(_lock); ReaderMap::iterator it = _readers.find(filename); if (it != _readers.end()) { if (it->second.refcount() == 1) { _readers.erase(it); } } #endif } FFmpegFileManager FFmpegFileManager::s_readerManager; /////////////////////////////////////////////////////////////////////////// /// mov64Reader void mov64Reader::memoryInfo(Memory::MemoryInfoArray& output, const void* restrict_to) const { if (restrict_to && iop->node() != (const Node*)restrict_to) return; Memory::MemoryInfo memInfo = Memory::MemoryInfo(iop, _data.size()); std::stringstream buf; buf << w() << "x" << h(); memInfo.addUserData("ImgSize", buf); output.push_back(memInfo); } /////////////////////////////////////////////////////// /// mov64ReaderFormat mov64ReaderFormat::mov64ReaderFormat(Read* iop) : _iop(iop) , _colorMatrixType(0) , _doNotAttachPrefix(false) , _matchMetaFormat(true) , _defaultColorMatrixType(0) { // Create labels for all the colour matrices, based on mov32Reader behaviour // This enables the default to change without affecting the knob value int i = 0; // Add a placeholder default _colourMatrixTypeLabels[i++] = kDefaultColorMatrixLabel; // Set the matrix labels from Foundry::Color for ( ; i < eNumYCbCrEncodings + kNumDefaultLabels; i++) { _colourMatrixTypeLabels[i] = YCbCrEncodingNames[i - kNumDefaultLabels]; } // Add a list delimiter _colourMatrixTypeLabels[i++] = NULL; mFnAssertMsg( i == mFnSizeofArray(_colourMatrixTypeLabels), "Color matrix labels not initialised correctly" ); } void mov64ReaderFormat::knobs(Knob_Callback c) { Obsolete_knob(c, "ycbcr_matrix", "knob mov64_ycbcr_matrix $value"); Enumeration_knob(c, &_colorMatrixType, _colourMatrixTypeLabels, "mov64_ycbcr_matrix", "ycbcr matrix"); SetFlags(c, Knob::NO_ANIMATION); Tooltip(c, "Choose color matrix used to convert from YCbCr to RGB. " "Only enabled when working with a YCbCr based pixel type. "); Named_Text_knob(c, "mov64_metadata", "metadata"); Bool_knob(c, &_doNotAttachPrefix, "mov64_no_prefix", "do not attach prefix"); Tooltip(c, "By default the 'quicktime' prefix is attached to general metadata keys to make it " "distinct from other metadata in the tree.\n\nEnable this option to read the general " "metadata 'as is' without attaching the prefix. Note that quicktime container " "specific metadata will still be inserted with the prefix."); SetFlags(c, Knob::NO_ANIMATION); Bool_knob(c, &_matchMetaFormat, "mov64_match_meta_format", "match key format"); Tooltip(c, "Attempts to clean up Quicktime meta keys to match standard Nuke metadata formatting.\n\n" "This will strip reverse DNS key names where possible, and place sub domains into child " "metadata namespaces."); SetFlags(c, Knob::NO_ANIMATION); ClearFlags(c, Knob::STARTLINE); } void mov64ReaderFormat::append(Hash& hash) { hash.append(_colorMatrixType); hash.append(_doNotAttachPrefix); hash.append(_matchMetaFormat); } void mov64ReaderFormat::initDynamicKnobs() { Knob* colorMatrixKnob = _iop->knob("mov64_ycbcr_matrix"); if (colorMatrixKnob) updateColorMatrixKnob(colorMatrixKnob); } void mov64ReaderFormat::setYCbCrMatrixEnabled(bool enabled) { Knob* colorMatrixKnob = _iop->knob("mov64_ycbcr_matrix"); if (colorMatrixKnob) { if (enabled) { colorMatrixKnob->enable(); } else { colorMatrixKnob->disable(); } } } void mov64ReaderFormat::updateColorMatrixKnob(DD::Image::Knob* colorMatrixKnob) { // Get the enumeration knob mFnAssert(colorMatrixKnob); Enumeration_KnobI* colorMatrixEnumerationKnob = colorMatrixKnob->enumerationKnob(); if (colorMatrixEnumerationKnob) { // Change the menu items // StringArray colorMatrices = colorMatrixEnumerationKnob->menu(); // Get the label if the value is valid for this list std::string label = GetKnobLabel(colorMatrixKnob, colorMatrices); const size_t arraySize = colorMatrices.size(); const size_t labelIndex = _defaultColorMatrixType; // 1 = Rec709, 2 = Rec601; mFnAssert(arraySize > labelIndex); const char* defaultColorMatrixName = YCbCrEncodingNames[labelIndex]; colorMatrices[0] = std::string(kDefaultLabel_Prefix) + std::string(defaultColorMatrixName) + std::string(kDefaultLabel_Suffix); colorMatrixEnumerationKnob->menu(colorMatrices); // Set the knob value using the new list SetKnobValue(colorMatrixKnob, colorMatrices, label); } } //////////////////////////////////////////////////////////////////////////////// mov64Reader::mov64Reader(Read* iop, const char* optionalFilename) : Reader(iop) , IBufferFillHeap("mov64Reader") , _data(this) , _memNeeded(0) , _numFrames(0) , _currentFrame(0) , _optionalFilename(optionalFilename) , _planarReadBuffer(0) { // get the reader from the FFmpeg reader, using optional filename if supplied const char* filename = _optionalFilename ? _optionalFilename : iop->filename(); _reader = FFmpegFileManager::s_readerManager.get(filename); if (_reader->invalid()) { const bool componentOfmovReader = (_optionalFilename != NULL); if (componentOfmovReader) { // Originally we gave a warning here, but warnings seem unpopular in the nuke universe. } else { iop->internalError(_reader->error()); } return; } // set iop info size int width, height, frames; double aspect; // get metadata from file if (_reader->info(width, height, aspect, frames)) { const int numberOfComponents = _reader->getNumberOfComponents(); info_.channels((numberOfComponents == 4) ? Mask_RGBA : Mask_RGB); set_info(width, height, numberOfComponents, aspect); info_.first_frame(1); info_.last_frame(frames); _numFrames = frames; _memNeeded = _reader->getBufferSize(); } // Find the ReaderFormat, to set the YCbCr colorspace // Note that preview will not create a ReaderFormat FileHandler* fileHandler = iop->handler(); mov64ReaderFormat* readerFormat = fileHandler ? dynamic_cast(fileHandler) : NULL; #ifndef FN_OS_LINUX // If this is a component of the movReader, then filehandler will be a movReaderFormat instead if (!readerFormat) { movReaderFormat* readerFormatMeta = fileHandler ? dynamic_cast(fileHandler) : NULL; if (readerFormatMeta) { // Get the internal reader format to use here readerFormat = readerFormatMeta->getMov64ReaderFormat(); } } #endif const bool inPreview = (readerFormat == NULL); if (!inPreview) { // YCbCr Matrix knob only enabled for YUV pixel formats if (_reader->isYUV()) { readerFormat->setYCbCrMatrixEnabled(true); // YCbCrMatrix is based on image height const int defaultColorMatrixType = _reader->isRec709Format() ? eYCbCrEncodingRec709 : eYCbCrEncodingRec601; readerFormat->setDefaultColorMatrixType(defaultColorMatrixType); // Based on Enumeration_knob choice const int currentColorMatrixType = readerFormat->getColorMatrixType(); _reader->setColorMatrixTypeOverride(currentColorMatrixType); } else { // Disable for RGB formats readerFormat->setYCbCrMatrixEnabled(false); } const bool doNotAttachPrefix = readerFormat->getDoNotAttachPrefix(); _reader->setDoNotAttachPrefix(doNotAttachPrefix); const bool matchMetaFormat = readerFormat->getMatchMetaFormat(); _reader->setMatchMetaFormat(matchMetaFormat); } // get stream 0 metadata _reader->metadata(meta); // set buffer size _data.resize(_memNeeded); // Set colorspace knob based on pixel format and errata. lut_ = LUT::builtin(_reader->getColorspace()); } mov64Reader::~mov64Reader() { // unref the reader _reader.clear(); // release the reader if is not used anymore const char* filename = _optionalFilename ? _optionalFilename : iop->filename(); FFmpegFileManager::s_readerManager.release(filename); delete[] _planarReadBuffer; } bool mov64Reader::invalid() { if (_reader) { return _reader->invalid(); } return true; } bool mov64Reader::fillBuffer(void* buffer) { if (buffer == NULL) { iop->internalError("mov64Reader has run out of memory", _memNeeded); return false; } // Translate from the 1-based frames expected by Nuke to 0-based frame offsets used during decoding if (!_reader->decode( static_cast(buffer), frame() - 1 )) { iop->internalError(_reader->error()); return false; } return true; } void mov64Reader::engine(int y, int x, int rx, ChannelMask channels, Row& out) { MemoryBufferGuard guard(_data); if (!guard.buffer()) { // for some reason the buffer is not available and the Iop error // is already called inside fillBuffer method. return; } if (_reader->getBitDepth() > 8) { engine16(y, x, rx, channels, out, static_cast(guard.buffer())); } else { engine8(y, x, rx, channels, out, static_cast(guard.buffer())); } } void mov64Reader::engine8(int y, int x, int rx, ChannelMask channels, Row& out, unsigned char* data) { int numberOfComponents = _reader->getNumberOfComponents(); foreach ( z, channels ) { float* TO = out.writable(z) + x; int componentPosition = _reader->getComponentPosition(z); // In-place RGBA swizzle. unsigned char* FROM = data + componentPosition; FROM += (height() - y - 1) * width() * numberOfComponents; FROM += x * numberOfComponents; from_byte(z, TO, FROM, NULL, rx - x, numberOfComponents); } } void mov64Reader::engine16(int y, int x, int rx, ChannelMask channels, Row& out, U16* data) { int numberOfComponents = _reader->getNumberOfComponents(); foreach ( z, channels ) { float* TO = out.writable(z) + x; int componentPosition = _reader->getComponentPosition(z); U16* FROM = data + componentPosition; // In-place RGBA swizzle. FROM += (height() - y - 1) * width() * numberOfComponents; FROM += x * numberOfComponents; from_short(z, TO, FROM, NULL, rx - x, 16, numberOfComponents); } } void mov64Reader::open() { int f = frame(); if (f < info_.first_frame()) f = info_.first_frame(); if (f > info_.last_frame()) f = info_.last_frame(); if (f != _currentFrame) _data.invalidate(); _currentFrame = f; } #ifdef NUKE_MOV64_PLANAR //////////////////////////////////////////////////////////////////////////////// // Get information about the planar image, including the size of the buffer need // to store the encoded image and the ideal decoded image format for // the specified channel. PlanarReadInfo mov64Reader::planarReadInfo(const ChannelSet &channels) { // Map from the LUT to a ColorCurveEnum to report the correct colour curve to // Hiero. ColorCurveEnum colorCurveType = eColorCurveUnknown; if (lut_ == LUT::builtin("linear")) colorCurveType = eColorCurveLinear; else if (lut_ == LUT::builtin("sRGB")) colorCurveType = eColorCurveSRGB; else if (lut_ == LUT::builtin("rec709")) colorCurveType = eColorCurveRec709; else if (lut_ == LUT::builtin("Cineon")) colorCurveType = eColorCurveLog; else if (lut_ == LUT::builtin("Gamma1.8")) colorCurveType = eColorCurveGamma18; else if (lut_ == LUT::builtin("Gamma2.2")) colorCurveType = eColorCurveGamma22; else if (lut_ == LUT::builtin("Gamma2.4")) colorCurveType = eColorCurveGamma24; else if (lut_ == LUT::builtin("Panalog")) colorCurveType = eColorCurvePanalog; else if (lut_ == LUT::builtin("REDLog")) colorCurveType = eColorCurveREDLog; else if (lut_ == LUT::builtin("ViperLog")) colorCurveType = eColorCurveViperLog; else if (lut_ == LUT::builtin("AlexaV3LogC")) colorCurveType = eColorCurveAlexaV3LogC; else if (lut_ == LUT::builtin("PLogLin")) colorCurveType = eColorCurvePLogLin; else if (lut_ == LUT::builtin("SLog")) colorCurveType = eColorCurveSLog; else if (lut_ == LUT::builtin("SLog1")) colorCurveType = eColorCurveSLog1; else if (lut_ == LUT::builtin("SLog2")) colorCurveType = eColorCurveSLog2; else if (lut_ == LUT::builtin("SLog3")) colorCurveType = eColorCurveSLog3; else if (lut_ == LUT::builtin("CLog")) colorCurveType = eColorCurveCLog; else if (lut_ == LUT::builtin("Protune")) colorCurveType = eColorCurveProtune; const int bitDepth = _reader->getBitDepth(); DataTypeEnum dataType = eDataTypeNone; int minValue = 0; int maxValue = (1 << bitDepth) - 1; int whitePoint = 1; ChannelSet srcChannels = channels; int numberOfComponents = _reader->getNumberOfComponents(); switch (bitDepth) { case 8: dataType = eDataTypeUInt8; break; case 10: case 12: case 14: case 16: dataType = eDataTypeUInt16; break; default: mFnAssertMsg(false, "Unsupported bit depth:" << bitDepth); break; } DD::Image::Box bounds(0, 0, w(), h()); ImagePlaneDescriptor baseDesc(bounds, true, srcChannels, numberOfComponents); DataInfo dataInfo(dataType, true, minValue, maxValue, whitePoint); GenericImagePlaneDescriptor desc(baseDesc, dataInfo, colorCurveType); // Currently passing 0 as readPassBufferSize. // In future it may get used by Core/Media to determine that it should allocate a buffer itself. return PlanarReadInfo(desc, /* readPassBufferSize = */ 0, /* isDecodeThreadable */ false, /* isValid */ true); } //////////////////////////////////////////////////////////////////////////////// // Planar read and decode the specified channels of the image in one go. void mov64Reader::planarReadAndDecode(GenericImagePlane& image, const ChannelSet& channels) { if (!_planarReadBuffer) { _planarReadBuffer = new uint8_t[_reader->getBufferSize()]; } planarReadPass(_planarReadBuffer, channels); planarDecodePass(_planarReadBuffer, image, channels, 0, 1); } //////////////////////////////////////////////////////////////////////////////// // Planar read the specified channels of the whole file into the buffer in one // go, returning the number of bytes read. int mov64Reader::planarReadPass(void* buffer, const ChannelSet& channels) { fillBuffer(buffer); return _reader->getBufferSize(); } //////////////////////////////////////////////////////////////////////////////// template void convert(GenericImagePlane& image, int dstLine, int dstLineInc, const uint8_t* srcData, long srcRowBytes, int width, int height, int maxValue, uint8_t* componentPosition) { // We have asked ffmpeg to give us either 8 or 16 bit data. // ffmpeg performs an internal conversion to RGB24 or RGB48LE. // While this is convenient as ffmpeg handles the myriad of incoming pixel formats, // it does suggest when using the planar path we could avoid this extra copy and // do the conversion here ourselves, achieving another speed up. int redIndex = componentPosition[0]; int greenIndex = componentPosition[1]; int blueIndex = componentPosition[2]; int alphaIndex = componentPosition[3]; int line = 0; while (height > line) { // const T* src = srcData; const T* src = reinterpret_cast(srcData); srcData += srcRowBytes; T* dst = &(image.writableAt(0, dstLine, 0)); int pixel = width; while (pixel--) { *dst++ = src[redIndex]; *dst++ = src[greenIndex]; *dst++ = src[blueIndex]; T value; if (srcHasAlpha) { value = src[alphaIndex]; src += 4; } else { value = maxValue; src += 3; } if (dstHasAlpha) *dst++ = value; } ++line; dstLine += dstLineInc; } } //////////////////////////////////////////////////////////////////////////////// // Do a planar decode of the image data in the specified source buffer into the // GenericImagePlane, for the specified channels. void mov64Reader::planarDecodePass(void* srcBuffer, GenericImagePlane& image, const ChannelSet& channels, int threadIndex, int nDecodeThreads) { mFnAssert(threadIndex >= 0); mFnAssert(threadIndex < nDecodeThreads); mFnAssert(nDecodeThreads > 0); // Assert that the image plane we're filling in is configured to hold the // whole image, not a sub area of it. Although the dpx format supports // specifying the offset and size of the 'original' image (in the orientation // header) we don't bother with that, either in this new planar read/decode // code or in the original. mFnAssert(image.desc().bounds().x() == 0); mFnAssert(image.desc().bounds().y() == 0); mFnAssert(image.desc().bounds().w() == w()); mFnAssert(image.desc().bounds().h() == h()); int readerHeight = _reader->getHeight(); int height = (readerHeight + (nDecodeThreads - 1)) / nDecodeThreads; // Number of lines for this thread to process. int width = _reader->getWidth(); // Determine the offset of the first line for this thread to // convert. Simply use the thread index and buffer rowbytes // to calculate the byte offset into the source buffer. long srcRowBytes = _reader->getRowSize(); const uint8_t* srcData = static_cast(srcBuffer) + (threadIndex * height * srcRowBytes); // Determine the location in the destination buffer. // If flipping vertically, move in reverse from the // last line to the first line of the destination // buffer. If multiple threads are in use, skip the // rows that are processed by other threads. int dstLine, dstLineInc; const bool flipVertically = true; if (flipVertically) { dstLine = (readerHeight - 1) - (threadIndex * height); dstLineInc = -1; } else { dstLine = threadIndex * height; dstLineInc = 1; } const int numberOfComponents = _reader->getNumberOfComponents(); const bool srcHasAlpha = (4 == numberOfComponents) ? true : false; const bool dstHasAlpha = (image.desc().channels() == Mask_RGBA) ? true : false; uint8_t componentPositionLUT[4]; componentPositionLUT[0] = _reader->getComponentPosition(Mask_Red); componentPositionLUT[1] = _reader->getComponentPosition(Mask_Green); componentPositionLUT[2] = _reader->getComponentPosition(Mask_Blue); componentPositionLUT[3] = _reader->getComponentPosition(Mask_Alpha); // The entire frame is divided amoung the worker threads. The frame // may not be an integer mulitple of the number of threads. This // means that one of the threads will process fewer lines than the // other threads. Test here for that condition and set the number // of lines appropriately. int linesRemaining = readerHeight - (height * threadIndex); if (height > linesRemaining) height = linesRemaining; int bitDepth = _reader->getBitDepth(); int maxValue = (1 << bitDepth) - 1; if (8 == bitDepth) { if (srcHasAlpha && dstHasAlpha) convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); else if (srcHasAlpha && !dstHasAlpha) convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); else if (!srcHasAlpha && dstHasAlpha) convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); else convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); } else if (16 >= bitDepth) { if (srcHasAlpha && dstHasAlpha) convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); else if (srcHasAlpha && !dstHasAlpha) convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); else if (!srcHasAlpha && dstHasAlpha) convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); else convert(image, dstLine, dstLineInc, srcData, srcRowBytes, width, height, maxValue, componentPositionLUT); } } #endif // NUKE_MOV64_PLANAR //////////////////////////////////////////////////////////////////// static Reader* Build(Read* iop, int fd, const unsigned char* b, int n) { ::close(fd); return new mov64Reader(iop); } static ReaderFormat* BuildFormat(Read* iop) { return new mov64ReaderFormat(iop); } static bool Test(int fd, const unsigned char* block, int length) { return true; } #if FN_OS_LINUX const Reader::Description mov64Reader::d("mov\0mov64\0m4v\0mp4\0ffmpeg\0", "mov64", Build, Test, BuildFormat); #else const Reader::Description mov64Reader::d("mov64\0ffmpeg\0", "mov64", Build, Test, BuildFormat); #endif