diff --git a/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp b/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp index 5478ba858e..1afb8acd6d 100644 --- a/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp +++ b/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp @@ -1,127 +1,127 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KisAsyncAnimationFramesSavingRenderer.h" #include "kis_image.h" #include "kis_paint_device.h" #include "KisImportExportFilter.h" #include "KisPart.h" #include "KisDocument.h" #include "kis_time_range.h" #include "kis_paint_layer.h" struct KisAsyncAnimationFramesSavingRenderer::Private { Private(KisImageSP image, const KisTimeRange &_range, int _sequenceNumberingOffset, KisPropertiesConfigurationSP _exportConfiguration) : savingDoc(KisPart::instance()->createDocument()), range(_range), sequenceNumberingOffset(_sequenceNumberingOffset), exportConfiguration(_exportConfiguration) { savingDoc->setAutoSaveDelay(0); savingDoc->setFileBatchMode(true); KisImageSP savingImage = new KisImage(savingDoc->createUndoStore(), image->bounds().width(), image->bounds().height(), image->colorSpace(), QString()); savingImage->setResolution(image->xRes(), image->yRes()); savingDoc->setCurrentImage(savingImage); KisPaintLayer* paintLayer = new KisPaintLayer(savingImage, "paint device", 255); savingImage->addNode(paintLayer, savingImage->root(), KisLayerSP(0)); savingDevice = paintLayer->paintDevice(); } QScopedPointer savingDoc; KisPaintDeviceSP savingDevice; KisTimeRange range; int sequenceNumberingOffset = 0; QString filenamePrefix; QString filenameSuffix; QByteArray outputMimeType; KisPropertiesConfigurationSP exportConfiguration; }; KisAsyncAnimationFramesSavingRenderer::KisAsyncAnimationFramesSavingRenderer(KisImageSP image, const QString &fileNamePrefix, const QString &fileNameSuffix, const QByteArray &outputMimeType, const KisTimeRange &range, const int sequenceNumberingOffset, KisPropertiesConfigurationSP exportConfiguration) : m_d(new Private(image, range, sequenceNumberingOffset, exportConfiguration)) { m_d->filenamePrefix = fileNamePrefix; m_d->filenameSuffix = fileNameSuffix; m_d->outputMimeType = outputMimeType; connect(this, SIGNAL(sigCompleteRegenerationInternal(int)), SLOT(notifyFrameCompleted(int))); connect(this, SIGNAL(sigCancelRegenerationInternal(int)), SLOT(notifyFrameCancelled(int))); } KisAsyncAnimationFramesSavingRenderer::~KisAsyncAnimationFramesSavingRenderer() { } void KisAsyncAnimationFramesSavingRenderer::frameCompletedCallback(int frame) { KisImageSP image = requestedImage(); if (!image) return; m_d->savingDevice->makeCloneFromRough(image->projection(), image->bounds()); KisTimeRange range(frame, 1); KisImportExportFilter::ConversionStatus status = KisImportExportFilter::OK; for (int i = range.start(); i <= range.end(); i++) { - QString frameNumber = QString("%1").arg(i - m_d->range.start() + m_d->sequenceNumberingOffset, 4, 10, QChar('0')); + QString frameNumber = QString("%1").arg(i + m_d->sequenceNumberingOffset, 4, 10, QChar('0')); QString filename = m_d->filenamePrefix + frameNumber + m_d->filenameSuffix; if (!m_d->savingDoc->exportDocumentSync(QUrl::fromLocalFile(filename), m_d->outputMimeType, m_d->exportConfiguration)) { status = KisImportExportFilter::InternalError; break; } } if (status == KisImportExportFilter::OK) { emit sigCompleteRegenerationInternal(frame); } else { emit sigCancelRegenerationInternal(frame); } } void KisAsyncAnimationFramesSavingRenderer::frameCancelledCallback(int frame) { notifyFrameCancelled(frame); } diff --git a/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.cpp b/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.cpp index c12d5bc5e8..28059211fb 100644 --- a/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.cpp +++ b/libs/ui/dialogs/KisAsyncAnimationRenderDialogBase.cpp @@ -1,333 +1,333 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KisAsyncAnimationRenderDialogBase.h" #include #include #include #include #include #include #include #include #include #include "KisViewManager.h" #include "KisAsyncAnimationRendererBase.h" #include "kis_time_range.h" #include "kis_image.h" #include "kis_image_config.h" #include "kis_memory_statistics_server.h" #include #include namespace { struct RendererPair { std::unique_ptr renderer; KisImageSP image; RendererPair() {} RendererPair(KisAsyncAnimationRendererBase *_renderer, KisImageSP _image) : renderer(_renderer), image(_image) { } RendererPair(RendererPair &&rhs) : renderer(std::move(rhs.renderer)), image(rhs.image) { } }; int calculateNumberMemoryAllowedClones(KisImageSP image) { KisMemoryStatisticsServer::Statistics stats = KisMemoryStatisticsServer::instance() ->fetchMemoryStatistics(image); const qint64 allowedMemory = 0.8 * stats.tilesHardLimit - stats.realMemorySize; const qint64 cloneSize = stats.projectionsSize; - return allowedMemory > 0 ? allowedMemory / cloneSize : 0; + return cloneSize > 0 ? allowedMemory / cloneSize : 0; } } struct KisAsyncAnimationRenderDialogBase::Private { Private(const QString &_actionTitle, KisImageSP _image, int _busyWait) : actionTitle(_actionTitle), image(_image), busyWait(_busyWait) { } QString actionTitle; KisImageSP image; int busyWait; bool isBatchMode = false; std::vector asyncRenderers; bool memoryLimitReached = false; QElapsedTimer processingTime; QScopedPointer progressDialog; QEventLoop waitLoop; QList stillDirtyFrames; QList framesInProgress; int dirtyFramesCount = 0; Result result = RenderComplete; int numDirtyFramesLeft() const { return stillDirtyFrames.size() + framesInProgress.size(); } }; KisAsyncAnimationRenderDialogBase::KisAsyncAnimationRenderDialogBase(const QString &actionTitle, KisImageSP image, int busyWait) : m_d(new Private(actionTitle, image, busyWait)) { } KisAsyncAnimationRenderDialogBase::~KisAsyncAnimationRenderDialogBase() { } KisAsyncAnimationRenderDialogBase::Result KisAsyncAnimationRenderDialogBase::regenerateRange(KisViewManager *viewManager) { { /** * Since this method can be called from the places where no * view manager is available, we need this manually crafted * ugly construction to "try-lock-cance" the image. */ bool imageIsIdle = true; if (viewManager) { imageIsIdle = viewManager->blockUntilOperationsFinished(m_d->image); } else { imageIsIdle = false; if (m_d->image->tryBarrierLock(true)) { m_d->image->unlock(); imageIsIdle = true; } } if (!imageIsIdle) { return RenderCancelled; } } m_d->stillDirtyFrames = calcDirtyFrames(); m_d->framesInProgress.clear(); m_d->result = RenderComplete; m_d->dirtyFramesCount = m_d->stillDirtyFrames.size(); if (!m_d->isBatchMode) { QWidget *parentWidget = viewManager ? viewManager->mainWindow() : 0; m_d->progressDialog.reset(new QProgressDialog(m_d->actionTitle, i18n("Cancel"), 0, 0, parentWidget)); m_d->progressDialog->setWindowModality(Qt::ApplicationModal); m_d->progressDialog->setMinimum(0); m_d->progressDialog->setMaximum(m_d->dirtyFramesCount); m_d->progressDialog->setMinimumDuration(m_d->busyWait); connect(m_d->progressDialog.data(), SIGNAL(canceled()), SLOT(slotCancelRegeneration())); } if (m_d->dirtyFramesCount <= 0) return m_d->result; m_d->processingTime.start(); KisImageConfig cfg; const int maxThreads = cfg.maxNumberOfThreads(); const int numAllowedWorker = 1 + calculateNumberMemoryAllowedClones(m_d->image); const int proposedNumWorkers = qMin(m_d->dirtyFramesCount, cfg.frameRenderingClones()); const int numWorkers = qMin(proposedNumWorkers, numAllowedWorker); const int numThreadsPerWorker = qMax(1, qCeil(qreal(maxThreads) / numWorkers)); m_d->memoryLimitReached = numWorkers < proposedNumWorkers; const int oldWorkingThreadsLimit = m_d->image->workingThreadsLimit(); ENTER_FUNCTION() << ppVar(numWorkers) << ppVar(numThreadsPerWorker); for (int i = 0; i < numWorkers; i++) { // reuse the image for one of the workers KisImageSP image = i == numWorkers - 1 ? m_d->image : m_d->image->clone(true); image->setWorkingThreadsLimit(numThreadsPerWorker); KisAsyncAnimationRendererBase *renderer = createRenderer(image); connect(renderer, SIGNAL(sigFrameCompleted(int)), SLOT(slotFrameCompleted(int))); connect(renderer, SIGNAL(sigFrameCancelled(int)), SLOT(slotFrameCancelled(int))); m_d->asyncRenderers.push_back(RendererPair(renderer, image)); } ENTER_FUNCTION() << "Copying done in" << m_d->processingTime.elapsed(); tryInitiateFrameRegeneration(); updateProgressLabel(); if (m_d->numDirtyFramesLeft() > 0) { m_d->waitLoop.exec(); } ENTER_FUNCTION() << "Full regeneration done in" << m_d->processingTime.elapsed(); for (auto &pair : m_d->asyncRenderers) { KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive()); if (viewManager) { viewManager->blockUntilOperationsFinishedForced(pair.image); } else { pair.image->barrierLock(true); pair.image->unlock(); } } m_d->asyncRenderers.clear(); if (viewManager) { viewManager->blockUntilOperationsFinishedForced(m_d->image); } else { m_d->image->barrierLock(true); m_d->image->unlock(); } m_d->image->setWorkingThreadsLimit(oldWorkingThreadsLimit); m_d->progressDialog.reset(); return m_d->result; } void KisAsyncAnimationRenderDialogBase::slotFrameCompleted(int frame) { Q_UNUSED(frame); m_d->framesInProgress.removeOne(frame); tryInitiateFrameRegeneration(); updateProgressLabel(); } void KisAsyncAnimationRenderDialogBase::slotFrameCancelled(int frame) { Q_UNUSED(frame); cancelProcessingImpl(false); } void KisAsyncAnimationRenderDialogBase::slotCancelRegeneration() { cancelProcessingImpl(true); } void KisAsyncAnimationRenderDialogBase::cancelProcessingImpl(bool isUserCancelled) { for (auto &pair : m_d->asyncRenderers) { if (pair.renderer->isActive()) { pair.renderer->cancelCurrentFrameRendering(); } KIS_SAFE_ASSERT_RECOVER_NOOP(!pair.renderer->isActive()); } m_d->stillDirtyFrames.clear(); m_d->framesInProgress.clear(); m_d->result = isUserCancelled ? RenderCancelled : RenderFailed; updateProgressLabel(); } void KisAsyncAnimationRenderDialogBase::tryInitiateFrameRegeneration() { bool hadWorkOnPreviousCycle = false; while (!m_d->stillDirtyFrames.isEmpty()) { for (auto &pair : m_d->asyncRenderers) { if (!pair.renderer->isActive()) { const int currentDirtyFrame = m_d->stillDirtyFrames.takeFirst(); initializeRendererForFrame(pair.renderer.get(), pair.image, currentDirtyFrame); pair.renderer->startFrameRegeneration(pair.image, currentDirtyFrame); hadWorkOnPreviousCycle = true; m_d->framesInProgress.append(currentDirtyFrame); break; } } if (!hadWorkOnPreviousCycle) break; hadWorkOnPreviousCycle = false; } } void KisAsyncAnimationRenderDialogBase::updateProgressLabel() { const int processedFramesCount = m_d->dirtyFramesCount - m_d->numDirtyFramesLeft(); const qint64 elapsedMSec = m_d->processingTime.elapsed(); const qint64 estimatedMSec = !processedFramesCount ? 0 : elapsedMSec * m_d->dirtyFramesCount / processedFramesCount; const QTime elapsedTime = QTime::fromMSecsSinceStartOfDay(elapsedMSec); const QTime estimatedTime = QTime::fromMSecsSinceStartOfDay(estimatedMSec); const QString timeFormat = estimatedTime.hour() > 0 ? "HH:mm:ss" : "mm:ss"; const QString elapsedTimeString = elapsedTime.toString(timeFormat); const QString estimatedTimeString = estimatedTime.toString(timeFormat); const QString memoryLimitMessage( i18n("\n\nMemory limit is reached!\nThe number of clones is limited to %1\n\n", m_d->asyncRenderers.size())); const QString progressLabel(i18n("%1\n\nElapsed: %2\nEstimated: %3\n\n%4", m_d->actionTitle, elapsedTimeString, estimatedTimeString, m_d->memoryLimitReached ? memoryLimitMessage : QString())); if (m_d->progressDialog) { m_d->progressDialog->setLabelText(progressLabel); m_d->progressDialog->setValue(processedFramesCount); } if (!m_d->numDirtyFramesLeft()) { m_d->waitLoop.quit(); } } void KisAsyncAnimationRenderDialogBase::setBatchMode(bool value) { m_d->isBatchMode = value; } bool KisAsyncAnimationRenderDialogBase::batchMode() const { return m_d->isBatchMode; } diff --git a/plugins/impex/video/video_saver.cpp b/plugins/impex/video/video_saver.cpp index d0564943a9..4521d300dc 100644 --- a/plugins/impex/video/video_saver.cpp +++ b/plugins/impex/video/video_saver.cpp @@ -1,361 +1,365 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "video_saver.h" #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include #include #include #include #include #include #include #include "KisPart.h" class KisFFMpegProgressWatcher : public QObject { Q_OBJECT public: KisFFMpegProgressWatcher(QFile &progressFile, int totalFrames) : m_progressFile(progressFile), m_totalFrames(totalFrames) { connect(&m_progressWatcher, SIGNAL(fileChanged(QString)), SLOT(slotFileChanged())); m_progressWatcher.addPath(m_progressFile.fileName()); } private Q_SLOTS: void slotFileChanged() { int currentFrame = -1; bool isEnded = false; while(!m_progressFile.atEnd()) { QString line = QString(m_progressFile.readLine()).remove(QChar('\n')); QStringList var = line.split("="); if (var[0] == "frame") { currentFrame = var[1].toInt(); } else if (var[0] == "progress") { isEnded = var[1] == "end"; } } if (isEnded) { emit sigProgressChanged(100); emit sigProcessingFinished(); } else { emit sigProgressChanged(100 * currentFrame / m_totalFrames); } } Q_SIGNALS: void sigProgressChanged(int percent); void sigProcessingFinished(); private: QFileSystemWatcher m_progressWatcher; QFile &m_progressFile; int m_totalFrames; }; class KisFFMpegRunner { public: KisFFMpegRunner(const QString &ffmpegPath) : m_cancelled(false), m_ffmpegPath(ffmpegPath) {} public: KisImageBuilder_Result runFFMpeg(const QStringList &specialArgs, const QString &actionName, const QString &logPath, int totalFrames) { dbgFile << "runFFMpeg: specialArgs" << specialArgs << "actionName" << actionName << "logPath" << logPath << "totalFrames" << totalFrames; QTemporaryFile progressFile(QDir::tempPath() + QDir::separator() + "KritaFFmpegProgress.XXXXXX"); progressFile.open(); m_process.setStandardOutputFile(logPath); m_process.setProcessChannelMode(QProcess::MergedChannels); QStringList args; args << "-v" << "debug" << "-nostdin" << "-progress" << progressFile.fileName() << specialArgs; qDebug() << "\t" << m_ffmpegPath << args.join(" "); m_cancelled = false; m_process.start(m_ffmpegPath, args); return waitForFFMpegProcess(actionName, progressFile, m_process, totalFrames); } void cancel() { m_cancelled = true; m_process.kill(); } private: KisImageBuilder_Result waitForFFMpegProcess(const QString &message, QFile &progressFile, QProcess &ffmpegProcess, int totalFrames) { KisFFMpegProgressWatcher watcher(progressFile, totalFrames); QProgressDialog progress(message, "", 0, 0, KisPart::instance()->currentMainwindow()); progress.setWindowModality(Qt::ApplicationModal); progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); progress.setRange(0, 100); QEventLoop loop; loop.connect(&watcher, SIGNAL(sigProcessingFinished()), SLOT(quit())); loop.connect(&ffmpegProcess, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(quit())); loop.connect(&ffmpegProcess, SIGNAL(error(QProcess::ProcessError)), SLOT(quit())); loop.connect(&watcher, SIGNAL(sigProgressChanged(int)), &progress, SLOT(setValue(int))); if (ffmpegProcess.state() != QProcess::NotRunning) { loop.exec(); // wait for some erroneous case ffmpegProcess.waitForFinished(5000); } KisImageBuilder_Result retval = KisImageBuilder_RESULT_OK; if (ffmpegProcess.state() != QProcess::NotRunning) { // sorry... ffmpegProcess.kill(); retval = KisImageBuilder_RESULT_FAILURE; } else if (m_cancelled) { retval = KisImageBuilder_RESULT_CANCEL; } else if (ffmpegProcess.exitCode()) { retval = KisImageBuilder_RESULT_FAILURE; } return retval; } private: QProcess m_process; bool m_cancelled; QString m_ffmpegPath; }; VideoSaver::VideoSaver(KisDocument *doc, const QString &ffmpegPath, bool batchMode) : m_image(doc->image()) , m_doc(doc) , m_batchMode(batchMode) , m_ffmpegPath(ffmpegPath) , m_runner(new KisFFMpegRunner(ffmpegPath)) { } VideoSaver::~VideoSaver() { } KisImageSP VideoSaver::image() { return m_image; } bool VideoSaver::hasFFMpeg() const { return !m_ffmpegPath.isEmpty(); } KisImageBuilder_Result VideoSaver::encode(const QString &filename, KisPropertiesConfigurationSP configuration) { qDebug() << "ffmpeg" << m_ffmpegPath << "filename" << filename << "configuration" << configuration->toXML(); if (m_ffmpegPath.isEmpty()) { m_ffmpegPath = configuration->getString("ffmpeg_path"); if (!QFileInfo(m_ffmpegPath).exists()) { return KisImageBuilder_RESULT_FAILURE; } } KisImageBuilder_Result result = KisImageBuilder_RESULT_OK; KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("first_frame")); KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("last_frame")); KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("height")); KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("width")); KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("include_audio")); KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("directory")); KIS_SAFE_ASSERT_RECOVER_NOOP(configuration->hasProperty("framerate")); KisImageAnimationInterface *animation = m_image->animationInterface(); const KisTimeRange fullRange = animation->fullClipRange(); const int frameRate = configuration->getInt("framerate", animation->framerate()); - const KisTimeRange clipRange(configuration->getInt("first_frame", fullRange.start()), configuration->getInt("last_frame", fullRange.end())); + const int sequenceNumberingOffset = configuration->getInt("sequence_start", 0); + const KisTimeRange clipRange(sequenceNumberingOffset + configuration->getInt("first_frame", fullRange.start()), + sequenceNumberingOffset + configuration->getInt("last_frame", fullRange.end()) + ); + const bool includeAudio = configuration->getBool("include_audio", true); const int exportHeight = configuration->getInt("height", int(m_image->height())); const int exportWidth = configuration->getInt("width", int(m_image->width())); // export dimensions could be off a little bit, so the last force option tweaks the pixels for the export to work const QString exportDimensions = QString("scale=w=").append(QString::number(exportWidth)).append(":h=") .append(QString::number(exportHeight)).append(":force_original_aspect_ratio=decrease"); const QDir framesDir(configuration->getString("directory")); QString resultFile; if (QFileInfo(filename).isAbsolute()) { resultFile = filename; } else { resultFile = framesDir.absolutePath() + "/" + filename; } const QFileInfo info(resultFile); const QString suffix = info.suffix().toLower(); const QString palettePath = framesDir.filePath("palette.png"); const QString savedFilesMask = configuration->getString("savedFilesMask"); const QStringList additionalOptionsList = configuration->getString("customUserOptions").split(' ', QString::SkipEmptyParts); if (suffix == "gif") { { QStringList args; args << "-r" << QString::number(frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask << "-vf" << "palettegen" << "-y" << palettePath; KisImageBuilder_Result result = m_runner->runFFMpeg(args, i18n("Fetching palette..."), framesDir.filePath("log_generate_palette_gif.log"), clipRange.duration()); if (result != KisImageBuilder_RESULT_OK) { return result; } } { QStringList args; args << "-r" << QString::number(frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask << "-i" << palettePath << "-lavfi" << "[0:v][1:v] paletteuse" << additionalOptionsList << "-y" << resultFile; // if we are exporting out at a different image size, we apply scaling filter if (m_image->height() != exportHeight || m_image->width() != exportWidth) { args << "-vf" << exportDimensions; } dbgFile << "savedFilesMask" << savedFilesMask << "start" << QString::number(clipRange.start()) << "duration" << clipRange.duration(); KisImageBuilder_Result result = m_runner->runFFMpeg(args, i18n("Encoding frames..."), framesDir.filePath("log_encode_gif.log"), clipRange.duration()); if (result != KisImageBuilder_RESULT_OK) { return result; } } } else { QStringList args; args << "-r" << QString::number(frameRate) << "-start_number" << QString::number(clipRange.start()) << "-i" << savedFilesMask; QFileInfo audioFileInfo = animation->audioChannelFileName(); if (includeAudio && audioFileInfo.exists()) { const int msecStart = clipRange.start() * 1000 / animation->framerate(); const int msecDuration = clipRange.duration() * 1000 / animation->framerate(); const QTime startTime = QTime::fromMSecsSinceStartOfDay(msecStart); const QTime durationTime = QTime::fromMSecsSinceStartOfDay(msecDuration); const QString ffmpegTimeFormat("H:m:s.zzz"); args << "-ss" << startTime.toString(ffmpegTimeFormat); args << "-t" << durationTime.toString(ffmpegTimeFormat); args << "-i" << audioFileInfo.absoluteFilePath(); } // if we are exporting out at a different image size, we apply scaling filter // export options HAVE to go after input options, so make sure this is after the audio import if (m_image->height() != exportHeight || m_image->width() != exportWidth) { args << "-vf" << exportDimensions; } args << additionalOptionsList << "-y" << resultFile; result = m_runner->runFFMpeg(args, i18n("Encoding frames..."), framesDir.filePath("log_encode.log"), clipRange.duration()); } return result; } void VideoSaver::cancel() { m_runner->cancel(); } #include "video_saver.moc"