Pencil2D Animation
Download Community News Docs Contribute
  • Overview
  • Articles
  • Code
  •  
  • Class List
  • Class Index
  • Class Hierarchy
  • Class Members
  • File List
Loading...
Searching...
No Matches
  • core_lib
  • src
movieexporter.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2012-2020 Matthew Chiawen Chang
5
6This program is free software; you can redistribute it and/or
7modify it under the terms of the GNU General Public License
8as published by the Free Software Foundation; version 2 of the License.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU 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#include <QRegularExpression>
31
32#include "object.h"
33#include "layercamera.h"
34#include "layersound.h"
35#include "soundclip.h"
36#include "util.h"
37
38#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
39using Qt::SplitBehaviorFlags;
40#else
41using SplitBehaviorFlags = QString::SplitBehavior;
42#endif
43
44MovieExporter::MovieExporter()
45{
46}
47
48MovieExporter::~MovieExporter()
49{
50}
51
78Status MovieExporter::run(const Object* obj,
79 const ExportMovieDesc& desc,
80 std::function<void(float, float)> majorProgress,
81 std::function<void(float)> minorProgress,
82 std::function<void(QString)> progressMessage)
83{
84 majorProgress(0.f, 0.03f);
85 minorProgress(0.f);
86 progressMessage(tr("Checking environment..."));
87
88 clock_t t1 = clock();
89
90 QString ffmpegPath = ffmpegLocation();
91 qDebug() << ffmpegPath;
92 if (!QFile::exists(ffmpegPath))
93 {
94#ifdef _WIN32
95 qCritical() << "Please place ffmpeg.exe in " << ffmpegPath << " directory";
96#else
97 qCritical() << "Please place ffmpeg in " << ffmpegPath << " directory";
98#endif
99 return Status::ERROR_FFMPEG_NOT_FOUND;
100 }
101
102 STATUS_CHECK(checkInputParameters(desc))
103 mDesc = desc;
104
105 qDebug() << "OutFile: " << mDesc.strFileName;
106
107 // Setup temporary folder
108 if (!mTempDir.isValid())
109 {
110 Q_ASSERT(false && "Cannot create temp folder.");
111 return Status::FAIL;
112 }
113
114 mTempWorkDir = mTempDir.path();
115
116 minorProgress(0.f);
117 if (desc.strFileName.endsWith("gif", Qt::CaseInsensitive))
118 {
119 majorProgress(0.03f, 1.f);
120 progressMessage(tr("Generating GIF..."));
121 minorProgress(0.f);
122 STATUS_CHECK(generateGif(obj, ffmpegPath, desc.strFileName, minorProgress))
123 }
124 else
125 {
126 majorProgress(0.03f, 0.25f);
127 progressMessage(tr("Assembling audio..."));
128 minorProgress(0.f);
129 STATUS_CHECK(assembleAudio(obj, ffmpegPath, minorProgress))
130 minorProgress(1.f);
131 majorProgress(0.25f, 1.f);
132 progressMessage(tr("Generating movie..."));
133 STATUS_CHECK(generateMovie(obj, ffmpegPath, desc.strFileName, minorProgress))
134 }
135 minorProgress(1.f);
136 majorProgress(1.f, 1.f);
137 progressMessage(tr("Done"));
138
139 clock_t t2 = clock() - t1;
140 qDebug("MOVIE = %.1f sec", static_cast<double>(t2 / CLOCKS_PER_SEC));
141
142 return Status::OK;
143}
144
145QString MovieExporter::error()
146{
147 return QString();
148}
149
162Status MovieExporter::assembleAudio(const Object* obj,
163 QString ffmpegPath,
164 std::function<void(float)> progress)
165{
166 // Quicktime assemble call
167 const int startFrame = mDesc.startFrame;
168 const int endFrame = mDesc.endFrame;
169 const int fps = mDesc.fps;
170
171 Q_ASSERT(startFrame >= 0);
172 Q_ASSERT(endFrame >= startFrame);
173
174 QDir dir(mTempWorkDir);
175 Q_ASSERT(dir.exists());
176
177 QString tempAudioPath = QDir(mTempWorkDir).filePath("tmpaudio.wav");
178 qDebug() << "TempAudio=" << tempAudioPath;
179
180 std::vector< SoundClip* > allSoundClips;
181
182 std::vector< LayerSound* > allSoundLayers = obj->getLayersByType<LayerSound>();
183 for (LayerSound* layer : allSoundLayers)
184 {
185 if (!layer->visible()) { continue; }
186 layer->foreachKeyFrame([&allSoundClips](KeyFrame* key)
187 {
188 if (!key->fileName().isEmpty())
189 {
190 allSoundClips.push_back(static_cast<SoundClip*>(key));
191 }
192 });
193 }
194
195 if (allSoundClips.empty()) return Status::SAFE;
196
197 int clipCount = 0;
198
199 QString filterComplex, amergeInput, panChannelLayout;
200 QStringList args;
201
202 int wholeLen = qCeil(endFrame * 44100.0 / fps);
203 for (auto clip : allSoundClips)
204 {
205 if (mCanceled)
206 {
207 return Status::CANCELED;
208 }
209
210 // Add sound file as input
211 args << "-i" << clip->fileName();
212
213 // Offset the sound to its correct position
214 // See https://superuser.com/questions/716320/ffmpeg-placing-audio-at-specific-location
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);
219
220 clipCount++;
221 }
222 // Remove final '+'
223 panChannelLayout.chop(1);
224 // Output arguments
225 // Mix audio
226 args << "-filter_complex";
227 if (clipCount == 1)
228 {
229 // If there is only one sound clip there is no need to use amerge
230 // Prior to ffmpeg 3.2, amerge does not support inputs=1
231 filterComplex.chop(1); // Remove final semicolon since there are no more filters added after
232 args << filterComplex << "-map" << amergeInput;
233 }
234 else {
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]";
238 }
239 // Convert audio file: 44100Hz sampling rate, stereo, signed 16 bit little endian
240 // Supported audio file types: wav, mp3, ogg... ( all file types supported by ffmpeg )
241 args << "-ar" << "44100" << "-acodec" << "pcm_s16le" << "-ac" << "2" << "-y";
242 // Trim audio
243 args << "-ss" << QString::number((startFrame - 1) / static_cast<double>(fps));
244 args << "-to" << QString::number(endFrame / static_cast<double>(fps));
245 // Output path
246 args << tempAudioPath;
247
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;
250
251 return Status::OK;
252}
253
270Status MovieExporter::generateMovie(
271 const Object* obj,
272 QString ffmpegPath,
273 QString strOutputFile,
274 std::function<void(float)> progress)
275{
276 if (mCanceled)
277 {
278 return Status::CANCELED;
279 }
280
281 // Frame generation setup
282
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;
289
290 auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
291 if (cameraLayer == nullptr)
292 {
293 cameraLayer = obj->getLayersByType< LayerCamera >().front();
294 }
295 int currentFrame = frameStart;
296
297 /* We create an image with the correct dimensions and background
298 * color here and then copy this and draw over top of it to
299 * generate each frame. This is faster than having to generate
300 * a new background image for each frame.
301 */
302 QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
303 QColor bgColor = Qt::white;
304 if (transparency)
305 {
306 bgColor.setAlpha(0);
307 }
308 imageToExportBase.fill(bgColor);
309
310 QSize camSize = cameraLayer->getViewSize();
311 QTransform centralizeCamera;
312 centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
313
314 int failCounter = 0;
315 /* Movie export uses a "sliding window" to reduce memory usage
316 * while having a relatively small impact on speed. This basically
317 * means that there is a maximum number of frames that can be waiting
318 * to be encoded by ffmpeg at any one time. The limit is set by the
319 * frameWindow variable which is designed to take up a maximum of
320 * about 1GB of memory
321 */
322 int frameWindow = static_cast<int>(1e9 / (camSize.width() * camSize.height() * 4.0));
323
324 // Build FFmpeg command
325
326 //int exportFps = mDesc.videoFps;
327 const QString tempAudioPath = QDir(mTempWorkDir).filePath("tmpaudio.wav");
328
329 QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
330 args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
331 args << "-framerate" << QString::number(mDesc.fps);
332
333 //args << "-r" << QString::number(exportFps);
334 args << "-i" << "-";
335 args << "-threads" << (QThread::idealThreadCount() == 1 ? "0" : QString::number(QThread::idealThreadCount()));
336
337 if (QFile::exists(tempAudioPath))
338 {
339 args << "-i" << tempAudioPath;
340 }
341
342 if (strOutputFile.endsWith(".apng", Qt::CaseInsensitive))
343 {
344 args << "-plays" << (loop ? "0" : "1");
345 }
346
347 if (strOutputFile.endsWith(".mp4", Qt::CaseInsensitive))
348 {
349 args << "-pix_fmt" << "yuv420p";
350 }
351
352 if (strOutputFile.endsWith(".avi", Qt::CaseInsensitive))
353 {
354 args << "-q:v" << "5";
355 }
356
357 args << "-y";
358 args << strOutputFile;
359
360 // Run FFmpeg command
361
362 Status status = executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
363 {
364 if(framesProcessed < 0)
365 {
366 failCounter++;
367 }
368
369 if(currentFrame > frameEnd)
370 {
371 ffmpeg.closeWriteChannel();
372 return false;
373 }
374
375 if((currentFrame - frameStart <= framesProcessed + frameWindow || failCounter > 10) && currentFrame <= frameEnd)
376 {
377 QImage imageToExport = imageToExportBase.copy();
378 QPainter painter(&imageToExport);
379
380 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
381 painter.setWorldTransform(view * centralizeCamera);
382 painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
383
384 obj->paintImage(painter, currentFrame, false, true);
385 painter.end();
386
387#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
388 int bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.sizeInBytes());
389 Q_ASSERT(bytesWritten == imageToExport.sizeInBytes());
390#else
391 int bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
392 Q_ASSERT(bytesWritten == imageToExport.byteCount());
393#endif
394
395 currentFrame++;
396 failCounter = 0;
397 return true;
398 }
399
400 return false;
401 });
402 STATUS_CHECK(status);
403
404 return Status::OK;
405}
406
419Status MovieExporter::generateGif(
420 const Object* obj,
421 QString ffmpegPath,
422 QString strOut,
423 std::function<void(float)> progress)
424{
425
426 if (mCanceled)
427 {
428 return Status::CANCELED;
429 }
430
431 // Frame generation setup
432
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;
439 int bytesWritten;
440
441 auto cameraLayer = static_cast<LayerCamera*>(obj->findLayerByName(strCameraName, Layer::CAMERA));
442 if (cameraLayer == nullptr)
443 {
444 cameraLayer = obj->getLayersByType< LayerCamera >().front();
445 }
446 int currentFrame = frameStart;
447
448 /* We create an image with the correct dimensions and background
449 * color here and then copy this and draw over top of it to
450 * generate each frame. This is faster than having to generate
451 * a new background image for each frame.
452 */
453 QImage imageToExportBase(exportSize, QImage::Format_ARGB32_Premultiplied);
454 QColor bgColor = Qt::white;
455 if (transparency)
456 {
457 bgColor.setAlpha(0);
458 }
459 imageToExportBase.fill(bgColor);
460
461 QSize camSize = cameraLayer->getViewSize();
462 QTransform centralizeCamera;
463 centralizeCamera.translate(camSize.width() / 2, camSize.height() / 2);
464
465 // Build FFmpeg command
466
467 QStringList args = {"-f", "rawvideo", "-pixel_format", "bgra"};
468 args << "-video_size" << QString("%1x%2").arg(exportSize.width()).arg(exportSize.height());
469 args << "-framerate" << QString::number(mDesc.fps);
470
471 args << "-i" << "-";
472
473 args << "-y";
474
475 args << "-filter_complex" << "[0:v]palettegen [p]; [0:v][p] paletteuse";
476
477 args << "-loop" << (loop ? "0" : "-1");
478 args << strOut;
479
480 // Run FFmpeg command
481
482 Status status = executeFFMpegPipe(ffmpegPath, args, progress, [&](QProcess& ffmpeg, int framesProcessed)
483 {
484 /* The GIF FFmpeg command requires the entires stream to be
485 * written before FFmpeg can encode the GIF. This is because
486 * the generated pallete is based off of the colors in all
487 * frames. The only way to avoid this would be to generate
488 * all the frames twice and run two separate commands, which
489 * would likely have unacceptable speed costs.
490 */
491
492 Q_UNUSED(framesProcessed);
493 if(currentFrame > frameEnd)
494 {
495 ffmpeg.closeWriteChannel();
496 return false;
497 }
498
499 QImage imageToExport = imageToExportBase.copy();
500 QPainter painter(&imageToExport);
501
502 QTransform view = cameraLayer->getViewAtFrame(currentFrame);
503 painter.setWorldTransform(view * centralizeCamera);
504 painter.setWindow(QRect(0, 0, camSize.width(), camSize.height()));
505
506 obj->paintImage(painter, currentFrame, false, true);
507
508#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
509 bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.sizeInBytes());
510 Q_ASSERT(bytesWritten == imageToExport.sizeInBytes());
511#else
512 bytesWritten = ffmpeg.write(reinterpret_cast<const char*>(imageToExport.constBits()), imageToExport.byteCount());
513 Q_ASSERT(bytesWritten == imageToExport.byteCount());
514#endif
515
516 currentFrame++;
517
518 return true;
519 });
520 STATUS_CHECK(status);
521
522 return Status::OK;
523}
524
541Status MovieExporter::executeFFmpeg(const QString& cmd, const QStringList& args, std::function<bool(int)> progress)
542{
543 qDebug() << cmd;
544
545 QProcess ffmpeg;
546 ffmpeg.setReadChannel(QProcess::StandardOutput);
547 // FFmpeg writes to stderr only for some reason, so we just read both channels together
548 ffmpeg.setProcessChannelMode(QProcess::MergedChannels);
549 ffmpeg.start(cmd, args);
550
551 Status status = Status::OK;
552 DebugDetails dd;
553 dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
554 if (ffmpeg.waitForStarted())
555 {
556 while(ffmpeg.state() == QProcess::Running)
557 {
558 if(!ffmpeg.waitForReadyRead()) break;
559
560 QString output(ffmpeg.readAll());
561 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
562 for (const QString& s : sList)
563 {
564 qDebug() << "[ffmpeg]" << s;
565 dd << s;
566 }
567
568 if(output.startsWith("frame="))
569 {
570 QString frame = output.mid(6, output.indexOf(' '));
571
572 bool shouldContinue = progress(frame.toInt());
573 if (!shouldContinue)
574 {
575 ffmpeg.terminate();
576 ffmpeg.waitForFinished(3000);
577 if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
578 ffmpeg.waitForFinished();
579 return Status::CANCELED;
580 }
581 }
582 }
583
584 QString output(ffmpeg.readAll());
585 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
586 for (const QString& s : sList)
587 {
588 qDebug() << "[ffmpeg]" << s;
589 dd << s;
590 }
591
592 if(ffmpeg.exitStatus() != QProcess::NormalExit || ffmpeg.exitCode() != 0)
593 {
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."));
597 dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
598 << QString("Exit code: %1").arg(ffmpeg.exitCode());
599 status.setDetails(dd);
600 return status;
601 }
602 }
603 else
604 {
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);
610 }
611 return status;
612}
613
657Status MovieExporter::executeFFMpegPipe(const QString& cmd, const QStringList& args, std::function<void(float)> progress, std::function<bool(QProcess&, int)> writeFrame)
658{
659 qDebug() << cmd;
660
661 QProcess ffmpeg;
662 ffmpeg.setReadChannel(QProcess::StandardOutput);
663 // FFmpeg writes to stderr only for some reason, so we just read both channels together
664 ffmpeg.setProcessChannelMode(QProcess::MergedChannels);
665 ffmpeg.start(cmd, args);
666
667 Status status = Status::OK;
668 DebugDetails dd;
669 dd << QStringLiteral("Command: %1 %2").arg(cmd).arg(args.join(' '));
670 if (ffmpeg.waitForStarted())
671 {
672 int framesGenerated = 0;
673 int lastFrameProcessed = 0;
674 const int frameStart = mDesc.startFrame;
675 const int frameEnd = mDesc.endFrame;
676 while(ffmpeg.state() == QProcess::Running)
677 {
678 if (mCanceled)
679 {
680 ffmpeg.terminate();
681 if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
682 return Status::CANCELED;
683 }
684
685 // Check FFmpeg progress
686
687 int framesProcessed = -1;
688 if(ffmpeg.waitForReadyRead(10))
689 {
690 QString output(ffmpeg.readAll());
691 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
692 for (const QString& s : sList)
693 {
694 qDebug() << "[ffmpeg]" << s;
695 dd << s;
696 }
697 if(output.startsWith("frame="))
698 {
699 lastFrameProcessed = framesProcessed = output.mid(6, output.indexOf(' ')).toInt();
700 }
701 }
702
703 if(!ffmpeg.isWritable())
704 {
705 continue;
706 }
707
708 while(writeFrame(ffmpeg, framesProcessed))
709 {
710 framesGenerated++;
711
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);
715 }
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);
719 }
720
721 QString output(ffmpeg.readAll());
722 QStringList sList = output.split(QRegularExpression("[\r\n]"), SplitBehaviorFlags::SkipEmptyParts);
723 for (const QString& s : sList)
724 {
725 qDebug() << "[ffmpeg]" << s;
726 dd << s;
727 }
728
729 if(ffmpeg.exitStatus() != QProcess::NormalExit || ffmpeg.exitCode() != 0)
730 {
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."));
734 dd << QString("Exit status: ").append(QProcess::NormalExit ? "NormalExit": "CrashExit")
735 << QString("Exit code: %1").arg(ffmpeg.exitCode());
736 status.setDetails(dd);
737 return status;
738 }
739 }
740 else
741 {
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);
747 }
748
749 return status;
750}
751
752Status MovieExporter::checkInputParameters(const ExportMovieDesc& desc)
753{
754 bool b = true;
755 b &= (!desc.strFileName.isEmpty());
756 b &= (desc.startFrame > 0);
757 b &= (desc.endFrame >= desc.startFrame);
758 b &= (desc.fps > 0);
759 b &= (!desc.strCameraName.isEmpty());
760
761 return b ? Status::OK : Status::INVALID_ARGUMENT;
762}
DebugDetails
Definition: pencilerror.h:25
KeyFrame
Definition: keyframe.h:30
LayerCamera
Definition: layercamera.h:30
LayerSound
Definition: layersound.h:26
MovieExporter::run
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.
Definition: movieexporter.cpp:78
MovieExporter::assembleAudio
Status assembleAudio(const Object *obj, QString ffmpegPath, std::function< void(float)> progress)
Combines all audio tracks in obj into a single file.
Definition: movieexporter.cpp:162
MovieExporter::generateMovie
Status generateMovie(const Object *obj, QString ffmpegPath, QString strOutputFile, std::function< void(float)> progress)
Exports obj to a movie image at strOut using FFmpeg.
Definition: movieexporter.cpp:270
MovieExporter::generateGif
Status generateGif(const Object *obj, QString ffmpeg, QString strOut, std::function< void(float)> progress)
Exports obj to a gif image at strOut using FFmpeg.
Definition: movieexporter.cpp:419
MovieExporter::executeFFMpegPipe
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...
Definition: movieexporter.cpp:657
MovieExporter::executeFFmpeg
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.
Definition: movieexporter.cpp:541
Object
Definition: object.h:42
SoundClip
Definition: soundclip.h:27
Status
Definition: pencilerror.h:40
QColor
QColor::setAlpha
void setAlpha(int alpha)
QDir
QDir::exists
bool exists() const const
QDir::filePath
QString filePath(const QString &fileName) const const
QFile::exists
bool exists() const const
QImage::byteCount
int byteCount() const const
QImage
QImage::Format_ARGB32_Premultiplied
Format_ARGB32_Premultiplied
QImage::constBits
const uchar * constBits() const const
QImage::copy
QImage copy(const QRect &rectangle) const const
QImage::fill
void fill(uint pixelValue)
QImage::sizeInBytes
qsizetype sizeInBytes() const const
QIODevice::isWritable
bool isWritable() const const
QIODevice::readAll
QByteArray readAll()
QIODevice::write
qint64 write(const char *data, qint64 maxSize)
QPainter
QPainter::end
bool end()
QPainter::setWindow
void setWindow(const QRect &rectangle)
QPainter::setWorldTransform
void setWorldTransform(const QTransform &matrix, bool combine)
QProcess
QProcess::NormalExit
NormalExit
QProcess::StandardOutput
StandardOutput
QProcess::MergedChannels
MergedChannels
QProcess::Running
Running
QProcess::closeWriteChannel
void closeWriteChannel()
QProcess::exitCode
int exitCode() const const
QProcess::exitStatus
QProcess::ExitStatus exitStatus() const const
QProcess::kill
void kill()
QProcess::setProcessChannelMode
void setProcessChannelMode(QProcess::ProcessChannelMode mode)
QProcess::setReadChannel
void setReadChannel(QProcess::ProcessChannel channel)
QProcess::start
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
QProcess::state
QProcess::ProcessState state() const const
QProcess::terminate
void terminate()
QProcess::waitForFinished
bool waitForFinished(int msecs)
QProcess::waitForReadyRead
virtual bool waitForReadyRead(int msecs) override
QProcess::waitForStarted
bool waitForStarted(int msecs)
QRect
QRegularExpression
QSize
QSize::height
int height() const const
QSize::width
int width() const const
QString::SplitBehavior
SplitBehavior
QString::split
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString
QString::append
QString & append(QChar ch)
QString::arg
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString::chop
void chop(int n)
QString::endsWith
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString::indexOf
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QString::isEmpty
bool isEmpty() const const
QString::mid
QString mid(int position, int n) const const
QString::number
QString number(int n, int base)
QString::startsWith
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString::toInt
int toInt(bool *ok, int base) const const
QStringList
QStringList::join
QString join(const QString &separator) const const
Qt::CaseInsensitive
CaseInsensitive
Qt::white
white
Qt::SplitBehaviorFlags
SplitBehaviorFlags
QTemporaryDir::isValid
bool isValid() const const
QTemporaryDir::path
QString path() const const
QThread::idealThreadCount
int idealThreadCount()
QTransform
QTransform::translate
QTransform & translate(qreal dx, qreal dy)
ExportMovieDesc
Definition: movieexporter.h:31
Generated on Thu May 8 2025 04:47:53 for Pencil2D by doxygen 1.9.6 based on revision 4513250b1d5b1a3676ec0e67b06b7a885ceaae39