1 /*
2 
3 Pencil2D - Traditional Animation Software
4 Copyright (C) 2012-2020 Matthew Chiawen Chang
5 
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; version 2 of the License.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 
15 */
16 
17 #include "movieexporter.h"
18 
19 #include <ctime>
20 #include <vector>
21 #include <cstdint>
22 #include <QDir>
23 #include <QDebug>
24 #include <QProcess>
25 #include <QApplication>
26 #include <QStandardPaths>
27 #include <QThread>
28 #include <QtMath>
29 #include <QPainter>
30 
31 #include "object.h"
32 #include "layercamera.h"
33 #include "layersound.h"
34 #include "soundclip.h"
35 #include "util.h"
36 
MovieExporter()37 MovieExporter::MovieExporter()
38 {
39 }
40 
~MovieExporter()41 MovieExporter::~MovieExporter()
42 {
43 }
44 
45 /** Begin exporting the movie described by exportDesc.
46  *
47  * @param[in] obj An Object containing the animation to export.
48  * @param[in] desc A structure containing all the export parameters.
49  *            See ExportMovieDesc.
50  * @param[out] majorProgress A function to update the major progress bar.
51  *                          The major progress bar goes from 0-100% only
52  *                          one time, representing the overall progress of
53  *                          the export. The first float parameter is
54  *                          the current progress %, and the second is
55  *                          the desired progress when the next sub-task
56  *                          completes. This should only be called at the
57  *                          beginning of a subtask.
58  * @param[out] minorProgress A function to update the minor progress bar.
59  *                           The minor progress bar goes from 0-100% for
60  *                           each sub-task of the exporting process.
61  *                           It is up to minor progress to update the
62  *                           major progress bar to reflect the sub-task
63  *                           progress.
64  * @param[out] progressMessage A function ot update the progres bar
65  *                             message. The messages will describe
66  *                             the current sub-task of the exporting
67  *                             process.
68  *
69  * @return Returns Status:OK on success, or Status::FAIL on error.
70  */
run(const Object * obj,const ExportMovieDesc & desc,std::function<void (float,float)> majorProgress,std::function<void (float)> minorProgress,std::function<void (QString)> progressMessage)71 Status MovieExporter::run(const Object* obj,
72                           const ExportMovieDesc& desc,
73                           std::function<void(float, float)> majorProgress,
74                           std::function<void(float)> minorProgress,
75                           std::function<void(QString)> progressMessage)
76 {
77     majorProgress(0.f, 0.03f);
78     minorProgress(0.f);
79     progressMessage(QObject::tr("Checking environment..."));
80 
81     clock_t t1 = clock();
82 
83     QString ffmpegPath = ffmpegLocation();
84     qDebug() << ffmpegPath;
85     if (!QFile::exists(ffmpegPath))
86     {
87 #ifdef _WIN32
88         qCritical() << "Please place ffmpeg.exe in " << ffmpegPath << " directory";
89 #else
90         qCritical() << "Please place ffmpeg in " << ffmpegPath << " directory";
91 #endif
92         return Status::ERROR_FFMPEG_NOT_FOUND;
93     }
94 
95     STATUS_CHECK(checkInputParameters(desc))
96     mDesc = desc;
97 
98     qDebug() << "OutFile: " << mDesc.strFileName;
99 
100     // Setup temporary folder
101     if (!mTempDir.isValid())
102     {
103         Q_ASSERT(false && "Cannot create temp folder.");
104         return Status::FAIL;
105     }
106 
107     mTempWorkDir = mTempDir.path();
108 
109     minorProgress(0.f);
110     if (desc.strFileName.endsWith("gif", Qt::CaseInsensitive))
111     {
112         majorProgress(0.03f, 1.f);
113         progressMessage(QObject::tr("Generating GIF..."));
114         minorProgress(0.f);
115         STATUS_CHECK(generateGif(obj, ffmpegPath, desc.strFileName, minorProgress))
116     }
117     else
118     {
119         majorProgress(0.03f, 0.25f);
120         progressMessage(QObject::tr("Assembling audio..."));
121         minorProgress(0.f);
122         STATUS_CHECK(assembleAudio(obj, ffmpegPath, minorProgress))
123         minorProgress(1.f);
124         majorProgress(0.25f, 1.f);
125         progressMessage(QObject::tr("Generating movie..."));
126         STATUS_CHECK(generateMovie(obj, ffmpegPath, desc.strFileName, minorProgress))
127     }
128     minorProgress(1.f);
129     majorProgress(1.f, 1.f);
130     progressMessage(QObject::tr("Done"));
131 
132     clock_t t2 = clock() - t1;
133     qDebug("MOVIE = %.1f sec", static_cast<double>(t2 / CLOCKS_PER_SEC));
134 
135     return Status::OK;
136 }
137 
error()138 QString MovieExporter::error()
139 {
140     return QString();
141 }
142 
143 /** Combines all audio tracks in obj into a single file.
144  *
145  *  @param[in] obj
146  *  @param[in] ffmpegPath
147  *  @param[out] progress A function that takes one float argument
148  *              (the percentage of the audio assembly complete) and
149  *              may display the output to the user in any way it
150  *              sees fit.
151  *
152  *  @return Returns the final status of the operation. Ok if successful,
153  *          or safe if there was intentionally no output.
154  */
assembleAudio(const Object * obj,QString ffmpegPath,std::function<void (float)> progress)155 Status MovieExporter::assembleAudio(const Object* obj,
156                                     QString ffmpegPath,
157                                     std::function<void(float)> progress)
158 {
159     // Quicktime assemble call
160     const int startFrame = mDesc.startFrame;
161     const int endFrame = mDesc.endFrame;
162     const int fps = mDesc.fps;
163 
164     Q_ASSERT(startFrame >= 0);
165     Q_ASSERT(endFrame >= startFrame);
166 
167     QDir dir(mTempWorkDir);
168     Q_ASSERT(dir.exists());
169 
170     QString tempAudioPath = QDir(mTempWorkDir).filePath("tmpaudio.wav");
171     qDebug() << "TempAudio=" << tempAudioPath;
172 
173     std::vector< SoundClip* > allSoundClips;
174 
175     std::vector< LayerSound* > allSoundLayers = obj->getLayersByType<LayerSound>();
176     for (LayerSound* layer : allSoundLayers)
177     {
178         layer->foreachKeyFrame([&allSoundClips](KeyFrame* key)
179         {
180             if (!key->fileName().isEmpty())
181             {
182                 allSoundClips.push_back(static_cast<SoundClip*>(key));
183             }
184         });
185     }
186 
187     if (allSoundClips.empty()) return Status::SAFE;
188 
189     int clipCount = 0;
190 
191     QString filterComplex, amergeInput, panChannelLayout;
192     QStringList args;
193 
194     int wholeLen = qCeil((endFrame - startFrame) * 44100.0 / fps);
195     for (auto clip : allSoundClips)
196     {
197         if (mCanceled)
198         {
199             return Status::CANCELED;
200         }
201 
202         // Add sound file as input
203         args << "-i" << clip->fileName();
204 
205         // Offset the sound to its correct position
206         // See https://superuser.com/questions/716320/ffmpeg-placing-audio-at-specific-location
207         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];")
208                     .arg(clipCount).arg(qRound(44100.0 * (clip->pos() - 1) / fps)).arg(wholeLen);
209         amergeInput += QString("[ad%1]").arg(clipCount);
210         panChannelLayout += QString("c%1+").arg(clipCount);
211 
212         clipCount++;
213     }
214     // Remove final '+'
215     panChannelLayout.chop(1);
216     // Output arguments
217     // Mix audio
218     args << "-filter_complex" << QString("%1%2 amerge=inputs=%3, pan=mono|c0=%4 [out]")
219             .arg(filterComplex).arg(amergeInput).arg(clipCount).arg(panChannelLayout);
220     // Convert audio file: 44100Hz sampling rate, stereo, signed 16 bit little endian
221     // Supported audio file types: wav, mp3, ogg... ( all file types supported by ffmpeg )
222     args << "-ar" << "44100" << "-acodec" << "pcm_s16le" << "-ac" << "2" << "-map" << "[out]" << "-y";
223     // Trim audio
224     args << "-ss" << QString::number((startFrame - 1) / static_cast<double>(fps));
225     args << "-to" << QString::number(endFrame / static_cast<double>(fps));
226     // Output path
227     args << tempAudioPath;
228 
229     STATUS_CHECK(MovieExporter::executeFFmpeg(ffmpegPath, args, [&progress, this] (int frame) { progress(frame / static_cast<float>(mDesc.endFrame - mDesc.startFrame)); return !mCanceled; }))
230     qDebug() << "audio file: " + tempAudioPath;
231 
232     return Status::OK;
233 }
234 
235 /** Exports obj to a movie image at strOut using FFmpeg.
236  *
237  *  @param[in]  obj An Object containing the animation to export.
238  *  @param[in]  ffmpegPath The path to the FFmpeg binary.
239  *  @param[in]  strOutputFile The output path. Should end with .gif.
240  *  @param[out] progress A function that takes one float argument
241  *              (the percentage of the gif generation complete) and
242  *              may display the output to the user in any way it
243  *              sees fit.
244  *
245  *  The movie formats supported by this operation are any file
246  *  formats that the referenced FFmpeg binary supports and that have
247  *  the required features (ex. video and audio support)
248  *
249  *  @return Returns the final status of the operation (ok or canceled)
250  */
generateMovie(const Object * obj,QString ffmpegPath,QString strOutputFile,std::function<void (float)> progress)251 Status MovieExporter::generateMovie(
252         const Object* obj,
253         QString ffmpegPath,
254         QString strOutputFile,
255         std::function<void(float)> progress)
256 {
257     if (mCanceled)
258     {
259         return Status::CANCELED;
260     }
261 
262     // Frame generation setup
263 
264     int frameStart = mDesc.startFrame;
265     int frameEnd = mDesc.endFrame;
266     const QSize exportSize = mDesc.exportSize;
267     bool transparency = mDesc.alpha;
268     QString strCameraName = mDesc.strCameraName;
269     bool loop = mDesc.loop;
270 
271     auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
272     if (cameraLayer == nullptr)
273     {
274         cameraLayer = obj->getLayersByType< LayerCamera >().front();
275     }
276     int currentFrame = frameStart;
277 
278     /* We create an image with the correct dimensions and background
279      * color here and then copy this and draw over top of it to
280      * generate each frame. This is faster than having to generate
281      * a new background image for each frame.
282      */
283     QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
284     QColor bgColor = Qt::white;
285     if (transparency)
286     {
287         bgColor.setAlpha(0);
288     }
289     imageToExportBase.fill(bgColor);
290 
291     QSize camSize = cameraLayer->getViewSize();
292     QTransform centralizeCamera;
293     centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
294 
295     int failCounter = 0;
296     /* Movie export uses a "sliding window" to reduce memory usage
297      * while having a relatively small impact on speed. This basically
298      * means that there is a maximum number of frames that can be waiting
299      * to be encoded by ffmpeg at any one time. The limit is set by the
300      * frameWindow variable which is designed to take up a maximum of
301      * about 1GB of memory
302      */
303     int frameWindow = static_cast<int>(1e9 / (camSize.width() * camSize.height() * 4.0));
304 
305     // Build FFmpeg command
306 
307     //int exportFps = mDesc.videoFps;
308     const QString tempAudioPath = QDir(mTempWorkDir).filePath("tmpaudio.wav");
309 
310     QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
311     args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
312     args << "-framerate" << QString::number(mDesc.fps);
313 
314     //args << "-r" << QString::number(exportFps);
315     args << "-i" << "-";
316     args << "-threads" << (QThread::idealThreadCount() == 1 ? "0" : QString::number(QThread::idealThreadCount()));
317 
318     if (QFile::exists(tempAudioPath))
319     {
320         args << "-i" << tempAudioPath;
321     }
322 
323     if (strOutputFile.endsWith(".apng", Qt::CaseInsensitive))
324     {
325         args << "-plays" << (loop ? "0" : "1");
326     }
327 
328     if (strOutputFile.endsWith("mp4", Qt::CaseInsensitive))
329     {
330         args << "-pix_fmt" << "yuv420p";
331     }
332 
333     if (strOutputFile.endsWith(".avi", Qt::CaseInsensitive))
334     {
335         args << "-q:v" << "5";
336     }
337 
338     args << "-y";
339     args << strOutputFile;
340 
341     // Run FFmpeg command
342 
343     STATUS_CHECK(executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
344     {
345         if(framesProcessed < 0)
346         {
347             failCounter++;
348         }
349 
350         if(currentFrame > frameEnd)
351         {
352             ffmpeg.closeWriteChannel();
353             return false;
354         }
355 
356         if((currentFrame - frameStart <= framesProcessed + frameWindow || failCounter > 10) && currentFrame <= frameEnd)
357         {
358             QImage imageToExport = imageToExportBase.copy();
359             QPainter painter(&imageToExport);
360 
361             QTransform view = cameraLayer->getViewAtFrame(currentFrame);
362             painter.setWorldTransform(view * centralizeCamera);
363             painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
364 
365             obj->paintImage(painter, currentFrame, false, true);
366             painter.end();
367 
368             // Should use sizeInBytes instead of byteCount to support large images,
369             // but this is only supported in QT 5.10+
370             int bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
371             Q_ASSERT(bytesWritten == imageToExport.byteCount());
372 
373             currentFrame++;
374             failCounter = 0;
375             return true;
376         }
377 
378         return false;
379     }));
380 
381     return Status::OK;
382 }
383 
384 /** Exports obj to a gif image at strOut using FFmpeg.
385  *
386  *  @param[in]  obj An Object containing the animation to export.
387  *  @param[in]  ffmpegPath The path to the FFmpeg binary.
388  *  @param[in]  strOut The output path. Should end with .gif.
389  *  @param[out] progress A function that takes one float argument
390  *              (the percentage of the gif generation complete) and
391  *              may display the output to the user in any way it
392  *              sees fit.
393  *
394  *  @return Returns the final status of the operation (ok or canceled)
395  */
generateGif(const Object * obj,QString ffmpegPath,QString strOut,std::function<void (float)> progress)396 Status MovieExporter::generateGif(
397         const Object* obj,
398         QString ffmpegPath,
399         QString strOut,
400         std::function<void(float)> progress)
401 {
402 
403     if (mCanceled)
404     {
405         return Status::CANCELED;
406     }
407 
408     // Frame generation setup
409 
410     int frameStart = mDesc.startFrame;
411     int frameEnd = mDesc.endFrame;
412     const QSize exportSize = mDesc.exportSize;
413     bool transparency = false;
414     QString strCameraName = mDesc.strCameraName;
415     bool loop = mDesc.loop;
416     int bytesWritten;
417 
418     auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
419     if (cameraLayer == nullptr)
420     {
421         cameraLayer = obj->getLayersByType< LayerCamera >().front();
422     }
423     int currentFrame = frameStart;
424 
425     /* We create an image with the correct dimensions and background
426      * color here and then copy this and draw over top of it to
427      * generate each frame. This is faster than having to generate
428      * a new background image for each frame.
429      */
430     QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
431     QColor bgColor = Qt::white;
432     if (transparency)
433     {
434         bgColor.setAlpha(0);
435     }
436     imageToExportBase.fill(bgColor);
437 
438     QSize camSize = cameraLayer->getViewSize();
439     QTransform centralizeCamera;
440     centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
441 
442     // Build FFmpeg command
443 
444     QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
445     args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
446     args << "-framerate" << QString::number(mDesc.fps);
447 
448     args << "-i" << "-";
449 
450     args << "-y";
451 
452     args << "-filter_complex" << "[0:v]palettegen [p]; [0:v][p] paletteuse";
453 
454     args << "-loop" << (loop ? "0" : "-1");
455     args << strOut;
456 
457     // Run FFmpeg command
458 
459     STATUS_CHECK(executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
460     {
461         /* The GIF FFmpeg command requires the entires stream to be
462          * written before FFmpeg can encode the GIF. This is because
463          * the generated pallete is based off of the colors in all
464          * frames. The only way to avoid this would be to generate
465          * all the frames twice and run two separate commands, which
466          * would likely have unacceptable speed costs.
467          */
468 
469         Q_UNUSED(framesProcessed);
470         if(currentFrame > frameEnd)
471         {
472             ffmpeg.closeWriteChannel();
473             return false;
474         }
475 
476         QImage imageToExport = imageToExportBase.copy();
477         QPainter painter(&imageToExport);
478 
479         QTransform view = cameraLayer->getViewAtFrame(currentFrame);
480         painter.setWorldTransform(view * centralizeCamera);
481         painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
482 
483         obj->paintImage(painter, currentFrame, false, true);
484 
485         bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
486         Q_ASSERT(bytesWritten == imageToExport.byteCount());
487 
488         currentFrame++;
489 
490         return true;
491     }));
492 
493     return Status::OK;
494 }
495 
496 /** Runs the specified command (should be ffmpeg) and allows for progress feedback.
497  *
498  *  @param[in]  cmd A string containing the command to execute
499  *  @param[in]  args A string list containing the arguments to
500  *              pass to the command
501  *  @param[out] progress A function that takes one float argument
502  *              (the percentage of the ffmpeg operation complete) and
503  *              may display the output to the user in any way it
504  *              sees fit.
505  *
506  *  executeFFMpeg does not allow for writing direct input, the only
507  *  input through the "-i" argument to specify input files on the disk.
508  *
509  *  @return Returns Status::OK if everything went well, and Status::FAIL
510  *  and error is detected (usually a non-zero exit code for ffmpeg).
511  */
executeFFmpeg(const QString & cmd,const QStringList & args,std::function<bool (int)> progress)512 Status MovieExporter::executeFFmpeg(const QString& cmd, const QStringList& args, std::function<bool(int)> progress)
513 {
514     qDebug() << cmd;
515 
516     QProcess ffmpeg;
517     ffmpeg.setReadChannel(QProcess::StandardOutput);
518     // FFmpeg writes to stderr only for some reason, so we just read both channels together
519     ffmpeg.setProcessChannelMode(QProcess::MergedChannels);
520     ffmpeg.start(cmd, args);
521 
522     Status status = Status::OK;
523     DebugDetails dd;
524     dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
525     if (ffmpeg.waitForStarted())
526     {
527         while(ffmpeg.state() == QProcess::Running)
528         {
529             if(!ffmpeg.waitForReadyRead()) break;
530 
531             QString output(ffmpeg.readAll());
532             QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
533             for (const QString& s : sList)
534             {
535                 qDebug() << "[ffmpeg]" << s;
536                 dd << s;
537             }
538 
539             if(output.startsWith("frame="))
540             {
541                 QString frame = output.mid(6, output.indexOf(' '));
542 
543                 bool shouldContinue = progress(frame.toInt());
544                 if (!shouldContinue)
545                 {
546                     ffmpeg.terminate();
547                     ffmpeg.waitForFinished(3000);
548                     if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
549                     ffmpeg.waitForFinished();
550                     return Status::CANCELED;
551                 }
552             }
553         }
554 
555         QString output(ffmpeg.readAll());
556         QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
557         for (const QString& s : sList)
558         {
559             qDebug() << "[ffmpeg]" << s;
560             dd << s;
561         }
562 
563         if(ffmpeg.exitStatus() != QProcess::NormalExit || ffmpeg.exitCode() != 0)
564         {
565             status = Status::FAIL;
566             status.setTitle(QObject::tr("Something went wrong"));
567             status.setDescription(QObject::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."));
568             dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
569                << QString("Exit code: %1").arg(ffmpeg.exitCode());
570             status.setDetails(dd);
571             return status;
572         }
573     }
574     else
575     {
576         qDebug() << "ERROR: Could not execute FFmpeg.";
577         status = Status::FAIL;
578         status.setTitle(QObject::tr("Something went wrong"));
579         status.setDescription(QObject::tr("Couldn't start the video backend, please try again."));
580         status.setDetails(dd);
581     }
582     return status;
583 }
584 
585 /** Runs the specified command (should be ffmpeg), and lets
586  *  writeFrame pipe data into it 1 frame at a time.
587  *
588  *  @param[in]  cmd A string containing the command to execute
589  *  @param[in]  args A string list containing the arguments to
590  *              pass to the command
591  *  @param[out] progress A function that takes one float argument
592  *              (the percentage of the ffmpeg operation complete) and
593  *              may display the output to the user in any way it
594  *              sees fit.
595  *  @param[in]  writeFrame A function that takes two arguments, a
596  *              process (the ffmpeg process) and an integer
597  *              (frames processed or -1, see full description).
598  *              This function should write a single frame to the
599  *              process. The function returns true value if it
600  *              actually wrote a frame.
601  *
602  *  This function operates generally as follows:
603  *  1. Spawn process with the command from cmd
604  *  2. Check ffmpeg's output for a progress update.
605  *  3. Add frames with writeFrame until it returns false.
606  *  4. Repeat from step 2 until all frames have been written.
607  *
608  *  The idea is that there are two forms of processing occuring
609  *  simultaneously, generating frames to send to ffmpeg, and ffmpeg
610  *  encoding those frames. Whether these this actually occur
611  *  concurrently or one after another appears to depend on the environment.
612  *
613  *  The writeFrame function deserves a bit of extra details. It does
614  *  not only return false when there is an error in generating or
615  *  writing a frame, it also does it when it wants to "return control"
616  *  to the rest of the executeFFMpegPipe function for the purposes of
617  *  reading updates from ffmpeg's output. This should be done every
618  *  once in a while if possible, but with some formats (specifically gif),
619  *  all frames must be loaded before any processing can continue, so
620  *  there is no point returning false for it until all frames have
621  *  been written. writeFrame is also responsible for closing the writeChannel
622  *  of the process when it has finished writing all frames. This indicates
623  *  to executeFFMpegPipe that it no longer needs to call writeFrame.
624  *
625  *  @return Returns Status::OK if everything went well, and Status::FAIL
626  *  and error is detected (usually a non-zero exit code for ffmpeg).
627  */
executeFFMpegPipe(const QString & cmd,const QStringList & args,std::function<void (float)> progress,std::function<bool (QProcess &,int)> writeFrame)628 Status MovieExporter::executeFFMpegPipe(const QString& cmd, const QStringList& args, std::function<void(float)> progress, std::function<bool(QProcess&, int)> writeFrame)
629 {
630     qDebug() << cmd;
631 
632     QProcess ffmpeg;
633     ffmpeg.setReadChannel(QProcess::StandardOutput);
634     // FFmpeg writes to stderr only for some reason, so we just read both channels together
635     ffmpeg.setProcessChannelMode(QProcess::MergedChannels);
636     ffmpeg.start(cmd, args);
637 
638     Status status = Status::OK;
639     DebugDetails dd;
640     dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
641     if (ffmpeg.waitForStarted())
642     {
643         int framesGenerated = 0;
644         int lastFrameProcessed = 0;
645         const int frameStart = mDesc.startFrame;
646         const int frameEnd = mDesc.endFrame;
647         while(ffmpeg.state() == QProcess::Running)
648         {
649             if (mCanceled)
650             {
651                 ffmpeg.terminate();
652                 if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
653                 return Status::CANCELED;
654             }
655 
656             // Check FFmpeg progress
657 
658             int framesProcessed = -1;
659             if(ffmpeg.waitForReadyRead(10))
660             {
661                 QString output(ffmpeg.readAll());
662                 QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
663                 for (const QString& s : sList)
664                 {
665                     qDebug() << "[ffmpeg]" << s;
666                     dd << s;
667                 }
668                 if(output.startsWith("frame="))
669                 {
670                     lastFrameProcessed = framesProcessed = output.mid(6, output.indexOf(' ')).toInt();
671                 }
672             }
673 
674             if(!ffmpeg.isWritable())
675             {
676                 continue;
677             }
678 
679             while(writeFrame(ffmpeg, framesProcessed))
680             {
681                 framesGenerated++;
682 
683                 const float percentGenerated = framesGenerated / static_cast<float>(frameEnd - frameStart);
684                 const float percentConverted = lastFrameProcessed / static_cast<float>(frameEnd - frameStart);
685                 progress((percentGenerated + percentConverted) / 2);
686             }
687             const float percentGenerated = framesGenerated / static_cast<float>(frameEnd - frameStart);
688             const float percentConverted = lastFrameProcessed / static_cast<float>(frameEnd - frameStart);
689             progress((percentGenerated + percentConverted) / 2);
690         }
691 
692         QString output(ffmpeg.readAll());
693         QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
694         for (const QString& s : sList)
695         {
696             qDebug() << "[ffmpeg]" << s;
697             dd << s;
698         }
699 
700         if(ffmpeg.exitStatus() != QProcess::NormalExit  || ffmpeg.exitCode() != 0)
701         {
702             status = Status::FAIL;
703             status.setTitle(QObject::tr("Something went wrong"));
704             status.setDescription(QObject::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."));
705             dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
706                << QString("Exit code: %1").arg(ffmpeg.exitCode());
707             status.setDetails(dd);
708             return status;
709         }
710     }
711     else
712     {
713         qDebug() << "ERROR: Could not execute FFmpeg.";
714         status = Status::FAIL;
715         status.setTitle(QObject::tr("Something went wrong"));
716         status.setDescription(QObject::tr("Couldn't start the video backend, please try again."));
717         status.setDetails(dd);
718     }
719 
720     return status;
721 }
722 
checkInputParameters(const ExportMovieDesc & desc)723 Status MovieExporter::checkInputParameters(const ExportMovieDesc& desc)
724 {
725     bool b = true;
726     b &= (!desc.strFileName.isEmpty());
727     b &= (desc.startFrame > 0);
728     b &= (desc.endFrame >= desc.startFrame);
729     b &= (desc.fps > 0);
730     b &= (!desc.strCameraName.isEmpty());
731 
732     return b ? Status::OK : Status::INVALID_ARGUMENT;
733 }
734