0030612: Visualization - provide texture map with video as image source
[occt.git] / src / Media / Media_FormatContext.cxx
diff --git a/src/Media/Media_FormatContext.cxx b/src/Media/Media_FormatContext.cxx
new file mode 100644 (file)
index 0000000..89b3dc2
--- /dev/null
@@ -0,0 +1,558 @@
+// 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 <Media_FormatContext.hxx>
+
+#include <Message.hxx>
+#include <Message_Messenger.hxx>
+
+#ifdef HAVE_FFMPEG
+#include <Standard_WarningsDisable.hxx>
+extern "C"
+{
+  #include <libavformat/avformat.h>
+};
+#include <Standard_WarningsRestore.hxx>
+#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::DefaultMessenger()->Send (TCollection_AsciiString ("FFmpeg: Couldn't open video file '") + theInput
+                                    + "'\nError: " + FormatAVErrorDescription (avErrCode), Message_Fail);
+    Close();
+    return false;
+  }
+
+  // retrieve stream information
+  if (avformat_find_stream_info (myFormatCtx, NULL) < 0)
+  {
+    Message::DefaultMessenger()->Send (TCollection_AsciiString ("FFmpeg: Couldn't find stream information in '") + theInput + "'", Message_Fail);
+    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::DefaultMessenger()->Send ("Error: FFmpeg library is unavailable", Message_Fail);
+  (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::DefaultMessenger()->Send (TCollection_AsciiString ("Error while seeking ") + aStreamType + " stream to "
+                                   + theSeekPts + " sec (" + (theSeekPts + StreamUnitsToSeconds (aStream, aStream.start_time)) + " sec)",
+                                     Message_Warning);
+  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::DefaultMessenger()->Send (TCollection_AsciiString("Disaster! Seeking to ") + theSeekPts + " [" + aFileName + "] has failed.", Message_Warning);
+  return false;
+#else
+  (void )theSeekPts;
+  (void )theToSeekBack;
+  return false;
+#endif
+}