]> OCCT Git - occt-copy.git/commitdiff
0025382: Visualization, TKOpenGl - improved video recording capability
authorisk <isk@opencascade.com>
Sat, 20 Feb 2016 05:16:21 +0000 (08:16 +0300)
committerisk <isk@opencascade.com>
Fri, 18 Mar 2016 09:45:03 +0000 (12:45 +0300)
OSD_Timer::ElapsedTime() - fix accumulation of the error.
Provide new command vanimation for animation playline definition.

Extend vanimation command with video recorder

* * *
pload MODELING VISUALIZATION
box b1 1 2 3
vinit View1
vclear
vaxo
vsetdispmode 1
vdisplay b1
vfit
vsetlocation b1 0 1 0

proc anim1 {thePts thePtsLoc theName} { vsetlocation b1 [expr sin($thePtsLoc)] cos($thePtsLoc)] 0 }
vanim anim1 -reset -onRedraw anim1
vanim anim  -reset -addSlice 0.0 30.0 anim1
vanim -play anim -playFps 30 -record test.mkv -recWidth 1920 -recHeight 1080

Added command vlogo

Ported on current master.

Removed vlogo command and logo layer item as unused.

src/ViewerTest/ViewerTest.cxx
src/ViewerTest/ViewerTest_AviCommands.cxx

index 13d4166f18fe459404d983ad598c0a029e53b88f..9de0d1464dfccd51a88ecea08e66c4edf3425fe7 100644 (file)
@@ -3896,125 +3896,6 @@ static int VPerf(Draw_Interpretor& di, Standard_Integer , const char** argv) {
   return 0;
 }
 
-
-//==================================================================================
-// Function : VAnimation
-//==================================================================================
-static int VAnimation (Draw_Interpretor& di, Standard_Integer argc, const char** argv) {
-  if (argc != 5) {
-    di<<"Use: "<<argv[0]<<" CrankArmFile CylinderHeadFile PropellerFile EngineBlockFile\n";
-    return 1;
-  }
-
-  Standard_Real thread = 4;
-  Standard_Real angleA=0;
-  Standard_Real angleB;
-  Standard_Real X;
-  gp_Ax1 Ax1(gp_Pnt(0,0,0),gp_Vec(0,0,1));
-
-  BRep_Builder B;
-  TopoDS_Shape CrankArm;
-  TopoDS_Shape CylinderHead;
-  TopoDS_Shape Propeller;
-  TopoDS_Shape EngineBlock;
-
-  //BRepTools::Read(CrankArm,"/dp_26/Indus/ege/assemblage/CrankArm.rle",B);
-  //BRepTools::Read(CylinderHead,"/dp_26/Indus/ege/assemblage/CylinderHead.rle",B);
-  //BRepTools::Read(Propeller,"/dp_26/Indus/ege/assemblage/Propeller.rle",B);
-  //BRepTools::Read(EngineBlock,"/dp_26/Indus/ege/assemblage/EngineBlock.rle",B);
-  BRepTools::Read(CrankArm,argv[1],B);
-  BRepTools::Read(CylinderHead,argv[2],B);
-  BRepTools::Read(Propeller,argv[3],B);
-  BRepTools::Read(EngineBlock,argv[4],B);
-
-  if (CrankArm.IsNull() || CylinderHead.IsNull() || Propeller.IsNull() || EngineBlock.IsNull()) {di<<" Syntaxe error:loading failure.\n";}
-
-
-  OSD_Timer myTimer;
-  myTimer.Start();
-
-  Handle(AIS_Shape) myAisCylinderHead = new AIS_Shape (CylinderHead);
-  Handle(AIS_Shape) myAisEngineBlock  = new AIS_Shape (EngineBlock);
-  Handle(AIS_Shape) myAisCrankArm     = new AIS_Shape (CrankArm);
-  Handle(AIS_Shape) myAisPropeller    = new AIS_Shape (Propeller);
-
-  GetMapOfAIS().Bind(myAisCylinderHead,"a");
-  GetMapOfAIS().Bind(myAisEngineBlock,"b");
-  GetMapOfAIS().Bind(myAisCrankArm,"c");
-  GetMapOfAIS().Bind(myAisPropeller,"d");
-
-  myAisCylinderHead->SetMutable (Standard_True);
-  myAisEngineBlock ->SetMutable (Standard_True);
-  myAisCrankArm    ->SetMutable (Standard_True);
-  myAisPropeller   ->SetMutable (Standard_True);
-
-  TheAISContext()->SetColor (myAisCylinderHead, Quantity_NOC_INDIANRED);
-  TheAISContext()->SetColor (myAisEngineBlock,  Quantity_NOC_RED);
-  TheAISContext()->SetColor (myAisPropeller,    Quantity_NOC_GREEN);
-
-  TheAISContext()->Display (myAisCylinderHead, Standard_False);
-  TheAISContext()->Display (myAisEngineBlock,  Standard_False);
-  TheAISContext()->Display (myAisCrankArm,     Standard_False);
-  TheAISContext()->Display (myAisPropeller,    Standard_False);
-
-  TheAISContext()->Deactivate(myAisCylinderHead);
-  TheAISContext()->Deactivate(myAisEngineBlock );
-  TheAISContext()->Deactivate(myAisCrankArm    );
-  TheAISContext()->Deactivate(myAisPropeller   );
-
-  // Boucle de mouvement
-  for (Standard_Real myAngle = 0;angleA<2*M_PI*10.175 ;myAngle++) {
-
-    angleA = thread*myAngle*M_PI/180;
-    X = Sin(angleA)*3/8;
-    angleB = atan(X / Sqrt(-X * X + 1));
-    Standard_Real decal(25*0.6);
-
-
-    //Build a transformation on the display
-    gp_Trsf aPropellerTrsf;
-    aPropellerTrsf.SetRotation(Ax1,angleA);
-    TheAISContext()->SetLocation(myAisPropeller,aPropellerTrsf);
-
-    gp_Ax3 base(gp_Pnt(3*decal*(1-Cos(angleA)),-3*decal*Sin(angleA),0),gp_Vec(0,0,1),gp_Vec(1,0,0));
-    gp_Trsf aCrankArmTrsf;
-    aCrankArmTrsf.SetTransformation(   base.Rotated(gp_Ax1(gp_Pnt(3*decal,0,0),gp_Dir(0,0,1)),angleB));
-    TheAISContext()->SetLocation(myAisCrankArm,aCrankArmTrsf);
-
-    TheAISContext()->UpdateCurrentViewer();
-  }
-
-  TopoDS_Shape myNewCrankArm  =myAisCrankArm ->Shape().Located( myAisCrankArm ->Transformation() );
-  TopoDS_Shape myNewPropeller =myAisPropeller->Shape().Located( myAisPropeller->Transformation() );
-
-  myAisCrankArm ->ResetTransformation();
-  myAisPropeller->ResetTransformation();
-
-  myAisCrankArm  -> Set(myNewCrankArm );
-  myAisPropeller -> Set(myNewPropeller);
-
-  TheAISContext()->Activate(myAisCylinderHead,0);
-  TheAISContext()->Activate(myAisEngineBlock,0 );
-  TheAISContext()->Activate(myAisCrankArm ,0   );
-  TheAISContext()->Activate(myAisPropeller ,0  );
-
-  myTimer.Stop();
-  myTimer.Show();
-  myTimer.Start();
-
-  TheAISContext()->Redisplay(myAisCrankArm ,Standard_False);
-  TheAISContext()->Redisplay(myAisPropeller,Standard_False);
-
-  TheAISContext()->UpdateCurrentViewer();
-  a3DView()->Redraw();
-
-  myTimer.Stop();
-  myTimer.Show();
-
-  return 0;
-
-}
-
 //==============================================================================
 //function : VShading
 //purpose  : Sharpen or roughten the quality of the shading
