Example #1
0
bool MovieExporter::execute(PlugInArgList* pInArgList, PlugInArgList* pOutArgList)
{
   FileDescriptor* pFileDescriptor(NULL);
   string filename;
   View* pView(NULL);
   AnimationController* pController(NULL);
   FrameType eType;
   AVOutputFormat* pOutFormat(NULL);
   int resolutionX(-1);
   int resolutionY(-1);
   rational<int> framerate(0);
   unsigned int bitrate(0);
   double startExport(0.0);
   double stopExport(0.0);
   bool fullResolution(false);

   mpProgress = NULL;
   mpStep = StepResource("Export movie", "app", "2233BFC9-9C51-4e31-A8C5-2512925CBE6D");

   // get input arguments and log some useful info about them
   { // scope the MessageResource
      MessageResource pMsg("Input arguments", "app", "4551F478-E182-4b56-B88F-6682F0E3A2CF");

      mpProgress = pInArgList->getPlugInArgValue<Progress>(Executable::ProgressArg());
      pMsg->addBooleanProperty("Progress Present", (mpProgress != NULL));

      pFileDescriptor = pInArgList->getPlugInArgValue<FileDescriptor>(Exporter::ExportDescriptorArg());
      if (pFileDescriptor == NULL)
      {
         log_error("No file specified");
         return false;
      }
      pMsg->addProperty("Destination", pFileDescriptor->getFilename());
      filename = pFileDescriptor->getFilename().getFullPathAndName();

      pView = pInArgList->getPlugInArgValue<View>(Exporter::ExportItemArg());
      if (pView == NULL)
      {
         log_error("No view specified");
         return false;
      }

      pController = pView->getAnimationController();
      if (pController == NULL)
      {
         log_error("No animation controller specified");
         return false;
      }
      pMsg->addProperty("Animation Controller Name", pController->getName());

      pOutFormat = getOutputFormat();
      if (pOutFormat == NULL)
      {
         log_error("Can't determine output format or format not supported.");
         return false;
      }
      pMsg->addProperty("Format", pOutFormat->long_name);

      bool resolutionFromInputArgs(false);
      if (!pInArgList->getPlugInArgValue("Resolution X", resolutionX) ||
         !pInArgList->getPlugInArgValue("Resolution Y", resolutionY))
      {
         if (mpOptionWidget.get() != NULL)
         {
            ViewResolutionWidget* pResolutionWidget = mpOptionWidget->getResolutionWidget();
            VERIFY(pResolutionWidget != NULL);

            switch(pResolutionWidget->getResolutionType())
            {
            case OptionsMovieExporter::VIEW_RESOLUTION:
               resolutionX = -1;
               resolutionY = -1;
               fullResolution = false;
               break;
            case OptionsMovieExporter::FULL_RESOLUTION:
               resolutionX = -1;
               resolutionY = -1;
               fullResolution = true;
               break;
            case OptionsMovieExporter::FIXED_RESOLUTION:
            {
               QSize resolution = pResolutionWidget->getResolution();
               resolutionX = resolution.width();
               resolutionY = resolution.height();
               fullResolution = false;
               break;
            }
            default:
               break; // nothing
            }
         }
         else
         {
            switch(StringUtilities::fromXmlString<OptionsMovieExporter::ResolutionType>(
               OptionsMovieExporter::getSettingResolutionType()))
            {
            case OptionsMovieExporter::VIEW_RESOLUTION:
               resolutionX = -1;
               resolutionY = -1;
               fullResolution = false;
               break;
            case OptionsMovieExporter::FULL_RESOLUTION:
               resolutionX = -1;
               resolutionY = -1;
               fullResolution = true;
               break;
            case OptionsMovieExporter::FIXED_RESOLUTION:
               resolutionX = OptionsMovieExporter::getSettingWidth();
               resolutionY = OptionsMovieExporter::getSettingHeight();
               fullResolution = false;
               break;
            default:
               break; // nothing
            }
         }
      }
      else
      {
         resolutionFromInputArgs = true;
      }

      if (resolutionX <= 0 || resolutionY <= 0)
      {
         QWidget* pWidget = pView->getWidget();
         if (pWidget != NULL)
         {
            resolutionX = pWidget->width();
            resolutionY = pWidget->height();
            resolutionFromInputArgs = false;
            if (fullResolution)
            {
               PerspectiveView* pPersp = dynamic_cast<PerspectiveView*>(pView);
               if (pPersp != NULL && pPersp->getZoomPercentage() > 100.)
               {
                  fullResolution = false;
                  if (mpProgress != NULL)
                  {
                     mpProgress->updateProgress("Full resolution export will be smaller than "
                        "view export, changing export resolution to current view size.", 0, WARNING);
                  }
               }
               else
               {
                  // translate to data coordinate
                  double x1 = 0.0;
                  double y1 = 0.0;
                  double x2 = 0.0;
                  double y2 = 0.0;
                  pView->translateScreenToWorld(0.0, 0.0, x1, y1);
                  pView->translateScreenToWorld(resolutionX, resolutionY, x2, y2);
                  resolutionX = (x2 > x1) ? (x2 - x1 + 0.5) : (x1 - x2 + 0.5);
                  resolutionY = (y2 > y1) ? (y2 - y1 + 0.5) : (y1 - y2 + 0.5);
               }
            }
         }
         else
         {
            log_error("Can't determine output resolution.");
            return false;
         }
      }

      int oldResX = resolutionX;
      int oldResY = resolutionY;
      if (!convertToValidResolution(resolutionX, resolutionY) ||
            (resolutionFromInputArgs && (resolutionX != oldResX || resolutionY != oldResY)))
      {
         stringstream msg;
         msg << "The export resolution does not meet the requirements of the the selected CODEC. "
                "The input arguments were X resolution of "
             << oldResX << " and Y resolution of " << oldResY << "."
             << "The adjusted resolution was (" << resolutionX << ", " << resolutionY << ")";
         log_error(msg.str());
         return false;
      }

      pMsg->addProperty("Resolution", QString("%1 x %2").arg(resolutionX).arg(resolutionY).toStdString());

      int framerateNum = 0;
      int framerateDen = 0;
      // first, get the framerate from the arg list
      // next, try the option widget
      // next, get from the animation controller
      if (pInArgList->getPlugInArgValue("Framerate Numerator", framerateNum) &&
         pInArgList->getPlugInArgValue("Framerate Denominator", framerateDen))
      {
         try
         {
            framerate.assign(framerateNum, framerateDen);
         }
         catch (const boost::bad_rational&)
         {
            // Do nothing; the code below handles this case
         }
      }
      if (framerate == 0)
      {
         if (mpOptionWidget.get() != NULL)
         {
            FramerateWidget* pFramerateWidget = mpOptionWidget->getFramerateWidget();
            VERIFY(pFramerateWidget != NULL);

            framerate = pFramerateWidget->getFramerate();
         }
         else
         {
            framerate = getFrameRate(pController);
         }
      }

      if (framerate == 0)
      {
         log_error("No framerate specified");
         return false;
      }

      // Validate the framerate
      boost::rational<int> validFrameRate = convertToValidFrameRate(framerate);
      if (validFrameRate != framerate)
      {
         QString msg = QString("The selected animation frame rate (%1/%2 fps) can not be represented in the "
                               "selected movie format. A frame rate of %3/%4 fps is being used instead.")
                              .arg(framerate.numerator())
                              .arg(framerate.denominator())
                              .arg(validFrameRate.numerator())
                              .arg(validFrameRate.denominator());
         mpProgress->updateProgress(msg.toStdString(), 0, WARNING);

         framerate = validFrameRate;
      }

      pMsg->addProperty("Framerate",
         QString("%1/%2").arg(framerate.numerator()).arg(framerate.denominator()).toStdString());

      if (!pInArgList->getPlugInArgValue("Bitrate", bitrate))
      {
         if (mpOptionWidget.get() != NULL)
         {
            BitrateWidget* pBitrateWidget = mpOptionWidget->getBitrateWidget();
            VERIFY(pBitrateWidget != NULL);

            bitrate = pBitrateWidget->getBitrate();
         }
         else
         {
            bitrate = OptionsMovieExporter::getSettingBitrate();
         }
      }
      pMsg->addProperty("Bitrate", QString::number(bitrate).toStdString());

      eType = pController->getFrameType();
      if (!pInArgList->getPlugInArgValue("Start Export", startExport))
      {
         if (mpOptionWidget.get() != NULL)
         {
            AnimationFrameSubsetWidget* pSubsetWidget = mpOptionWidget->getSubsetWidget();
            VERIFY(pSubsetWidget != NULL);

            startExport = pSubsetWidget->getStartFrame();
         }
         else
         {
            if (pController->getBumpersEnabled())
            {
               startExport = pController->getStartBumper();
            }
            else
            {
               startExport = pController->getStartFrame();
            }
         }
      }
      else
      {
         // adjust to 0-based since the input arg uses 1-based
         --startExport;
      }

      if (!pInArgList->getPlugInArgValue("Stop Export", stopExport))
      {
         if (mpOptionWidget.get() != NULL)
         {
            AnimationFrameSubsetWidget* pSubsetWidget = mpOptionWidget->getSubsetWidget();
            VERIFY(pSubsetWidget != NULL);

            stopExport = pSubsetWidget->getStopFrame();
         }
         else
         {
            if (pController->getBumpersEnabled())
            {
               stopExport = pController->getStopBumper();
            }
            else
            {
               stopExport = pController->getStopFrame();
            }
         }
      }
      else
      {
         // adjust to 0-based since the input arg users 1-based
         --stopExport;
      }
      string valueType("Time");
      if (eType == FRAME_ID)
      {
         valueType = "Frame";
      }

      pMsg->addProperty("Start "+valueType, QString::number(startExport).toStdString());
      pMsg->addProperty("Stop "+valueType, QString::number(stopExport).toStdString());
   }

   AvFormatContextResource pFormat(pOutFormat);
   VERIFY(pFormat != NULL);
   snprintf(pFormat->filename, sizeof(pFormat->filename), "%s", filename.c_str());
   AvStreamResource pVideoStream(pFormat, pOutFormat->video_codec);
   if (pVideoStream == NULL)
   {
      log_error("Unable to create video stream.");
      return false;
   }

   /**
    * allow changing of:
    *    dia_size/
    */
   AVCodecContext* pCodecContext = pVideoStream->codec;
   if (!setAvCodecOptions(pCodecContext))
   {
      log_error("Unable to initialize CODEC options");
      return false;
   }
   // set time_base, width, height, and bitrate here since
   // they can be passed in via the input args
   pCodecContext->width = resolutionX;
   pCodecContext->height = resolutionY;
   pCodecContext->bit_rate = bitrate * 1000;
   // the AVCodecContext wants a time_base which is
   // the inverse of fps.
   pCodecContext->time_base.num = framerate.denominator();
   pCodecContext->time_base.den = framerate.numerator();

   if (av_set_parameters(pFormat, NULL) < 0)
   {
      log_error("Invalid output format parameters.");
      return false;
   }
   if (!open_video(pFormat, pVideoStream))
   {
      log_error("Unable to initialize video stream.");
      return false;
   }
   if (url_fopen(&pFormat->pb, filename.c_str(), URL_WRONLY) < 0)
   {
      log_error("Could not open the output file. Ensure the destination directory is writable.");
      return false;
   }
   av_write_header(pFormat);

   // calculate time interval
   if ((framerate < getFrameRate(pController)) && (mpProgress != NULL))
   {
      mpProgress->updateProgress("The selected output frame rate may not encode all the frames in the movie.  "
                                 "Frames may be dropped.", 0, WARNING);
   }

   // do not use the boost::rational<int> overloaded operator '/' since it truncates type double to int
   double interval = pController->getIntervalMultiplier() * framerate.denominator() / framerate.numerator();

   // export the frames
   AVFrame* pTmpPicture = alloc_picture(PIX_FMT_RGBA32, pCodecContext->width, pCodecContext->height);
   if (pTmpPicture == NULL)
   {
      QString msg("Unable to allocate frame buffer of size %1 x %2");
      log_error(msg.arg(pCodecContext->width).arg(pCodecContext->height).toStdString());
      return false;
   }
   QImage image(pTmpPicture->data[0], pCodecContext->width, pCodecContext->height, QImage::Format_ARGB32);

   // For frame id based animation, each band of the data set fills one second of animation. 
   // If the requested frame rate for export is 15 fps, then each band is replicated 15 times. The execution
   // loop uses a pseudo time value, video_pts, to walk through the animation. The interval between 
   // exported frames is the inverse of the frame rate, e.g., for 15 fps the interval is 0.06667.
   // To fully export the last requested frame, we need to add just under an extra pseudo second to
   // the end time - stopExport. If we added a full extra second, we would export one video frame of the band past
   // the last requested frame - could cause crash if the last request frame was the last band.
   if (eType == FRAME_ID)
   {
      stopExport += 0.99;
   }

   bool drawClassMarkings = fullResolution &&
      Service<ConfigurationSettings>()->getSettingDisplayClassificationMarkings();
   QString classText;
   QFont classFont;
   QColor classColor;
   QPoint classPositionTop;
   QPoint classPositionBottom;
   const int shadowOffset = 2;
   if (drawClassMarkings)
   {
      const int topMargin = 1;
      const int bottomMargin = 4;
      const int leftMargin = 5;
      const int rightMargin = 5;

      classText = QString::fromStdString(pView->getClassificationText());
      classColor = COLORTYPE_TO_QCOLOR(pView->getClassificationColor());
      pView->getClassificationFont(classFont);
      QFontMetrics fontMetrics(classFont);
      int classWidth = fontMetrics.width(classText);
      int classHeight = fontMetrics.ascent();
      int topX((pCodecContext->width / 2) - (classWidth / 2) + shadowOffset);
      int bottomX((pCodecContext->width / 2) - (classWidth / 2));
      // determine the classification position
      switch (pView->getClassificationPosition())
      {
      case TOP_LEFT_BOTTOM_LEFT:
         topX = leftMargin + shadowOffset;
         bottomX = leftMargin + shadowOffset;
         break;
      case TOP_LEFT_BOTTOM_RIGHT:
         topX = leftMargin + shadowOffset;
         bottomX = pCodecContext->width - rightMargin - classWidth + shadowOffset;
         break;
      case TOP_RIGHT_BOTTOM_LEFT:
         topX = pCodecContext->width - rightMargin - classWidth + shadowOffset;
         bottomX = leftMargin + shadowOffset;
         break;
      case TOP_RIGHT_BOTTOM_RIGHT:
         topX = pCodecContext->width - rightMargin - classWidth + shadowOffset;
         bottomX = pCodecContext->width - rightMargin - classWidth + shadowOffset;
         break;
      default:
         // nothing to do, markings centered by default
         break;
      }
      int screenY = 1 + classHeight;
      classPositionTop = QPoint(topX, 1 + classHeight);
      classPositionBottom = QPoint(bottomX, pCodecContext->height - 1);
   }

   // make sure controller is not running prior to export. Save current state and restore after export finished
   AnimationState savedAnimationState = pController->getAnimationState();
   pController->setAnimationState(STOP);

   for (double video_pts = startExport; video_pts <= stopExport; video_pts += interval)
   {
      if (isAborted() == true)
      {
         // reset resources to close output file so it can be deleted
         pVideoStream = AvStreamResource();
         pFormat = AvFormatContextResource(NULL);
         mpPicture = NULL;
         free(mpVideoOutbuf);
         mpVideoOutbuf = NULL;
         remove(filename.c_str());

         if (mpProgress != NULL)
         {
            mpProgress->updateProgress("Export aborted", 0, ABORT);
         }
         pController->setAnimationState(savedAnimationState);
         mpStep->finalize(Message::Abort);
         return false;
      }

      // generate the next frame
      pController->setCurrentFrame(video_pts);
      if (mpProgress != NULL)
      {
         double progressValue = (video_pts - startExport) / (stopExport - startExport) * 100.0;
         mpProgress->updateProgress("Saving movie", static_cast<int>(progressValue), NORMAL);
      }
      if (fullResolution)
      {
         QSize totalSize(pCodecContext->width, pCodecContext->height);
         QSize subImageSize(pView->getWidget()->width(), pView->getWidget()->height());
         // Make sure that the sub-image and the main image have the same aspect ratio to
         // minimize wasted tile space
         if (pCodecContext->width > pCodecContext->height)
         {
            subImageSize.setWidth(
               totalSize.width() / static_cast<float>(totalSize.height()) * subImageSize.height() + 0.5);
         }
         else
         {
            subImageSize.setHeight(
               totalSize.height() / static_cast<float>(totalSize.width()) * subImageSize.width() + 0.5);
         }
         // Remove pan and zoom limits so they don't interfere with the subimage grab
         // and restore them when done
         SpatialDataView* pSdv = dynamic_cast<SpatialDataView*>(pView);
         PanLimitType panLimit;
         if (pSdv != NULL)
         {
            panLimit = pSdv->getPanLimit();
            pSdv->setPanLimit(NO_LIMIT);
         }
         View::SubImageIterator* pSubImage(pView->getSubImageIterator(totalSize, subImageSize));
         if (pSubImage == NULL)
         {
            if (pSdv != NULL)
            {
               pSdv->setPanLimit(panLimit);
            }
            if (mpProgress != NULL)
            {
               mpProgress->updateProgress("Unable to render full scale data", 0, ERRORS);
            }
            pController->setAnimationState(savedAnimationState);
            mpStep->finalize(Message::Failure);
            return false;
         }
         QPainter outPainter(&image);
         QPoint origin(0, image.height() - subImageSize.height());
         while (pSubImage->hasNext())
         {
            QImage subImage;
            if (!pSubImage->next(subImage))
            {
               if (pSdv != NULL)
               {
                  pSdv->setPanLimit(panLimit);
               }
               if (mpProgress != NULL)
               {
                  mpProgress->updateProgress("An error occurred when generating the image", 0, ERRORS);
               }
               pController->setAnimationState(savedAnimationState);
               mpStep->finalize(Message::Failure);
               delete pSubImage;
               return false;
            }
            // copy this subimage to the output buffer
            outPainter.drawImage(origin, subImage);

            int newX = origin.x() + subImage.width();
            int newY = origin.y();
            if (newX >= totalSize.width())
            {
               newY -= subImage.height();
               newX = 0;
            }
            origin = QPoint(newX, newY);
         }
         delete pSubImage;
         if (drawClassMarkings)
         {
            outPainter.setFont(classFont);
            outPainter.setPen(Qt::black);
            outPainter.drawText(classPositionTop, classText);
            outPainter.drawText(classPositionBottom, classText);
            outPainter.setPen(classColor);
            outPainter.drawText(classPositionTop - QPoint(shadowOffset, shadowOffset), classText);
            outPainter.drawText(classPositionBottom - QPoint(shadowOffset, shadowOffset), classText);
         }
         if (pSdv != NULL)
         {
            pSdv->setPanLimit(panLimit);
         }
      }
      else
      {
         pView->getCurrentImage(image);
      }
      img_convert(reinterpret_cast<AVPicture*>(mpPicture),
         pCodecContext->pix_fmt,
         reinterpret_cast<AVPicture*>(pTmpPicture),
         PIX_FMT_RGBA32,
         pCodecContext->width,
         pCodecContext->height);
      if (!write_video_frame(pFormat, pVideoStream))
      {
         // reset resources to close output file so it can be deleted
         pVideoStream = AvStreamResource();
         pFormat = AvFormatContextResource(NULL);
         mpPicture = NULL;
         free(mpVideoOutbuf);
         mpVideoOutbuf = NULL;
         remove(filename.c_str());
         string msg = "Can't write frame.";
         log_error(msg.c_str());
         pController->setAnimationState(savedAnimationState);
         return false;
      }
   }
   for (int frame = 0; frame < pCodecContext->delay; ++frame)
   {
      write_video_frame(pFormat, pVideoStream);
   }

   av_write_trailer(pFormat);

   if (mpProgress != NULL)
   {
      mpProgress->updateProgress("Finished saving movie", 100, NORMAL);
   }

   free(mpVideoOutbuf);
   mpVideoOutbuf = NULL;
   pController->setAnimationState(savedAnimationState);
   mpStep->finalize(Message::Success);
   return true;
}