17#include "movieexporter.h"
25#include <QApplication>
26#include <QStandardPaths>
30#include <QRegularExpression>
33#include "layercamera.h"
34#include "layersound.h"
38#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
44MovieExporter::MovieExporter()
48MovieExporter::~MovieExporter()
80 std::function<
void(
float,
float)> majorProgress,
81 std::function<
void(
float)> minorProgress,
82 std::function<
void(
QString)> progressMessage)
84 majorProgress(0.f, 0.03f);
86 progressMessage(tr(
"Checking environment..."));
90 QString ffmpegPath = ffmpegLocation();
91 qDebug() << ffmpegPath;
92 if (!QFile::exists(ffmpegPath))
95 qCritical() <<
"Please place ffmpeg.exe in " << ffmpegPath <<
" directory";
97 qCritical() <<
"Please place ffmpeg in " << ffmpegPath <<
" directory";
99 return Status::ERROR_FFMPEG_NOT_FOUND;
102 STATUS_CHECK(checkInputParameters(desc))
105 qDebug() <<
"OutFile: " << mDesc.strFileName;
110 Status status = Status::FAIL;
111 status.setTitle(tr(
"Error creating folder"));
112 status.setDescription(tr(
"Unable to create a temporary folder, cannot export animation."));
116 status.setDetails(dd);
120 mTempWorkDir = mTempDir.
path();
125 majorProgress(0.03f, 1.f);
126 progressMessage(tr(
"Generating GIF..."));
128 STATUS_CHECK(
generateGif(obj, ffmpegPath, desc.strFileName, minorProgress))
132 majorProgress(0.03f, 0.25f);
133 progressMessage(tr(
"Assembling audio..."));
137 majorProgress(0.25f, 1.f);
138 progressMessage(tr(
"Generating movie..."));
139 STATUS_CHECK(
generateMovie(obj, ffmpegPath, desc.strFileName, minorProgress))
142 majorProgress(1.f, 1.f);
143 progressMessage(tr(
"Done"));
145 clock_t t2 = clock() - t1;
146 qDebug(
"MOVIE = %.1f sec",
static_cast<double>(t2 / CLOCKS_PER_SEC));
170 std::function<
void(
float)> progress)
173 const int startFrame = mDesc.startFrame;
174 const int endFrame = mDesc.endFrame;
175 const int fps = mDesc.fps;
177 Q_ASSERT(startFrame >= 0);
178 Q_ASSERT(endFrame >= startFrame);
180 QDir dir(mTempWorkDir);
184 qDebug() <<
"TempAudio=" << tempAudioPath;
186 std::vector< SoundClip* > allSoundClips;
188 std::vector< LayerSound* > allSoundLayers = obj->getLayersByType<
LayerSound>();
191 if (!layer->visible()) {
continue; }
192 layer->foreachKeyFrame([&allSoundClips](
KeyFrame* key)
194 if (!key->fileName().
isEmpty())
196 allSoundClips.push_back(
static_cast<SoundClip*
>(key));
201 if (allSoundClips.empty())
return Status::SAFE;
205 QString filterComplex, amergeInput, panChannelLayout;
208 int wholeLen = qCeil(endFrame * 44100.0 / fps);
209 for (
auto clip : allSoundClips)
213 return Status::CANCELED;
217 args <<
"-i" << clip->fileName();
221 filterComplex +=
QString(
"[%1:a:0] aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=mono,volume=1,adelay=%2S|%2S,apad=whole_len=%3[ad%1];")
222 .
arg(clipCount).
arg(qRound(44100.0 * (clip->pos() - 1) / fps)).
arg(wholeLen);
223 amergeInput +=
QString(
"[ad%1]").
arg(clipCount);
224 panChannelLayout +=
QString(
"c%1+").
arg(clipCount);
229 panChannelLayout.
chop(1);
232 args <<
"-filter_complex";
237 filterComplex.
chop(1);
238 args << filterComplex <<
"-map" << amergeInput;
241 args <<
QString(
"%1%2 amerge=inputs=%3, pan=mono|c0=%4 [out]")
242 .
arg(filterComplex).
arg(amergeInput).
arg(clipCount).
arg(panChannelLayout);
243 args <<
"-map" <<
"[out]";
247 args <<
"-ar" <<
"44100" <<
"-acodec" <<
"pcm_s16le" <<
"-ac" <<
"2" <<
"-y";
249 args <<
"-ss" <<
QString::number((startFrame - 1) /
static_cast<double>(fps));
252 args << tempAudioPath;
254 STATUS_CHECK(
MovieExporter::executeFFmpeg(ffmpegPath, args, [&progress,
this] (
int frame) { progress(frame /
static_cast<float>(mDesc.endFrame - mDesc.startFrame));
return !mCanceled; }))
255 qDebug() <<
"audio file: " + tempAudioPath;
280 std::function<
void(
float)> progress)
284 return Status::CANCELED;
289 int frameStart = mDesc.startFrame;
290 int frameEnd = mDesc.endFrame;
291 const QSize exportSize = mDesc.exportSize;
292 bool transparency = mDesc.alpha;
293 QString strCameraName = mDesc.strCameraName;
294 bool loop = mDesc.loop;
296 auto cameraLayer =
static_cast<LayerCamera*
>(obj->findLayerByName(strCameraName, Layer::CAMERA));
297 if (cameraLayer ==
nullptr)
299 cameraLayer = obj->getLayersByType<
LayerCamera >().front();
301 int currentFrame = frameStart;
314 imageToExportBase.
fill(bgColor);
316 QSize camSize = cameraLayer->getViewSize();
328 int frameWindow =
static_cast<int>(1e9 / (camSize.
width() * camSize.
height() * 4.0));
335 QStringList args = {
"-f",
"rawvideo",
"-pixel_format",
"bgra"};
343 if (QFile::exists(tempAudioPath))
345 args <<
"-i" << tempAudioPath;
350 args <<
"-plays" << (loop ?
"0" :
"1");
355 args <<
"-pix_fmt" <<
"yuv420p";
360 args <<
"-q:v" <<
"5";
364 args << strOutputFile;
370 if(framesProcessed < 0)
375 if(currentFrame > frameEnd)
381 if((currentFrame - frameStart <= framesProcessed + frameWindow || failCounter > 10) && currentFrame <= frameEnd)
383 QImage imageToExport = imageToExportBase.
copy();
386 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
390 obj->paintImage(painter, currentFrame,
false,
true);
393#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
395 Q_ASSERT(bytesWritten == imageToExport.
sizeInBytes());
397 int bytesWritten = ffmpeg.
write(
reinterpret_cast<const char*
>(imageToExport.
constBits()), imageToExport.
byteCount());
398 Q_ASSERT(bytesWritten == imageToExport.
byteCount());
408 STATUS_CHECK(status);
429 std::function<
void(
float)> progress)
434 return Status::CANCELED;
439 int frameStart = mDesc.startFrame;
440 int frameEnd = mDesc.endFrame;
441 const QSize exportSize = mDesc.exportSize;
442 bool transparency =
false;
443 QString strCameraName = mDesc.strCameraName;
444 bool loop = mDesc.loop;
447 auto cameraLayer =
static_cast<LayerCamera*
>(obj->findLayerByName(strCameraName, Layer::CAMERA));
448 if (cameraLayer ==
nullptr)
450 cameraLayer = obj->getLayersByType<
LayerCamera >().front();
452 int currentFrame = frameStart;
465 imageToExportBase.
fill(bgColor);
467 QSize camSize = cameraLayer->getViewSize();
473 QStringList args = {
"-f",
"rawvideo",
"-pixel_format",
"bgra"};
481 args <<
"-filter_complex" <<
"[0:v]palettegen [p]; [0:v][p] paletteuse";
483 args <<
"-loop" << (loop ?
"0" :
"-1");
498 Q_UNUSED(framesProcessed);
499 if(currentFrame > frameEnd)
505 QImage imageToExport = imageToExportBase.
copy();
508 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
512 obj->paintImage(painter, currentFrame,
false,
true);
514#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
516 Q_ASSERT(bytesWritten == imageToExport.
sizeInBytes());
518 bytesWritten = ffmpeg.
write(
reinterpret_cast<const char*
>(imageToExport.
constBits()), imageToExport.
byteCount());
519 Q_ASSERT(bytesWritten == imageToExport.
byteCount());
526 STATUS_CHECK(status);
555 ffmpeg.
start(cmd, args);
557 Status status = Status::OK;
559 dd << QStringLiteral(
"Command: %1 %2").arg(cmd).arg(args.
join(
' '));
570 qDebug() <<
"[ffmpeg]" << s;
578 bool shouldContinue = progress(frame.
toInt());
585 return Status::CANCELED;
594 qDebug() <<
"[ffmpeg]" << s;
600 status = Status::FAIL;
601 status.setTitle(tr(
"Something went wrong"));
602 status.setDescription(tr(
"Looks like our video backend did not exit normally. Your movie may not have exported correctly. Please try again and report this if it persists."));
605 status.setDetails(dd);
611 qDebug() <<
"ERROR: Could not execute FFmpeg.";
612 status = Status::FAIL;
613 status.setTitle(tr(
"Something went wrong"));
614 status.setDescription(tr(
"Couldn't start the video backend, please try again."));
615 status.setDetails(dd);
671 ffmpeg.
start(cmd, args);
673 Status status = Status::OK;
675 dd << QStringLiteral(
"Command: %1 %2").arg(cmd).arg(args.
join(
' '));
678 int framesGenerated = 0;
679 int lastFrameProcessed = 0;
680 const int frameStart = mDesc.startFrame;
681 const int frameEnd = mDesc.endFrame;
688 return Status::CANCELED;
693 int framesProcessed = -1;
700 qDebug() <<
"[ffmpeg]" << s;
705 lastFrameProcessed = framesProcessed = output.
mid(6, output.
indexOf(
' ')).
toInt();
714 while(writeFrame(ffmpeg, framesProcessed))
718 const float percentGenerated = framesGenerated /
static_cast<float>(frameEnd - frameStart);
719 const float percentConverted = lastFrameProcessed /
static_cast<float>(frameEnd - frameStart);
720 progress((percentGenerated + percentConverted) / 2);
722 const float percentGenerated = framesGenerated /
static_cast<float>(frameEnd - frameStart);
723 const float percentConverted = lastFrameProcessed /
static_cast<float>(frameEnd - frameStart);
724 progress((percentGenerated + percentConverted) / 2);
731 qDebug() <<
"[ffmpeg]" << s;
737 status = Status::FAIL;
738 status.setTitle(tr(
"Something went wrong"));
739 status.setDescription(tr(
"Looks like our video backend did not exit normally. Your movie may not have exported correctly. Please try again and report this if it persists."));
742 status.setDetails(dd);
748 qDebug() <<
"ERROR: Could not execute FFmpeg.";
749 status = Status::FAIL;
750 status.setTitle(tr(
"Something went wrong"));
751 status.setDescription(tr(
"Couldn't start the video backend, please try again."));
752 status.setDetails(dd);
761 b &= (!desc.strFileName.
isEmpty());
762 b &= (desc.startFrame > 0);
763 b &= (desc.endFrame >= desc.startFrame);
765 b &= (!desc.strCameraName.
isEmpty());
767 return b ? Status::OK : Status::INVALID_ARGUMENT;
Status run(const Object *obj, const ExportMovieDesc &desc, std::function< void(float, float)> majorProgress, std::function< void(float)> minorProgress, std::function< void(QString)> progressMessage)
Begin exporting the movie described by exportDesc.
Status assembleAudio(const Object *obj, QString ffmpegPath, std::function< void(float)> progress)
Combines all audio tracks in obj into a single file.
Status generateMovie(const Object *obj, QString ffmpegPath, QString strOutputFile, std::function< void(float)> progress)
Exports obj to a movie image at strOut using FFmpeg.
Status generateGif(const Object *obj, QString ffmpeg, QString strOut, std::function< void(float)> progress)
Exports obj to a gif image at strOut using FFmpeg.
Status executeFFMpegPipe(const QString &cmd, const QStringList &args, std::function< void(float)> progress, std::function< bool(QProcess &, int)> writeFrame)
Runs the specified command (should be ffmpeg), and lets writeFrame pipe data into it 1 frame at a tim...
static Status executeFFmpeg(const QString &cmd, const QStringList &args, std::function< bool(int)> progress)
Runs the specified command (should be ffmpeg) and allows for progress feedback.
bool exists() const const
QString filePath(const QString &fileName) const const
int byteCount() const const
const uchar * constBits() const const
QImage copy(const QRect &rectangle) const const
void fill(uint pixelValue)
int sizeInBytes() const const
bool isWritable() const const
qint64 write(const char *data, qint64 maxSize)
void setWindow(const QRect &rectangle)
int exitCode() const const
QProcess::ExitStatus exitStatus() const const
void setProcessChannelMode(QProcess::ProcessChannelMode mode)
void setReadChannel(QProcess::ProcessChannel channel)
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
QProcess::ProcessState state() const const
bool waitForFinished(int msecs)
virtual bool waitForReadyRead(int msecs) override
bool waitForStarted(int msecs)
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString & append(QChar ch)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString mid(int position, int n) const const
QString number(int n, int base)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QString join(const QString &separator) const const
QString errorString() const const
bool isValid() const const
QString path() const const