@@ -5755,10 +5636,6 @@ void ViewerTest::Commands(Draw_Interpretor& theCommands)
       "\n\t\t: Tests the animation of an object along a predefined trajectory.",
       __FILE__,VPerf,group);
 
-  theCommands.Add("vanimation",
-                 "vanimation CrankArmFile CylinderHeadFile PropellerFile EngineBlockFile",
-                 __FILE__,VAnimation,group);
-
   theCommands.Add("vsetshading",
       "vsetshading  : vsetshading name Quality(default=0.0008) "
       "\n\t\t: Sets deflection coefficient that defines the quality of the shape representation in the shading mode.",
index 5aa4729c199bdc2f51e7ba38e63833b1c3331d5f..4608ddbc43d1d97259ddd5ee122f1dee7e97ea32 100644 (file)
 
 #if (defined(_WIN32) || defined(__WIN32__)) && defined(HAVE_VIDEOCAPTURE)
   #include <windows.h>
-  #include <Aspect_Window.hxx>
   #include <OpenGl_AVIWriter.hxx>
-  #include <V3d_View.hxx>
 #endif
 
 #include <ViewerTest.hxx>
+
+#include <Aspect_Window.hxx>
+#include <Draw.hxx>
 #include <Draw_Interpretor.hxx>
+#include <Message.hxx>
+#include <Message_Messenger.hxx>
+#include <Message_ProgressSentry.hxx>
+#include <NCollection_Sequence.hxx>
+#include <NCollection_DataMap.hxx>
+#include <Image_PixMap.hxx>
+#include <OSD_Timer.hxx>
+#include <Precision.hxx>
+#include <V3d_View.hxx>
+#include <TCollection_AsciiString.hxx>
 
 static Standard_Integer avi_record(Draw_Interpretor& /*di*/,
                                    Standard_Integer argc, const char** argv)
@@ -84,14 +95,888 @@ static Standard_Integer avi_record(Draw_Interpretor& /*di*/,
   return aResult;
 }
 
+extern "C"
+{
+#ifdef _MSC_VER
+
+  #pragma comment(lib, "avutil.lib")
+  #pragma comment(lib, "avcodec.lib")
+  #pragma comment(lib, "avformat.lib")
+  #pragma comment(lib, "swscale.lib")
+
+  // suppress some common warnings in FFmpeg headers
+  #pragma warning(disable : 4244)
+#endif
+
+  #include <libavcodec/avcodec.h>
+  #include <libavformat/avformat.h>
+  #include <libswscale/swscale.h>
+
+#ifdef _MSC_VER
+  #pragma warning(default : 4244)
+#endif
+};
+
+//! Video recording tool.
+class ViewerTest_VideoRecorder : public Standard_Transient
+{
+
+public:
+
+  //! Empty constructor.
+  ViewerTest_VideoRecorder()
+  : myAVContext   (NULL),
+    myVideoStream (NULL),
+    myVideoCodec  (NULL),
+    myFrame       (NULL),
+    myScaleCtx    (NULL),
+    myPixFmtOut   (AV_PIX_FMT_YUV420P),
+    myPixFmtSrc   (AV_PIX_FMT_RGBA), // AV_PIX_FMT_BGR0
+    myFrameCount  (0)
+  {
+    myFrameRate.num = 1;
+    myFrameRate.den = 30;
+
+    // initialize libavcodec, and register all codecs and formats, should be done once
+    av_register_all();
+  }
+
+  //! Destructor.
+  ~ViewerTest_VideoRecorder()
+  {
+    Close();
+  }
+
+  //! Setup playback FPS. Should be set before Open().
+  void SetFramerate (const Standard_Integer theNumerator,
+                     const Standard_Integer theDenominator)
+  {
+    myFrameRate.num = theNumerator;
+    myFrameRate.den = theDenominator;
+  }
+
+  //! Setup playback FPS. Should be set before Open().
+  //! For fixed-fps content, timebase should be 1/framerate and timestamp increments should be identical to 1.
+  void SetFramerate (const Standard_Integer theValue)
+  {
+    myFrameRate.num = int(theValue);
+    myFrameRate.den = 1;
+  }
+
+  //! Close the stream - stop recorder.
+  void Close()
+  {
+    if (myScaleCtx != NULL)
+    {
+      sws_freeContext (myScaleCtx);
+    }
+
+    if (myAVContext == NULL)
+    {
+      return;
+    }
+
+    // Write the trailer, if any. The trailer must be written before you close the CodecContexts open when you wrote the header;
+    // otherwise av_write_trailer() may try to use memory that was freed on av_codec_close().
+    av_write_trailer (myAVContext);
+
+    // close each codec
+    if (myVideoStream != NULL)
+    {
+      avcodec_close (myVideoStream->codec);
+      myVideoStream = NULL;
+    }
+    if (myFrame != NULL)
+    {
+      av_free (myPicDst.data[0]);
+      av_frame_free (&myFrame);
+      myFrame = NULL;
+    }
+
+    if (!(myAVContext->oformat->flags & AVFMT_NOFILE))
+    {
+      // close the output file
+      avio_close (myAVContext->pb);
+    }
+
+    // free the stream
+    avformat_free_context (myAVContext);
+    myAVContext = NULL;
+  }
+
+  //! Open output stream - start recorder.
+  Standard_Boolean Open (const char*            theFileName,
+                         const Standard_Integer theWidth,
+                         const Standard_Integer theHeight)
+  {
+    Close();
+    myFrameCount = 0;
+    if (theWidth <= 0
+     || theHeight <= 0)
+    {
+      return Standard_False;
+    }
+
+    // allocate the output media context
+    avformat_alloc_output_context2 (&myAVContext, NULL, NULL, theFileName);
+    if (myAVContext == NULL)
+    {
+      ::Message::DefaultMessenger()->Send ("ViewerTest_VideoRecorder, could not deduce output format from file extension - using MPEG.", Message_Warning);
+      avformat_alloc_output_context2 (&myAVContext, NULL, "mpeg", theFileName);
+    }
+    if (myAVContext == NULL)
+    {
+      return Standard_False;
+    }
+
+    // add the audio stream using the default format codecs and initialize the codecs
+    if (!addVideoStream (myAVContext->oformat->video_codec, theWidth, theHeight))
+    {
+      Close();
+      return Standard_False;
+    }
+
+    // open video codec and allocate the necessary encode buffers
+    if (!openVideoCodec())
+    {
+      Close();
+      return Standard_False;
+    }
+
+av_dump_format (myAVContext, 0, theFileName, 1); /// debug
+
+    // open the output file, if needed
+    if ((myAVContext->oformat->flags & AVFMT_NOFILE) == 0)
+    {
+      const int aResAv = avio_open (&myAVContext->pb, theFileName, AVIO_FLAG_WRITE);
+      if (aResAv < 0)
+      {
+        const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: could not open '") + theFileName + "', " + formatAvError (aResAv);
+        ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+        Close();
+        return Standard_False;
+      }
+    }
+
+    // write the stream header, if any
+    const int aResAv = avformat_write_header (myAVContext, NULL);
+    if (aResAv < 0)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not open output file '") + theFileName + "', " + formatAvError (aResAv);
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      Close();
+      return Standard_False;
+    }
+    return Standard_True;
+  }
+
+  //! Access RGBA frame, should NOT be re-initialized outside.
+  Image_PixMap& ChangeFrame()
+  {
+    return myImgSrcRgba;
+  }
+
+  //! Push new frame, should be called after Open().
+  Standard_Boolean PushFrame()
+  {
+    return writeVideoFrame (Standard_False);
+  }
+
+protected:
+
+  //! Wrapper for av_strerror().
+  TCollection_AsciiString formatAvError (const int theError) const
+  {
+    char anErrBuf[AV_ERROR_MAX_STRING_SIZE] = {};
+    av_strerror (theError, anErrBuf, AV_ERROR_MAX_STRING_SIZE);
+    return anErrBuf;
+  }
+
+  //! Append video stream.
+  Standard_Boolean addVideoStream (const AVCodecID        theCodecId,
+                                   const Standard_Integer theWidth,
+                                   const Standard_Integer theHeight)
+  {
+    // find the encoder
+    myVideoCodec = avcodec_find_encoder (theCodecId);
+    if (myVideoCodec == NULL)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not find encoder for ") + avcodec_get_name (theCodecId);
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+
+    myVideoStream = avformat_new_stream (myAVContext, myVideoCodec);
+    if (myVideoStream == NULL)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not allocate stream!");
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+    myVideoStream->id = myAVContext->nb_streams - 1;
+
+    AVCodecContext* aCodecCtx = myVideoStream->codec;
+    aCodecCtx->codec_id = theCodecId;
+    // resolution must be a multiple of two
+    aCodecCtx->width    = theWidth;
+    aCodecCtx->height   = theHeight;
+    // Timebase is the fundamental unit of time (in seconds) in terms of which frame timestamps are represented.
+    // For fixed-fps content, timebase should be 1/framerate and timestamp increments should be identical to 1.
+    aCodecCtx->time_base.den = myFrameRate.num;
+    aCodecCtx->time_base.num = myFrameRate.den;
+    aCodecCtx->gop_size      = 12; // emit one intra frame every twelve frames at most
+    aCodecCtx->pix_fmt       = myPixFmtOut;
+
+    // some formats want stream headers to be separate
+    if (myAVContext->oformat->flags & AVFMT_GLOBALHEADER)
+    {
+      aCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
+    }
+    return Standard_True;
+  }
+
+  //! Open video codec.
+  Standard_Boolean openVideoCodec()
+  {
+    // open the codec
+    AVCodecContext* aCodecCtx = myVideoStream->codec;
+
+    AVDictionary* anOptions = NULL;
+    //av_dict_set (&anOptions, "threads", "auto", 0);
+    switch (aCodecCtx->codec_id)
+    {
+      case AV_CODEC_ID_MPEG2VIDEO:
+      {
+        // just for testing, we also add B frames
+        aCodecCtx->max_b_frames = 2;
+        aCodecCtx->bit_rate = 6000000;
+        break;
+      }
+      case AV_CODEC_ID_H264:
+      {
+        // use CRF (Constant Rate Factor) as best single-pass compressing method
+        av_dict_set (&anOptions, "crf",     "20",        0); // quality 18-28, 23 is default (normal), 18 is almost lossless
+        av_dict_set (&anOptions, "preset",  "slow",      0); // good compression (see also "veryslow", "ultrafast")
+
+        // live-capturing
+        //av_dict_set (&anOptions, "qp",     "0",         0); // instead of crf
+        //av_dict_set (&anOptions, "preset", "ultrafast", 0);
+
+        // compatibility with devices
+        //av_dict_set (&anOptions, "profile", "baseline",  0);
+        //av_dict_set (&anOptions, "level",   "3.0",       0);
+        break;
+      }
+      /*case AV_CODEC_ID_VP8:
+      case AV_CODEC_ID_VP9:
+      {
+        av_dict_set (&anOptions, "crf",     "20",        0); // quality 4\9663, 10 is normal
+        break;
+      }*/
+    }
+
+    int aResAv = avcodec_open2 (aCodecCtx, myVideoCodec, &anOptions);
+    if (anOptions != NULL)
+    {
+      av_dict_free (&anOptions);
+    }
+    if (aResAv < 0)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not open video codec, ") + formatAvError (aResAv);
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+
+    // allocate and init a re-usable frame
+    myFrame = av_frame_alloc();
+    if (myFrame == NULL)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not allocate video frame!");
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+
+    // allocate the encoded raw picture
+    aResAv = avpicture_alloc (&myPicDst, aCodecCtx->pix_fmt, aCodecCtx->width, aCodecCtx->height);
+    if (aResAv < 0)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not allocate picture ")
+                                         + aCodecCtx->width+ "x" + aCodecCtx->height + ", " + formatAvError (aResAv);
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+    // copy data and linesize picture pointers to frame
+    myFrame->format = aCodecCtx->pix_fmt;
+    myFrame->width  = aCodecCtx->width;
+    myFrame->height = aCodecCtx->height;
+    *((AVPicture* )myFrame) = myPicDst;
+
+    const Standard_Size aStride = aCodecCtx->width + 16 - (aCodecCtx->width % 16);
+    if (!myImgSrcRgba.InitZero (Image_PixMap::ImgRGBA, aCodecCtx->width, aCodecCtx->height, aStride))
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not allocate RGBA32 picture ")
+                                         + aCodecCtx->width+ "x" + aCodecCtx->height;
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+
+    myScaleCtx = sws_getContext (aCodecCtx->width, aCodecCtx->height, myPixFmtSrc,
+                                 aCodecCtx->width, aCodecCtx->height, aCodecCtx->pix_fmt,
+                                 SWS_BICUBIC, NULL, NULL, NULL);
+    if (myScaleCtx == NULL)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not initialize the conversion context!");
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+
+    return Standard_True;
+  }
+
+  //! Write new video frame.
+  Standard_Boolean writeVideoFrame (const Standard_Boolean theToFlush)
+  {
+    int aResAv = 0;
+    AVCodecContext* aCodecCtx = myVideoStream->codec;
+    if (!theToFlush)
+    {
+      uint8_t* aSrcData[4]     = { (uint8_t*)myImgSrcRgba.ChangeData(),   NULL, NULL, NULL };
+      int      aSrcLinesize[4] = { (int     )myImgSrcRgba.SizeRowBytes(), 0,    0,    0    };
+      sws_scale (myScaleCtx,
+                 aSrcData, aSrcLinesize,
+                 0, aCodecCtx->height,
+                 myPicDst.data, myPicDst.linesize);
+    }
+
+    AVPacket aPacket = { 0 };
+    av_init_packet (&aPacket);
+    if ((myAVContext->oformat->flags & AVFMT_RAWPICTURE) != 0
+     && !theToFlush)
+    {
+      // raw video case - directly store the picture in the packet
+      aPacket.flags        |= AV_PKT_FLAG_KEY;
+      aPacket.stream_index  = myVideoStream->index;
+      aPacket.data          = myPicDst.data[0];
+      aPacket.size          = sizeof(AVPicture);
+
+      aResAv = av_interleaved_write_frame (myAVContext, &aPacket);
+    }
+    else
+    {
+      // encode the image
+      myFrame->pts = myFrameCount;
+      int isGotPacket = 0;
+      aResAv = avcodec_encode_video2 (aCodecCtx, &aPacket, theToFlush ? NULL : myFrame, &isGotPacket);
+      if (aResAv < 0)
+      {
+        const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not encode video frame, ") + formatAvError (aResAv);
+        ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+        return Standard_False;
+      }
+
+      // if size is zero, it means the image was buffered
+      if (isGotPacket)
+      {
+        const AVRational& aTimeBase = aCodecCtx->time_base;
+
+        // rescale output packet timestamp values from codec to stream timebase
+        aPacket.pts          = av_rescale_q_rnd (aPacket.pts,      aTimeBase, myVideoStream->time_base, AVRounding(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
+        aPacket.dts          = av_rescale_q_rnd (aPacket.dts,      aTimeBase, myVideoStream->time_base, AVRounding(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
+        aPacket.duration     = av_rescale_q     (aPacket.duration, aTimeBase, myVideoStream->time_base);
+        aPacket.stream_index = myVideoStream->index;
+
+        // write the compressed frame to the media file
+        aResAv = av_interleaved_write_frame (myAVContext, &aPacket);
+      }
+      else
+      {
+        aResAv = 0;
+      }
+    }
+
+    if (aResAv < 0)
+    {
+      const TCollection_AsciiString aMsg = TCollection_AsciiString ("Error: can not write video frame, ") + formatAvError (aResAv);
+      ::Message::DefaultMessenger()->Send (aMsg, Message_Fail);
+      return Standard_False;
+    }
+    ++myFrameCount;
+    return Standard_True;
+  }
+
+protected:
+
+  AVFormatContext* myAVContext;
+  AVStream*        myVideoStream;
+  AVCodec*         myVideoCodec;
+  AVFrame*         myFrame;
+  AVPicture        myPicDst;
+  SwsContext*      myScaleCtx;
+  AVRational       myFrameRate;
+  AVPixelFormat    myPixFmtOut;
+
+  Image_PixMap     myImgSrcRgba;
+  AVPixelFormat    myPixFmtSrc;
+
+  int64_t          myFrameCount;
+
+public:
+
+  DEFINE_STANDARD_RTTIEXT(ViewerTest_VideoRecorder, Standard_Transient) // Type definition
+
+};
+
+//DEFINE_STANDARD_HANDLE(ViewerTest_VideoRecorder, Standard_Transient)
+//IMPLEMENT_STANDARD_HANDLE (ViewerTest_VideoRecorder, Standard_Transient)
+IMPLEMENT_STANDARD_RTTIEXT(ViewerTest_VideoRecorder, Standard_Transient)
+//DEFINE_STANDARD_HANDLE(ViewerTest_Animation, Standard_Transient)
+
+//! Structure defining animation sequence
+class ViewerTest_Animation : public Standard_Transient
+{
+
+public:
+
+  //! Connected animation slice
+  struct Slice
+  {
+
+    Standard_Real                StartTime; //!< presentation timestamp within parent animation slice
+    Standard_Real                Duration;  //!< animation duration
+    Handle(ViewerTest_Animation) Animation; //!< connected animation to be executed
+
+    Slice() : StartTime (0.0), Duration (0.0) {}
+
+  };
+
+public:
+
+  TCollection_AsciiString     OnStart;  //!< procedure to be evaluated at the beggining of this animation slice
+  TCollection_AsciiString     OnEnd;    //!< procedure to be evaluated at the end       of this animation slice
+  TCollection_AsciiString     OnRedraw; //!< procedure to be evaluated at every frame during this animation slice
+  NCollection_Sequence<Slice> Slices;   //!< sequence of child animation slices
+
+public:
+
+  //! Empty constructor
+  ViewerTest_Animation() {}
+
+  //! Reset the animation definition
+  void Reset()
+  {
+    OnStart.Clear();
+    OnEnd.Clear();
+    OnRedraw.Clear();
+    Slices.Clear();
+  }
+
+  //! Estimated duration
+  Standard_Real Duration() const
+  {
+    Standard_Real aDuration = -1.0;
+    for (NCollection_Sequence<Slice>::Iterator aSliceIter (Slices);
+         aSliceIter.More(); aSliceIter.Next())
+    {
+      Standard_Real aSliceDuration = aSliceIter.Value().Duration >= 0.0
+                                   ? aSliceIter.Value().Duration
+                                   : aSliceIter.Value().Animation->Duration();
+      aDuration = Max (aDuration, aSliceIter.Value().StartTime + aSliceDuration);
+    }
+    return aDuration;
+  }
+
+public:
+
+  DEFINE_STANDARD_RTTIEXT(ViewerTest_Animation,Standard_Transient) // Type definition
+
+};
+
+//IMPLEMENT_STANDARD_HANDLE (ViewerTest_Animation, Standard_Transient)
+IMPLEMENT_STANDARD_RTTIEXT(ViewerTest_Animation, Standard_Transient)
+
+static NCollection_DataMap<TCollection_AsciiString, Handle(ViewerTest_Animation)> ViewerTest_TheAnimationsMap;
+
+//! Evaluate animation sequence
+static void VEvalAnimation (Draw_Interpretor&           theDI,
+                            const ViewerTest_Animation& theAnim,
+                            const Standard_Real         theStartPts,
+                            const Standard_Real         theEndPts,
+                            const Standard_Real         thePrevPts,
+                            const Standard_Real         thePts)
+{
+  const Standard_Real aLocPts = thePts - theStartPts;
+  if ( thePts     >= theStartPts
+   &&  thePrevPts <  theStartPts
+   && !theAnim.OnStart.IsEmpty())
+  {
+    const TCollection_AsciiString aCmd = theAnim.OnStart + " " + thePts + " " + aLocPts + " onStart";
+    theDI.Eval (aCmd.ToCString());
+  }
+
+  for (NCollection_Sequence<ViewerTest_Animation::Slice>::Iterator aSliceIter (theAnim.Slices);
+       aSliceIter.More(); aSliceIter.Next())
+  {
+    VEvalAnimation (theDI,
+                    *aSliceIter.Value().Animation,
+                    aSliceIter.Value().StartTime,
+                    aSliceIter.Value().StartTime + aSliceIter.Value().Duration,
+                    thePrevPts,
+                    thePts);
+  }
+
+  if (!theAnim.OnRedraw.IsEmpty()
+   && thePts >= theStartPts
+   && thePts <  theEndPts)
+  {
+    const TCollection_AsciiString aCmd = theAnim.OnRedraw + " " + thePts + " " + aLocPts + " onRedraw";
+    theDI.Eval (aCmd.ToCString());
+  }
+
+  if ( thePts     >= theEndPts
+   &&  thePrevPts <  theEndPts
+   && !theAnim.OnEnd.IsEmpty())
+  {
+    const TCollection_AsciiString aCmd = theAnim.OnEnd + " " + thePts + " " + aLocPts + " onEnd";
+    theDI.Eval (aCmd.ToCString());
+  }
+}
+
+//==============================================================================
+//function : VAnimation
+//purpose  :
+//==============================================================================
+static Standard_Integer VAnimation (Draw_Interpretor& theDI,
+                                    Standard_Integer  theArgNb,
+                                    const char**      theArgVec)
+{
+  Handle(AIS_InteractiveContext) aCtx = ViewerTest::GetAISContext();
+  if (theArgNb < 2)
+  {
+    for (NCollection_DataMap<TCollection_AsciiString, Handle(ViewerTest_Animation)>::Iterator anAnimIter (ViewerTest_TheAnimationsMap);
+         anAnimIter.More(); anAnimIter.Next())
+    {
+      theDI << anAnimIter.Key() << " " << anAnimIter.Value()->Duration() << " sec\n";
+    }
+    return 0;
+  }
+
+  Standard_Boolean toPlay        = Standard_False;
+  Standard_Boolean isInteractive = Standard_False;
+  Standard_Real    aPlayFrom     =  0.0;
+  Standard_Real    aPlayDuration = -1.0;
+  Standard_Integer aPlayFpsNum   = 0;
+  Standard_Integer aPlayFpsDen   = 1;
+  Standard_CString aRecordFile   = NULL;
+  Standard_Integer aRecWidth     = 0;
+  Standard_Integer aRecHeight    = 0;
+  TCollection_AsciiString aName;
+  Handle(ViewerTest_Animation) anAnim;
+  Standard_Integer anArgIter = 1;
+  for (; anArgIter < theArgNb; ++anArgIter)
+  {
+    Standard_CString        anArg = theArgVec[anArgIter];
+    TCollection_AsciiString anArgCase (anArg);
+    anArgCase.LowerCase();
+    if (anArgCase == "-onstart")
+    {
+      if (++anArgIter >= theArgNb
+       || anAnim.IsNull())
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      anAnim->OnStart = theArgVec[anArgIter];
+    }
+    else if (anArgCase == "-onend")
+    {
+      if (++anArgIter >= theArgNb
+       || anAnim.IsNull())
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      anAnim->OnEnd = theArgVec[anArgIter];
+    }
+    else if (anArgCase == "-onredraw"
+          || anArgCase == "-ondraw")
+    {
+      if (++anArgIter >= theArgNb
+       || anAnim.IsNull())
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      anAnim->OnRedraw = theArgVec[anArgIter];
+    }
+    else if (anArgCase == "-reset")
+    {
+      if (anAnim.IsNull())
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      anAnim->Reset();
+    }
+    else if (anArgCase == "-delete")
+    {
+      if (++anArgIter < theArgNb
+       || anAnim.IsNull())
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      ViewerTest_TheAnimationsMap.UnBind (aName);
+    }
+    else if (anArgCase == "-addslice"
+          || anArgCase == "-slice")
+    {
+      if (anArgIter + 3 >= theArgNb
+       || anAnim.IsNull())
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+
+      ViewerTest_Animation::Slice aSlice;
+      aSlice.StartTime = Draw::Atof (theArgVec[++anArgIter]);
+      aSlice.Duration  = Draw::Atof (theArgVec[++anArgIter]);
+      if (aSlice.StartTime < 0.0)
+      {
+        std::cout << "Error: PTS should not be negative (specified " << aSlice.StartTime << ")\n";
+        return 1;
+      }
+      TCollection_AsciiString anOtherName (theArgVec[++anArgIter]);
+      if (!ViewerTest_TheAnimationsMap.Find (anOtherName, aSlice.Animation))
+      {
+        std::cout << "Error: animation with name '" << anOtherName << "' is not registered!\n";
+        return 1;
+      }
+      Standard_Integer anIndex = 1;
+      for (NCollection_Sequence<ViewerTest_Animation::Slice>::Iterator aSliceIter (anAnim->Slices);
+           aSliceIter.More(); aSliceIter.Next(), ++anIndex)
+      {
+        if (aSlice.StartTime < aSliceIter.Value().StartTime)
+        {
+          anAnim->Slices.InsertBefore (anIndex, aSlice);
+          anIndex = -1;
+          break;
+        }
+      }
+      if (anIndex != -1)
+      {
+        anAnim->Slices.Append (aSlice);
+      }
+    }
+    else if (anArgCase == "-play")
+    {
+      toPlay = Standard_True;
+    }
+    else if (anArgCase == "-playfrom")
+    {
+      if (++anArgIter >= theArgNb)
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      aPlayFrom = Draw::Atof (theArgVec[anArgIter]);
+    }
+    else if (anArgCase == "-playduration"
+          || anArgCase == "-playdur")
+    {
+      if (++anArgIter >= theArgNb)
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      aPlayDuration = Draw::Atof (theArgVec[anArgIter]);
+    }
+    else if (anArgCase == "-playfps"
+          || anArgCase == "-fps")
+    {
+      if (++anArgIter >= theArgNb)
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+
+      TCollection_AsciiString aFpsStr (theArgVec[anArgIter]);
+      Standard_Integer aSplitIndex = aFpsStr.FirstLocationInSet ("/", 1, aFpsStr.Length());
+      if (aSplitIndex == 0)
+      {
+        aPlayFpsNum = aFpsStr.IntegerValue();
+        aPlayFpsDen = 1;
+      }
+      else
+      {
+        const TCollection_AsciiString aDenStr = aFpsStr.Split (aSplitIndex);
+        aFpsStr.Split (aFpsStr.Length() - 1);
+        const TCollection_AsciiString aNumStr = aFpsStr;
+        aPlayFpsNum = aNumStr.IntegerValue();
+        aPlayFpsDen = aDenStr.IntegerValue();
+        if (aPlayFpsDen < 1)
+        {
+          std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+          return 1;
+        }
+      }
+    }
+    else if (anArgCase == "-inter"
+          || anArgCase == "-interactive")
+    {
+      isInteractive = Standard_True;
+    }
+    else if (anArgCase == "-rec"
+          || anArgCase == "-record")
+    {
+      if (++anArgIter >= theArgNb)
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      aRecordFile = theArgVec[anArgIter];
+    }
+    else if (anArgCase == "-recwidth"
+          || anArgCase == "-recordwidth")
+    {
+      if (++anArgIter >= theArgNb)
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      aRecWidth = Draw::Atoi (theArgVec[anArgIter]);
+    }
+    else if (anArgCase == "-recheight"
+          || anArgCase == "-recordheight")
+    {
+      if (++anArgIter >= theArgNb)
+      {
+        std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+        return 1;
+      }
+      aRecHeight = Draw::Atoi (theArgVec[anArgIter]);
+    }
+    else if (aName.IsEmpty())
+    {
+      aName = anArg;
+      if (!ViewerTest_TheAnimationsMap.Find (aName, anAnim))
+      {
+        anAnim = new ViewerTest_Animation();
+        ViewerTest_TheAnimationsMap.Bind (aName, anAnim);
+      }
+    }
+    else
+    {
+      std::cout << "Error: wrong syntax at '" << anArg << "'\n";
+      return 1;
+    }
+  }
+
+  if (!toPlay)
+  {
+    return 0;
+  }
+
+  // play animation
+  if (aPlayDuration < 0.0)
+  {
+    aPlayDuration = anAnim->Duration();
+  }
+  Standard_Real aPtsTo = aPlayFrom + aPlayDuration;
+  OSD_Timer aTimer;
+  aTimer.Start();
+  Standard_Real aPrevPts = aPlayFrom - Precision::Confusion();
+  if (aPlayFpsNum > 0)
+  {
+    const Handle(V3d_View)&          aView = ViewerTest::CurrentView();
+    Handle(ViewerTest_VideoRecorder) aRecorder;
+    if (aRecordFile != NULL)
+    {
+      if (aView.IsNull())
+      {
+        std::cout << "Error: no active view!\n";
+        return 1;
+      }
+
+      Standard_Integer aWinWidth = 0, aWinHeight = 0;
+      aView->Window()->Size (aWinWidth, aWinHeight);
+      if (aRecWidth <= 0)
+      {
+        aRecWidth = aWinWidth;
+      }
+      if (aRecHeight <= 0)
+      {
+        aRecHeight = aWinHeight;
+      }
+
+      aRecorder = new ViewerTest_VideoRecorder();
+      aRecorder->SetFramerate (aPlayFpsNum, aPlayFpsDen);
+      if (!aRecorder->Open (aRecordFile, aRecWidth, aRecHeight))
+      {
+        return 0;
+      }
+    }
+
+    // fixed FPS, bad formula - to be corrected
+    const Standard_Real aFrameDur = Standard_Real(aPlayFpsDen) / Standard_Real(aPlayFpsNum);
+    for (Standard_Real aPts = aPlayFrom; aPts <= aPtsTo; aPts += aFrameDur)
+    {
+      VEvalAnimation (theDI, *anAnim,
+                      aPlayFrom, aPtsTo,
+                      aPrevPts,  aPts);
+      aPrevPts = aPts;
+      if (!aRecorder.IsNull())
+      {
+        if (!aView->ToPixMap (aRecorder->ChangeFrame(),
+                              aRecorder->ChangeFrame().SizeX(), aRecorder->ChangeFrame().SizeY(),
+                              Graphic3d_BT_RGBA, Standard_True, V3d_SDO_MONO))
+        {
+          std::cout << "Error: view dump is failed!\n";
+          return 0;
+        }
+        if (!aRecorder->PushFrame())
+        {
+          return 0;
+        }
+      }
+
+      if (isInteractive)
+      {
+        // handle user events
+        theDI.Eval ("after 1 set waiter 1");
+        theDI.Eval ("vwait waiter");
+      }
+    }
+  }
+  else
+  {
+    // live playback
+    for (Standard_Real aPts = aPlayFrom; aPts <= aPtsTo; aPts = aTimer.ElapsedTime())
+    {
+      VEvalAnimation (theDI, *anAnim,
+                      aPlayFrom, aPtsTo,
+                      aPrevPts,  aPts);
+      aPrevPts = aPts;
+      if (isInteractive)
+      {
+        // handle user events
+        theDI.Eval ("after 1 set waiter 1");
+        theDI.Eval ("vwait waiter");
+      }
+    }
+  }
+  aTimer.Show();
+  //theDI << "Duration: " << aTimer.ElapsedTime() << " sec.\n";
+  return 0;
+}
+
 //=======================================================================
 //function : AviCommands
-//purpose  : 
+//purpose  :
 //=======================================================================
 
 void ViewerTest::AviCommands(Draw_Interpretor& theCommands)
 {
-  const char* group = "ViewerTest AVI commands";
+  const char* aGroup = "ViewerTest AVI commands";
 
   theCommands.Add("vrecord", "vrecord [option]\n"
                   "where [option] can be:\n"
@@ -102,5 +987,51 @@ void ViewerTest::AviCommands(Draw_Interpretor& theCommands)
                   "\tstatus             : log error message,\n"
                   "\tsave               : close the AVI file\n",
                  __FILE__,
-                 &avi_record, group); //Draft_Modification
+                 &avi_record, aGroup); //Draft_Modification
+
+  theCommands.Add("vanimation", "Alias for vanimation",
+    __FILE__, VAnimation, aGroup);
+
+  theCommands.Add("vanim",
+            "List existing animations:"
+    "\n\t\t:  vanim"
+    "\n\t\t: Animation playback:"
+    "\n\t\t:  vanim -play AnimName [-interactive] [-fps FPS=0/1]"
+    "\n\t\t:       [-playFrom Sec=0] [-playDuration Sec=-1.0]"
+    "\n\t\t:       [-record VideoFileName]"
+    "\n\t\t:       [-recordWidth Width] [-recordHeight Height]"
+    "\n\t\t: where"
+    "\n\t\t:   -interactive  handle user events on idle"
+    "\n\t\t:   -playFrom     animation start PTS"
+    "\n\t\t:   -playDuration animation duration"
+    "\n\t\t:   -playFps      constant framerate for recording (e.g. 30/1),"
+    "\n\t\t:                 disables live time"
+    "\n\t\t:   -record       record to the file"
+    "\n\t\t:   -recordWidth  width of recorded video"
+    "\n\t\t:                (window width if not set)"
+    "\n\t\t:   -recordHeight height of recorded video"
+    "\n\t\t:                (window height if not set)"
+    "\n\t\t: Animation definition:"
+    "\n\t\t:  vanim AnimName"
+    "\n\t\t:        -delete"
+    "\n\t\t:        -onStart  StartProcName"
+    "\n\t\t:        -onEnd    EndProcName"
+    "\n\t\t:        -onRedraw AnimProcName"
+    "\n\t\t:        -addSlice FromPts Duration OtherAnimName"
+    "\n\t\t:        -reset"
+    "\n\t\t: where"
+    "\n\t\t:   -delete   remove animation with specified name"
+    "\n\t\t:   -onStart  procedure to be called once"
+    "\n\t\t:             at the beginning"
+    "\n\t\t:   -onEnd    procedure to be called once at the end"
+    "\n\t\t:   -onRedraw procedure to be called at each frame"
+    "\n\t\t:   -addSlice call another animation within specified duration"
+    "\n\t\t:   -reset    clear list of linked slices"
+    "\n\t\t: Callback procedure should has the following prototype:"
+    "\n\t\t:  callback thePts theLocalPts theEvent"
+    "\n\t\t: where"
+    "\n\t\t:   -thePts       current presentation timestamp"
+    "\n\t\t:   -theLocalPts  presentation timestamp within slice"
+    "\n\t\t:   -theEvent     event from the list onStart,onEnd,onRedraw",
+    __FILE__, VAnimation, aGroup);
 }