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;
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 Q_ASSERT(
false &&
"Cannot create temp folder.");
114 mTempWorkDir = mTempDir.
path();
119 majorProgress(0.03f, 1.f);
120 progressMessage(tr(
"Generating GIF..."));
122 STATUS_CHECK(
generateGif(obj, ffmpegPath, desc.strFileName, minorProgress))
126 majorProgress(0.03f, 0.25f);
127 progressMessage(tr(
"Assembling audio..."));
131 majorProgress(0.25f, 1.f);
132 progressMessage(tr(
"Generating movie..."));
133 STATUS_CHECK(
generateMovie(obj, ffmpegPath, desc.strFileName, minorProgress))
136 majorProgress(1.f, 1.f);
137 progressMessage(tr(
"Done"));
139 clock_t t2 = clock() - t1;
140 qDebug(
"MOVIE = %.1f sec",
static_cast<double>(t2 / CLOCKS_PER_SEC));
164 std::function<
void(
float)> progress)
167 const int startFrame = mDesc.startFrame;
168 const int endFrame = mDesc.endFrame;
169 const int fps = mDesc.fps;
171 Q_ASSERT(startFrame >= 0);
172 Q_ASSERT(endFrame >= startFrame);
174 QDir dir(mTempWorkDir);
178 qDebug() <<
"TempAudio=" << tempAudioPath;
180 std::vector< SoundClip* > allSoundClips;
182 std::vector< LayerSound* > allSoundLayers = obj->getLayersByType<
LayerSound>();
185 if (!layer->visible()) {
continue; }
186 layer->foreachKeyFrame([&allSoundClips](
KeyFrame* key)
188 if (!key->fileName().
isEmpty())
190 allSoundClips.push_back(
static_cast<SoundClip*
>(key));
195 if (allSoundClips.empty())
return Status::SAFE;
199 QString filterComplex, amergeInput, panChannelLayout;
202 int wholeLen = qCeil(endFrame * 44100.0 / fps);
203 for (
auto clip : allSoundClips)
207 return Status::CANCELED;
211 args <<
"-i" << clip->fileName();
215 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];")
216 .
arg(clipCount).
arg(qRound(44100.0 * (clip->pos() - 1) / fps)).
arg(wholeLen);
217 amergeInput +=
QString(
"[ad%1]").
arg(clipCount);
218 panChannelLayout +=
QString(
"c%1+").
arg(clipCount);
223 panChannelLayout.
chop(1);
226 args <<
"-filter_complex";
231 filterComplex.
chop(1);
232 args << filterComplex <<
"-map" << amergeInput;
235 args <<
QString(
"%1%2 amerge=inputs=%3, pan=mono|c0=%4 [out]")
236 .
arg(filterComplex).
arg(amergeInput).
arg(clipCount).
arg(panChannelLayout);
237 args <<
"-map" <<
"[out]";
241 args <<
"-ar" <<
"44100" <<
"-acodec" <<
"pcm_s16le" <<
"-ac" <<
"2" <<
"-y";
243 args <<
"-ss" <<
QString::number((startFrame - 1) /
static_cast<double>(fps));
246 args << tempAudioPath;
248 STATUS_CHECK(
MovieExporter::executeFFmpeg(ffmpegPath, args, [&progress,
this] (
int frame) { progress(frame /
static_cast<float>(mDesc.endFrame - mDesc.startFrame));
return !mCanceled; }))
249 qDebug() <<
"audio file: " + tempAudioPath;
274 std::function<
void(
float)> progress)
278 return Status::CANCELED;
283 int frameStart = mDesc.startFrame;
284 int frameEnd = mDesc.endFrame;
285 const QSize exportSize = mDesc.exportSize;
286 bool transparency = mDesc.alpha;
287 QString strCameraName = mDesc.strCameraName;
288 bool loop = mDesc.loop;
290 auto cameraLayer =
static_cast<LayerCamera*
>(obj->findLayerByName(strCameraName, Layer::CAMERA));
291 if (cameraLayer ==
nullptr)
293 cameraLayer = obj->getLayersByType<
LayerCamera >().front();
295 int currentFrame = frameStart;
308 imageToExportBase.
fill(bgColor);
310 QSize camSize = cameraLayer->getViewSize();
322 int frameWindow =
static_cast<int>(1e9 / (camSize.
width() * camSize.
height() * 4.0));
329 QStringList args = {
"-f",
"rawvideo",
"-pixel_format",
"bgra"};
339 args <<
"-i" << tempAudioPath;
344 args <<
"-plays" << (loop ?
"0" :
"1");
349 args <<
"-pix_fmt" <<
"yuv420p";
354 args <<
"-q:v" <<
"5";
358 args << strOutputFile;
364 if(framesProcessed < 0)
369 if(currentFrame > frameEnd)
375 if((currentFrame - frameStart <= framesProcessed + frameWindow || failCounter > 10) && currentFrame <= frameEnd)
377 QImage imageToExport = imageToExportBase.
copy();
380 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
384 obj->paintImage(painter, currentFrame,
false,
true);
387#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
389 Q_ASSERT(bytesWritten == imageToExport.
sizeInBytes());
391 int bytesWritten = ffmpeg.
write(
reinterpret_cast<const char*
>(imageToExport.
constBits()), imageToExport.
byteCount());
392 Q_ASSERT(bytesWritten == imageToExport.
byteCount());
402 STATUS_CHECK(status);
423 std::function<
void(
float)> progress)
428 return Status::CANCELED;
433 int frameStart = mDesc.startFrame;
434 int frameEnd = mDesc.endFrame;
435 const QSize exportSize = mDesc.exportSize;
436 bool transparency =
false;
437 QString strCameraName = mDesc.strCameraName;
438 bool loop = mDesc.loop;
441 auto cameraLayer =
static_cast<LayerCamera*
>(obj->findLayerByName(strCameraName, Layer::CAMERA));
442 if (cameraLayer ==
nullptr)
444 cameraLayer = obj->getLayersByType<
LayerCamera >().front();
446 int currentFrame = frameStart;
459 imageToExportBase.
fill(bgColor);
461 QSize camSize = cameraLayer->getViewSize();
467 QStringList args = {
"-f",
"rawvideo",
"-pixel_format",
"bgra"};
475 args <<
"-filter_complex" <<
"[0:v]palettegen [p]; [0:v][p] paletteuse";
477 args <<
"-loop" << (loop ?
"0" :
"-1");
492 Q_UNUSED(framesProcessed);
493 if(currentFrame > frameEnd)
499 QImage imageToExport = imageToExportBase.
copy();
502 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
506 obj->paintImage(painter, currentFrame,
false,
true);
508#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
510 Q_ASSERT(bytesWritten == imageToExport.
sizeInBytes());
512 bytesWritten = ffmpeg.
write(
reinterpret_cast<const char*
>(imageToExport.
constBits()), imageToExport.
byteCount());
513 Q_ASSERT(bytesWritten == imageToExport.
byteCount());
520 STATUS_CHECK(status);
549 ffmpeg.
start(cmd, args);
551 Status status = Status::OK;
553 dd << QStringLiteral(
"Command: %1 %2").arg(cmd).arg(args.
join(
' '));
564 qDebug() <<
"[ffmpeg]" << s;
572 bool shouldContinue = progress(frame.
toInt());
579 return Status::CANCELED;
588 qDebug() <<
"[ffmpeg]" << s;
594 status = Status::FAIL;
595 status.setTitle(tr(
"Something went wrong"));
596 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."));
599 status.setDetails(dd);
605 qDebug() <<
"ERROR: Could not execute FFmpeg.";
606 status = Status::FAIL;
607 status.setTitle(tr(
"Something went wrong"));
608 status.setDescription(tr(
"Couldn't start the video backend, please try again."));
609 status.setDetails(dd);
665 ffmpeg.
start(cmd, args);
667 Status status = Status::OK;
669 dd << QStringLiteral(
"Command: %1 %2").arg(cmd).arg(args.
join(
' '));
672 int framesGenerated = 0;
673 int lastFrameProcessed = 0;
674 const int frameStart = mDesc.startFrame;
675 const int frameEnd = mDesc.endFrame;
682 return Status::CANCELED;
687 int framesProcessed = -1;
694 qDebug() <<
"[ffmpeg]" << s;
699 lastFrameProcessed = framesProcessed = output.
mid(6, output.
indexOf(
' ')).
toInt();
708 while(writeFrame(ffmpeg, framesProcessed))
712 const float percentGenerated = framesGenerated /
static_cast<float>(frameEnd - frameStart);
713 const float percentConverted = lastFrameProcessed /
static_cast<float>(frameEnd - frameStart);
714 progress((percentGenerated + percentConverted) / 2);
716 const float percentGenerated = framesGenerated /
static_cast<float>(frameEnd - frameStart);
717 const float percentConverted = lastFrameProcessed /
static_cast<float>(frameEnd - frameStart);
718 progress((percentGenerated + percentConverted) / 2);
725 qDebug() <<
"[ffmpeg]" << s;
731 status = Status::FAIL;
732 status.setTitle(tr(
"Something went wrong"));
733 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."));
736 status.setDetails(dd);
742 qDebug() <<
"ERROR: Could not execute FFmpeg.";
743 status = Status::FAIL;
744 status.setTitle(tr(
"Something went wrong"));
745 status.setDescription(tr(
"Couldn't start the video backend, please try again."));
746 status.setDetails(dd);
755 b &= (!desc.strFileName.
isEmpty());
756 b &= (desc.startFrame > 0);
757 b &= (desc.endFrame >= desc.startFrame);
759 b &= (!desc.strCameraName.
isEmpty());
761 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
bool exists() const const
int byteCount() const const
const uchar * constBits() const const
QImage copy(const QRect &rectangle) const const
void fill(uint pixelValue)
qsizetype 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
bool isValid() const const
QString path() const const