// Created by: Kirill GAVRILOV // Copyright (c) 2019 OPEN CASCADE SAS // // This file is part of Open CASCADE Technology software library. // // This library is free software; you can redistribute it and/or modify it under // the terms of the GNU Lesser General Public License version 2.1 as published // by the Free Software Foundation, with special exception defined in the file // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT // distribution for complete text of the license and disclaimer of any warranty. // // Alternatively, this file may be used under the terms of Open CASCADE // commercial license or contractual agreement. // activate some C99 macros like UINT64_C in "stdint.h" which used by FFmpeg #ifndef __STDC_CONSTANT_MACROS #define __STDC_CONSTANT_MACROS #endif #include #include #include #ifdef HAVE_FFMPEG #include extern "C" { #include }; #include #endif IMPLEMENT_STANDARD_RTTIEXT(Media_FormatContext, Standard_Transient) namespace { static const double THE_SECONDS_IN_HOUR = 3600.0; static const double THE_SECONDS_IN_MINUTE = 60.0; static const double THE_SECOND_IN_HOUR = 1.0 / THE_SECONDS_IN_HOUR; static const double THE_SECOND_IN_MINUTE = 1.0 / THE_SECONDS_IN_MINUTE; #ifdef HAVE_FFMPEG static const AVRational ST_AV_TIME_BASE_Q = {1, AV_TIME_BASE}; static const double ST_AV_TIME_BASE_D = av_q2d (ST_AV_TIME_BASE_Q); //! Format framerate value. static TCollection_AsciiString formatFps (double theVal) { const uint64_t aVal = uint64_t(theVal * 100.0 + 0.5); char aBuff[256]; if(aVal == 0) { Sprintf (aBuff, "%1.4f", theVal); } else if (aVal % 100) { Sprintf (aBuff, "%3.2f", theVal); } else if (aVal % (100 * 1000)) { Sprintf (aBuff, "%1.0f", theVal); } else { Sprintf (aBuff, "%1.0fk", theVal / 1000); } return aBuff; } #endif } // ======================================================================= // function : FormatAVErrorDescription // purpose : // ======================================================================= TCollection_AsciiString Media_FormatContext::FormatAVErrorDescription (int theErrCodeAV) { #ifdef HAVE_FFMPEG char aBuff[4096]; memset (aBuff, 0, sizeof(aBuff)); if (av_strerror (theErrCodeAV, aBuff, 4096) != -1) { return TCollection_AsciiString (aBuff); } #ifdef _MSC_VER wchar_t aBuffW[4096]; memset (aBuffW, 0, sizeof(aBuffW)); if (_wcserror_s (aBuffW, 4096, AVUNERROR(theErrCodeAV)) == 0) { return TCollection_AsciiString (aBuffW); } #elif defined(_WIN32) // MinGW has only thread-unsafe variant char* anErrDesc = strerror (AVUNERROR(theErrCodeAV)); if (anErrDesc != NULL) { return TCollection_AsciiString (anErrDesc); } #endif return TCollection_AsciiString (aBuff); #else return TCollection_AsciiString ("AVError #") + theErrCodeAV; #endif } // ======================================================================= // function : FormatUnitsToSeconds // purpose : // ======================================================================= double Media_FormatContext::FormatUnitsToSeconds (int64_t theTimeUnits) { #ifdef HAVE_FFMPEG return (theTimeUnits != AV_NOPTS_VALUE) ? (ST_AV_TIME_BASE_D * theTimeUnits) : 0.0; #else (void )theTimeUnits; return 0.0; #endif } // ======================================================================= // function : UnitsToSeconds // purpose : // ======================================================================= double Media_FormatContext::UnitsToSeconds (const AVRational& theTimeBase, int64_t theTimeUnits) { #ifdef HAVE_FFMPEG return (theTimeUnits != AV_NOPTS_VALUE) ? (av_q2d (theTimeBase) * theTimeUnits) : 0.0; #else (void )&theTimeBase; (void )theTimeUnits; return 0.0; #endif } // ======================================================================= // function : StreamUnitsToSeconds // purpose : // ======================================================================= double Media_FormatContext::StreamUnitsToSeconds (const AVStream& theStream, int64_t theTimeUnits) { #ifdef HAVE_FFMPEG return UnitsToSeconds (theStream.time_base, theTimeUnits); #else (void )&theStream; (void )theTimeUnits; return 0.0; #endif } // ======================================================================= // function : SecondsToUnits // purpose : // ======================================================================= int64_t Media_FormatContext::SecondsToUnits (double theTimeSeconds) { #ifdef HAVE_FFMPEG return int64_t(theTimeSeconds / ST_AV_TIME_BASE_D); #else (void )theTimeSeconds; return 0; #endif } // ======================================================================= // function : SecondsToUnits // purpose : // ======================================================================= int64_t Media_FormatContext::SecondsToUnits (const AVRational& theTimeBase, double theTimeSeconds) { #ifdef HAVE_FFMPEG return int64_t(theTimeSeconds / av_q2d (theTimeBase)); #else (void )&theTimeBase; (void )theTimeSeconds; return 0; #endif } // ======================================================================= // function : Media_FormatContext // purpose : // ======================================================================= int64_t Media_FormatContext::StreamSecondsToUnits (const AVStream& theStream, double theTimeSeconds) { #ifdef HAVE_FFMPEG return SecondsToUnits (theStream.time_base, theTimeSeconds); #else (void )&theStream; (void )theTimeSeconds; return 0; #endif } // ======================================================================= // function : Media_FormatContext // purpose : // ======================================================================= Media_FormatContext::Media_FormatContext() : myFormatCtx (NULL), myPtsStartBase(0.0), myDuration (0.0) { // } // ======================================================================= // function : ~Media_FormatContext // purpose : // ======================================================================= Media_FormatContext::~Media_FormatContext() { Close(); } // ======================================================================= // function : NbSteams // purpose : // ======================================================================= unsigned int Media_FormatContext::NbSteams() const { #ifdef HAVE_FFMPEG return myFormatCtx->nb_streams; #else return 0; #endif } // ======================================================================= // function : Stream // purpose : // ======================================================================= const AVStream& Media_FormatContext::Stream (unsigned int theIndex) const { #ifdef HAVE_FFMPEG return *myFormatCtx->streams[theIndex]; #else (void )theIndex; throw Standard_ProgramError("Media_FormatContext::Stream()"); #endif } // ======================================================================= // function : OpenInput // purpose : // ======================================================================= bool Media_FormatContext::OpenInput (const TCollection_AsciiString& theInput) { #ifdef HAVE_FFMPEG const int avErrCode = avformat_open_input (&myFormatCtx, theInput.ToCString(), NULL, NULL); if (avErrCode != 0) { Message::SendFail (TCollection_AsciiString ("FFmpeg: Couldn't open video file '") + theInput + "'\nError: " + FormatAVErrorDescription (avErrCode)); Close(); return false; } // retrieve stream information if (avformat_find_stream_info (myFormatCtx, NULL) < 0) { Message::SendFail (TCollection_AsciiString ("FFmpeg: Couldn't find stream information in '") + theInput + "'"); Close(); return false; } #ifdef _DEBUG av_dump_format (myFormatCtx, 0, theInput.ToCString(), false); #endif myDuration = 0.0; myPtsStartBase = 0.0; TCollection_AsciiString anExt = theInput; anExt.LowerCase(); if (anExt.EndsWith (".png") || anExt.EndsWith (".jpg") || anExt.EndsWith (".jpeg") || anExt.EndsWith (".mpo") || anExt.EndsWith (".bmp") || anExt.EndsWith (".tif") || anExt.EndsWith (".tiff")) { // black-list images to workaround non-zero duration return true; } myDuration = FormatUnitsToSeconds (myFormatCtx->duration); if (myFormatCtx->nb_streams != 0) { myPtsStartBase = 2.e+100; for (unsigned int aStreamId = 0; aStreamId < myFormatCtx->nb_streams; ++aStreamId) { const AVStream& aStream = *myFormatCtx->streams[aStreamId]; myPtsStartBase = Min (myPtsStartBase, StreamUnitsToSeconds (aStream, aStream.start_time)); myDuration = Max (myDuration, StreamUnitsToSeconds (aStream, aStream.duration)); } } return true; #else Message::SendFail ("Error: FFmpeg library is unavailable"); (void )theInput; return false; #endif } // ======================================================================= // function : Close // purpose : // ======================================================================= void Media_FormatContext::Close() { if (myFormatCtx != NULL) { #ifdef HAVE_FFMPEG avformat_close_input (&myFormatCtx); //avformat_free_context (myFormatCtx); #endif } } // ======================================================================= // function : FormatTime // purpose : // ======================================================================= TCollection_AsciiString Media_FormatContext::FormatTime (double theSeconds) { double aSecIn = theSeconds; unsigned int aHours = (unsigned int )(aSecIn * THE_SECOND_IN_HOUR); aSecIn -= double(aHours) * THE_SECONDS_IN_HOUR; unsigned int aMinutes = (unsigned int )(aSecIn * THE_SECOND_IN_MINUTE); aSecIn -= double(aMinutes) * THE_SECONDS_IN_MINUTE; unsigned int aSeconds = (unsigned int )aSecIn; aSecIn -= double(aSeconds); double aMilliSeconds = 1000.0 * aSecIn; char aBuffer[64]; if (aHours > 0) { Sprintf (aBuffer, "%02u:%02u:%02u", aHours, aMinutes, aSeconds); return aBuffer; } else if (aMinutes > 0) { Sprintf (aBuffer, "%02u:%02u", aMinutes, aSeconds); return aBuffer; } else if (aSeconds > 0) { Sprintf (aBuffer, "%2u s", aSeconds); return aBuffer; } return TCollection_AsciiString (aMilliSeconds) + " ms"; } // ======================================================================= // function : FormatTimeProgress // purpose : // ======================================================================= TCollection_AsciiString Media_FormatContext::FormatTimeProgress (double theProgress, double theDuration) { double aSecIn1 = theProgress; unsigned int aHours1 = (unsigned int )(aSecIn1 * THE_SECOND_IN_HOUR); aSecIn1 -= double(aHours1) * THE_SECONDS_IN_HOUR; unsigned int aMinutes1 = (unsigned int )(aSecIn1 * THE_SECOND_IN_MINUTE); aSecIn1 -= double(aMinutes1) * THE_SECONDS_IN_MINUTE; unsigned int aSeconds1 = (unsigned int )aSecIn1; aSecIn1 -= double(aSeconds1); double aSecIn2 = theDuration; unsigned int aHours2 = (unsigned int )(aSecIn2 * THE_SECOND_IN_HOUR); aSecIn2 -= double(aHours2) * THE_SECONDS_IN_HOUR; unsigned int aMinutes2 = (unsigned int )(aSecIn2 * THE_SECOND_IN_MINUTE); aSecIn2 -= double(aMinutes2) * THE_SECONDS_IN_MINUTE; unsigned int aSeconds2 = (unsigned int )aSecIn2; aSecIn2 -= double(aSeconds2); char aBuffer[256]; if (aHours1 > 0 || aHours2 > 0) { Sprintf (aBuffer, "%02u:%02u:%02u / %02u:%02u:%02u", aHours1, aMinutes1, aSeconds1, aHours2, aMinutes2, aSeconds2); return aBuffer; } Sprintf (aBuffer, "%02u:%02u / %02u:%02u", aMinutes1, aSeconds1, aMinutes2, aSeconds2); return aBuffer; } // ======================================================================= // function : StreamInfo // purpose : // ======================================================================= TCollection_AsciiString Media_FormatContext::StreamInfo (unsigned int theIndex, AVCodecContext* theCodecCtx) const { #ifdef HAVE_FFMPEG const AVStream& aStream = *myFormatCtx->streams[theIndex]; AVCodecContext* aCodecCtx = theCodecCtx; if (aCodecCtx == NULL) { Standard_DISABLE_DEPRECATION_WARNINGS aCodecCtx = aStream.codec; Standard_ENABLE_DEPRECATION_WARNINGS } char aFrmtBuff[4096] = {}; avcodec_string (aFrmtBuff, sizeof(aFrmtBuff), aCodecCtx, 0); TCollection_AsciiString aStreamInfo (aFrmtBuff); if (aStream.sample_aspect_ratio.num && av_cmp_q(aStream.sample_aspect_ratio, aStream.codecpar->sample_aspect_ratio)) { AVRational aDispAspectRatio; av_reduce (&aDispAspectRatio.num, &aDispAspectRatio.den, aStream.codecpar->width * int64_t(aStream.sample_aspect_ratio.num), aStream.codecpar->height * int64_t(aStream.sample_aspect_ratio.den), 1024 * 1024); aStreamInfo = aStreamInfo + ", SAR " + aStream.sample_aspect_ratio.num + ":" + aStream.sample_aspect_ratio.den + " DAR " + aDispAspectRatio.num + ":" + aDispAspectRatio.den; } if (aStream.codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { if (aStream.avg_frame_rate.den != 0 && aStream.avg_frame_rate.num != 0) { aStreamInfo += TCollection_AsciiString(", ") + formatFps (av_q2d (aStream.avg_frame_rate)) + " fps"; } if (aStream.r_frame_rate.den != 0 && aStream.r_frame_rate.num != 0) { aStreamInfo += TCollection_AsciiString(", ") + formatFps (av_q2d (aStream.r_frame_rate)) + " tbr"; } if (aStream.time_base.den != 0 && aStream.time_base.num != 0) { aStreamInfo += TCollection_AsciiString(", ") + formatFps(1 / av_q2d (aStream.time_base)) + " tbn"; } if (aCodecCtx->time_base.den != 0 && aCodecCtx->time_base.num != 0) { aStreamInfo += TCollection_AsciiString(", ") + formatFps(1 / av_q2d (aCodecCtx->time_base)) + " tbc"; } } if (myDuration > 0.0) { aStreamInfo += TCollection_AsciiString(", duration: ") + FormatTime (myDuration); } return aStreamInfo; #else (void )theIndex; (void )theCodecCtx; return TCollection_AsciiString(); #endif } // ======================================================================= // function : ReadPacket // purpose : // ======================================================================= bool Media_FormatContext::ReadPacket (const Handle(Media_Packet)& thePacket) { if (thePacket.IsNull()) { return false; } #ifdef HAVE_FFMPEG return av_read_frame (myFormatCtx, thePacket->ChangePacket()) >= 0; #else return false; #endif } // ======================================================================= // function : SeekStream // purpose : // ======================================================================= bool Media_FormatContext::SeekStream (unsigned int theStreamId, double theSeekPts, bool theToSeekBack) { #ifdef HAVE_FFMPEG const int aFlags = theToSeekBack ? AVSEEK_FLAG_BACKWARD : 0; AVStream& aStream = *myFormatCtx->streams[theStreamId]; if ((aStream.disposition & AV_DISPOSITION_ATTACHED_PIC) != 0) { return false; } int64_t aSeekTarget = StreamSecondsToUnits (aStream, theSeekPts + StreamUnitsToSeconds (aStream, aStream.start_time)); bool isSeekDone = av_seek_frame (myFormatCtx, theStreamId, aSeekTarget, aFlags) >= 0; // try 10 more times in backward direction to work-around huge duration between key frames // will not work for some streams with undefined cur_dts (AV_NOPTS_VALUE)!!! for (int aTries = 10; isSeekDone && theToSeekBack && aTries > 0 && (aStream.cur_dts > aSeekTarget); --aTries) { aSeekTarget -= StreamSecondsToUnits (aStream, 1.0); isSeekDone = av_seek_frame (myFormatCtx, theStreamId, aSeekTarget, aFlags) >= 0; } if (isSeekDone) { return true; } TCollection_AsciiString aStreamType = aStream.codecpar->codec_type == AVMEDIA_TYPE_VIDEO ? "Video" : (aStream.codecpar->codec_type == AVMEDIA_TYPE_AUDIO ? "Audio" : ""); Message::SendWarning (TCollection_AsciiString ("Error while seeking ") + aStreamType + " stream to " + theSeekPts + " sec (" + (theSeekPts + StreamUnitsToSeconds (aStream, aStream.start_time)) + " sec)"); return false; #else (void )theStreamId; (void )theSeekPts; (void )theToSeekBack; return false; #endif } // ======================================================================= // function : Seek // purpose : // ======================================================================= bool Media_FormatContext::Seek (double theSeekPts, bool theToSeekBack) { #ifdef HAVE_FFMPEG const int aFlags = theToSeekBack ? AVSEEK_FLAG_BACKWARD : 0; int64_t aSeekTarget = SecondsToUnits (theSeekPts); if (av_seek_frame (myFormatCtx, -1, aSeekTarget, aFlags) >= 0) { return true; } const char* aFileName = #if(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100)) myFormatCtx->url; #else myFormatCtx->filename; #endif Message::SendWarning (TCollection_AsciiString("Disaster! Seeking to ") + theSeekPts + " [" + aFileName + "] has failed."); return false; #else (void )theSeekPts; (void )theToSeekBack; return false; #endif }