All Classes Namespaces Functions Variables Enumerations Properties Pages
movieexporter.cpp
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 
37 MovieExporter::MovieExporter()
38 {
39 }
40 
41 MovieExporter::~MovieExporter()
42 {
43 }
44 
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 
138 QString MovieExporter::error()
139 {
140  return QString();
141 }
142 
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  allSoundClips.push_back(static_cast<SoundClip*>(key));
181  });
182  }
183 
184  if (allSoundClips.empty()) return Status::SAFE;
185 
186  int clipCount = 0;
187 
188  QString filterComplex, amergeInput, panChannelLayout;
189  QStringList args;
190 
191  int wholeLen = qCeil((endFrame - startFrame) * 44100.0 / fps);
192  for (auto clip : allSoundClips)
193  {
194  if (mCanceled)
195  {
196  return Status::CANCELED;
197  }
198 
199  // Add sound file as input
200  args << "-i" << clip->fileName();
201 
202  // Offset the sound to its correct position
203  // See https://superuser.com/questions/716320/ffmpeg-placing-audio-at-specific-location
204  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];")
205  .arg(clipCount).arg(qRound(44100.0 * (clip->pos() - 1) / fps)).arg(wholeLen);
206  amergeInput += QString("[ad%1]").arg(clipCount);
207  panChannelLayout += QString("c%1+").arg(clipCount);
208 
209  clipCount++;
210  }
211  // Remove final '+'
212  panChannelLayout.chop(1);
213  // Output arguments
214  // Mix audio
215  args << "-filter_complex" << QString("%1%2 amerge=inputs=%3, pan=mono|c0=%4 [out]")
216  .arg(filterComplex).arg(amergeInput).arg(clipCount).arg(panChannelLayout);
217  // Convert audio file: 44100Hz sampling rate, stereo, signed 16 bit little endian
218  // Supported audio file types: wav, mp3, ogg... ( all file types supported by ffmpeg )
219  args << "-ar" << "44100" << "-acodec" << "pcm_s16le" << "-ac" << "2" << "-map" << "[out]" << "-y";
220  // Trim audio
221  args << "-ss" << QString::number((startFrame - 1) / static_cast<double>(fps));
222  args << "-to" << QString::number(endFrame / static_cast<double>(fps));
223  // Output path
224  args << tempAudioPath;
225 
226  STATUS_CHECK(MovieExporter::executeFFmpeg(ffmpegPath, args, [&progress, this] (int frame) { progress(frame / static_cast<float>(mDesc.endFrame - mDesc.startFrame)); return !mCanceled; }))
227  qDebug() << "audio file: " + tempAudioPath;
228 
229  return Status::OK;
230 }
231 
249  const Object* obj,
250  QString ffmpegPath,
251  QString strOutputFile,
252  std::function<void(float)> progress)
253 {
254  if (mCanceled)
255  {
256  return Status::CANCELED;
257  }
258 
259  // Frame generation setup
260 
261  int frameStart = mDesc.startFrame;
262  int frameEnd = mDesc.endFrame;
263  const QSize exportSize = mDesc.exportSize;
264  bool transparency = mDesc.alpha;
265  QString strCameraName = mDesc.strCameraName;
266  bool loop = mDesc.loop;
267 
268  auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
269  if (cameraLayer == nullptr)
270  {
271  cameraLayer = obj->getLayersByType< LayerCamera >().front();
272  }
273  int currentFrame = frameStart;
274 
275  /* We create an image with the correct dimensions and background
276  * color here and then copy this and draw over top of it to
277  * generate each frame. This is faster than having to generate
278  * a new background image for each frame.
279  */
280  QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
281  QColor bgColor = Qt::white;
282  if (transparency)
283  {
284  bgColor.setAlpha(0);
285  }
286  imageToExportBase.fill(bgColor);
287 
288  QSize camSize = cameraLayer->getViewSize();
289  QTransform centralizeCamera;
290  centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
291 
292  int failCounter = 0;
293  /* Movie export uses a "sliding window" to reduce memory usage
294  * while having a relatively small impact on speed. This basically
295  * means that there is a maximum number of frames that can be waiting
296  * to be encoded by ffmpeg at any one time. The limit is set by the
297  * frameWindow variable which is designed to take up a maximum of
298  * about 1GB of memory
299  */
300  int frameWindow = static_cast<int>(1e9 / (camSize.width() * camSize.height() * 4.0));
301 
302  // Build FFmpeg command
303 
304  //int exportFps = mDesc.videoFps;
305  const QString tempAudioPath = QDir(mTempWorkDir).filePath("tmpaudio.wav");
306 
307  QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
308  args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
309  args << "-framerate" << QString::number(mDesc.fps);
310 
311  //args << "-r" << QString::number(exportFps);
312  args << "-i" << "-";
313  args << "-threads" << (QThread::idealThreadCount() == 1 ? "0" : QString::number(QThread::idealThreadCount()));
314 
315  if (QFile::exists(tempAudioPath))
316  {
317  args << "-i" << tempAudioPath;
318  }
319 
320  if (strOutputFile.endsWith(".apng", Qt::CaseInsensitive))
321  {
322  args << "-plays" << (loop ? "0" : "1");
323  }
324 
325  if (strOutputFile.endsWith("mp4", Qt::CaseInsensitive))
326  {
327  args << "-pix_fmt" << "yuv420p";
328  }
329 
330  if (strOutputFile.endsWith(".avi", Qt::CaseInsensitive))
331  {
332  args << "-q:v" << "5";
333  }
334 
335  args << "-y";
336  args << strOutputFile;
337 
338  // Run FFmpeg command
339 
340  STATUS_CHECK(executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
341  {
342  if(framesProcessed < 0)
343  {
344  failCounter++;
345  }
346 
347  if(currentFrame > frameEnd)
348  {
349  ffmpeg.closeWriteChannel();
350  return false;
351  }
352 
353  if((currentFrame - frameStart <= framesProcessed + frameWindow || failCounter > 10) && currentFrame <= frameEnd)
354  {
355  QImage imageToExport = imageToExportBase.copy();
356  QPainter painter(&imageToExport);
357 
358  QTransform view = cameraLayer->getViewAtFrame(currentFrame);
359  painter.setWorldTransform(view * centralizeCamera);
360  painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
361 
362  obj->paintImage(painter, currentFrame, false, true);
363  painter.end();
364 
365  // Should use sizeInBytes instead of byteCount to support large images,
366  // but this is only supported in QT 5.10+
367  int bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
368  Q_ASSERT(bytesWritten == imageToExport.byteCount());
369 
370  currentFrame++;
371  failCounter = 0;
372  return true;
373  }
374 
375  return false;
376  }));
377 
378  return Status::OK;
379 }
380 
394  const Object* obj,
395  QString ffmpegPath,
396  QString strOut,
397  std::function<void(float)> progress)
398 {
399 
400  if (mCanceled)
401  {
402  return Status::CANCELED;
403  }
404 
405  // Frame generation setup
406 
407  int frameStart = mDesc.startFrame;
408  int frameEnd = mDesc.endFrame;
409  const QSize exportSize = mDesc.exportSize;
410  bool transparency = false;
411  QString strCameraName = mDesc.strCameraName;
412  bool loop = mDesc.loop;
413  int bytesWritten;
414 
415  auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
416  if (cameraLayer == nullptr)
417  {
418  cameraLayer = obj->getLayersByType< LayerCamera >().front();
419  }
420  int currentFrame = frameStart;
421 
422  /* We create an image with the correct dimensions and background
423  * color here and then copy this and draw over top of it to
424  * generate each frame. This is faster than having to generate
425  * a new background image for each frame.
426  */
427  QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
428  QColor bgColor = Qt::white;
429  if (transparency)
430  {
431  bgColor.setAlpha(0);
432  }
433  imageToExportBase.fill(bgColor);
434 
435  QSize camSize = cameraLayer->getViewSize();
436  QTransform centralizeCamera;
437  centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
438 
439  // Build FFmpeg command
440 
441  QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
442  args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
443  args << "-framerate" << QString::number(mDesc.fps);
444 
445  args << "-i" << "-";
446 
447  args << "-y";
448 
449  args << "-filter_complex" << "[0:v]palettegen [p]; [0:v][p] paletteuse";
450 
451  args << "-loop" << (loop ? "0" : "-1");
452  args << strOut;
453 
454  // Run FFmpeg command
455 
456  STATUS_CHECK(executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
457  {
458  /* The GIF FFmpeg command requires the entires stream to be
459  * written before FFmpeg can encode the GIF. This is because
460  * the generated pallete is based off of the colors in all
461  * frames. The only way to avoid this would be to generate
462  * all the frames twice and run two separate commands, which
463  * would likely have unacceptable speed costs.
464  */
465 
466  Q_UNUSED(framesProcessed);
467  if(currentFrame > frameEnd)
468  {
469  ffmpeg.closeWriteChannel();
470  return false;
471  }
472 
473  QImage imageToExport = imageToExportBase.copy();
474  QPainter painter(&imageToExport);
475 
476  QTransform view = cameraLayer->getViewAtFrame(currentFrame);
477  painter.setWorldTransform(view * centralizeCamera);
478  painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
479 
480  obj->paintImage(painter, currentFrame, false, true);
481 
482  bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
483  Q_ASSERT(bytesWritten == imageToExport.byteCount());
484 
485  currentFrame++;
486 
487  return true;
488  }));
489 
490  return Status::OK;
491 }
492 
509 Status MovieExporter::executeFFmpeg(const QString& cmd, const QStringList& args, std::function<bool(int)> progress)
510 {
511  qDebug() << cmd;
512 
513  QProcess ffmpeg;
515  // FFmpeg writes to stderr only for some reason, so we just read both channels together
517  ffmpeg.start(cmd, args);
518 
519  Status status = Status::OK;
520  DebugDetails dd;
521  dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
522  if (ffmpeg.waitForStarted())
523  {
524  while(ffmpeg.state() == QProcess::Running)
525  {
526  if(!ffmpeg.waitForReadyRead()) break;
527 
528  QString output(ffmpeg.readAll());
529  QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
530  for (const QString& s : sList)
531  {
532  qDebug() << "[ffmpeg]" << s;
533  dd << s;
534  }
535 
536  if(output.startsWith("frame="))
537  {
538  QString frame = output.mid(6, output.indexOf(' '));
539 
540  bool shouldContinue = progress(frame.toInt());
541  if (!shouldContinue)
542  {
543  ffmpeg.terminate();
544  ffmpeg.waitForFinished(3000);
545  if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
546  ffmpeg.waitForFinished();
547  return Status::CANCELED;
548  }
549  }
550  }
551 
552  QString output(ffmpeg.readAll());
553  QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
554  for (const QString& s : sList)
555  {
556  qDebug() << "[ffmpeg]" << s;
557  dd << s;
558  }
559 
560  if(ffmpeg.exitStatus() != QProcess::NormalExit || ffmpeg.exitCode() != 0)
561  {
562  status = Status::FAIL;
563  status.setTitle(QObject::tr("Something went wrong"));
564  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."));
565  dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
566  << QString("Exit code: %1").arg(ffmpeg.exitCode());
567  status.setDetails(dd);
568  return status;
569  }
570  }
571  else
572  {
573  qDebug() << "ERROR: Could not execute FFmpeg.";
574  status = Status::FAIL;
575  status.setTitle(QObject::tr("Something went wrong"));
576  status.setDescription(QObject::tr("Couldn't start the video backend, please try again."));
577  status.setDetails(dd);
578  }
579  return status;
580 }
581 
625 Status MovieExporter::executeFFMpegPipe(const QString& cmd, const QStringList& args, std::function<void(float)> progress, std::function<bool(QProcess&, int)> writeFrame)
626 {
627  qDebug() << cmd;
628 
629  QProcess ffmpeg;
631  // FFmpeg writes to stderr only for some reason, so we just read both channels together
633  ffmpeg.start(cmd, args);
634 
635  Status status = Status::OK;
636  DebugDetails dd;
637  dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
638  if (ffmpeg.waitForStarted())
639  {
640  int framesGenerated = 0;
641  int lastFrameProcessed = 0;
642  const int frameStart = mDesc.startFrame;
643  const int frameEnd = mDesc.endFrame;
644  while(ffmpeg.state() == QProcess::Running)
645  {
646  if (mCanceled)
647  {
648  ffmpeg.terminate();
649  if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
650  return Status::CANCELED;
651  }
652 
653  // Check FFmpeg progress
654 
655  int framesProcessed = -1;
656  if(ffmpeg.waitForReadyRead(10))
657  {
658  QString output(ffmpeg.readAll());
659  QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
660  for (const QString& s : sList)
661  {
662  qDebug() << "[ffmpeg]" << s;
663  dd << s;
664  }
665  if(output.startsWith("frame="))
666  {
667  lastFrameProcessed = framesProcessed = output.mid(6, output.indexOf(' ')).toInt();
668  }
669  }
670 
671  if(!ffmpeg.isWritable())
672  {
673  continue;
674  }
675 
676  while(writeFrame(ffmpeg, framesProcessed))
677  {
678  framesGenerated++;
679 
680  const float percentGenerated = framesGenerated / static_cast<float>(frameEnd - frameStart);
681  const float percentConverted = lastFrameProcessed / static_cast<float>(frameEnd - frameStart);
682  progress((percentGenerated + percentConverted) / 2);
683  }
684  const float percentGenerated = framesGenerated / static_cast<float>(frameEnd - frameStart);
685  const float percentConverted = lastFrameProcessed / static_cast<float>(frameEnd - frameStart);
686  progress((percentGenerated + percentConverted) / 2);
687  }
688 
689  QString output(ffmpeg.readAll());
690  QStringList sList = output.split(QRegExp("[\r\n]"), QString::SkipEmptyParts);
691  for (const QString& s : sList)
692  {
693  qDebug() << "[ffmpeg]" << s;
694  dd << s;
695  }
696 
697  if(ffmpeg.exitStatus() != QProcess::NormalExit || ffmpeg.exitCode() != 0)
698  {
699  status = Status::FAIL;
700  status.setTitle(QObject::tr("Something went wrong"));
701  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."));
702  dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
703  << QString("Exit code: %1").arg(ffmpeg.exitCode());
704  status.setDetails(dd);
705  return status;
706  }
707  }
708  else
709  {
710  qDebug() << "ERROR: Could not execute FFmpeg.";
711  status = Status::FAIL;
712  status.setTitle(QObject::tr("Something went wrong"));
713  status.setDescription(QObject::tr("Couldn't start the video backend, please try again."));
714  status.setDetails(dd);
715  }
716 
717  return status;
718 }
719 
720 Status MovieExporter::checkInputParameters(const ExportMovieDesc& desc)
721 {
722  bool b = true;
723  b &= (!desc.strFileName.isEmpty());
724  b &= (desc.startFrame > 0);
725  b &= (desc.endFrame >= desc.startFrame);
726  b &= (desc.fps > 0);
727  b &= (!desc.strCameraName.isEmpty());
728 
729  return b ? Status::OK : Status::INVALID_ARGUMENT;
730 }
QString & append(QChar ch)
void kill()
bool isWritable() const const
int width() const const
bool end()
Format_ARGB32_Premultiplied
virtual bool waitForReadyRead(int msecs) override
QString filePath(const QString &fileName) const const
void setAlpha(int alpha)
QString join(const QString &separator) const const
bool isValid() const const
bool exists() const const
int byteCount() const const
QImage copy(const QRect &rectangle) const const
void chop(int n)
QString tr(const char *sourceText, const char *disambiguation, int n)
void terminate()
Status assembleAudio(const Object *obj, QString ffmpegPath, std::function< void(float)> progress)
Combines all audio tracks in obj into a single file.
void setWindow(const QRect &rectangle)
QTransform & translate(qreal dx, qreal dy)
QString number(int n, int base)
bool exists() const const
void fill(uint pixelValue)
void setWorldTransform(const QTransform &matrix, bool combine)
CaseInsensitive
int toInt(bool *ok, int base) const const
bool isEmpty() const const
QByteArray readAll()
const uchar * constBits() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
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.
QString path() const const
bool waitForStarted(int msecs)
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.
void setProcessChannelMode(QProcess::ProcessChannelMode mode)
int idealThreadCount()
QString mid(int position, int n) const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int height() const const
qint64 write(const char *data, qint64 maxSize)
Definition: object.h:54
Status generateGif(const Object *obj, QString ffmpeg, QString strOut, std::function< void(float)> progress)
Exports obj to a gif image at strOut using FFmpeg.
void setReadChannel(QProcess::ProcessChannel channel)
void closeWriteChannel()
QProcess::ExitStatus exitStatus() const const
int exitCode() const const
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
QProcess::ProcessState state() const const
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 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...
bool waitForFinished(int msecs)