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
movieimporter.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon
5Copyright (C) 2012-2020 Matthew Chiawen Chang
6
7This program is free software; you can redistribute it and/or
8modify it under the terms of the GNU General Public License
9as published by the Free Software Foundation; version 2 of the License.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16*/
17#include "movieimporter.h"
18
19#include <QDebug>
20#include <QTemporaryDir>
21#include <QProcess>
22#include <QRegularExpression>
23#include <QtMath>
24#include <QTime>
25#include <QFileInfo>
26
27#include "movieexporter.h"
28#include "layermanager.h"
29#include "viewmanager.h"
30#include "soundmanager.h"
31
32#include "soundclip.h"
33#include "bitmapimage.h"
34
35#include "util.h"
36#include "editor.h"
37
38MovieImporter::MovieImporter(QObject* parent) : QObject(parent)
39{
40}
41
42MovieImporter::~MovieImporter()
43{
44}
45
46Status MovieImporter::estimateFrames(const QString &filePath, int fps, int *frameEstimate)
47{
48 Status status = Status::OK;
49 DebugDetails dd;
50 Layer* layer = mEditor->layers()->currentLayer();
51 if (layer->type() != Layer::BITMAP)
52 {
53 status = Status::FAIL;
54 status.setTitle(tr("Bitmap only"));
55 status.setDescription(tr("You need to be on the bitmap layer to import a movie clip"));
56 return status;
57 }
58
59 // --------- Import all the temporary frames ----------
60 STATUS_CHECK(verifyFFmpegExists());
61 QString ffmpegPath = ffmpegLocation();
62 dd << "ffmpeg path:" << ffmpegPath;
63
64 // Get frame estimate
65 int frames = -1;
66 bool ok = true;
67 QString ffprobePath = ffprobeLocation();
68 dd << "ffprobe path:" << ffprobePath;
69 if (QFileInfo::exists(ffprobePath))
70 {
71 QStringList probeArgs = {"-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filePath};
72 QProcess ffprobe;
73 ffprobe.setReadChannel(QProcess::StandardOutput);
74 ffprobe.start(ffprobePath, probeArgs);
75 ffprobe.waitForFinished();
76 if (ffprobe.exitStatus() == QProcess::NormalExit && ffprobe.exitCode() == 0)
77 {
78 QString output(ffprobe.readAll());
79 double seconds = output.toDouble(&ok);
80 if (ok)
81 {
82 frames = qCeil(seconds * fps);
83 }
84 else
85 {
86 ffprobe.setReadChannel(QProcess::StandardError);
87 dd << "FFprobe output could not be parsed"
88 << "stdout:"
89 << output
90 << "stderr:"
91 << ffprobe.readAll();
92 }
93 }
94 else
95 {
96 ffprobe.setProcessChannelMode(QProcess::MergedChannels);
97 dd << "FFprobe did not exit normally"
98 << QString("Exit status: ").append(ffprobe.exitStatus() == QProcess::NormalExit ? "NormalExit" : "CrashExit")
99 << QString("Exit code: %1").arg(ffprobe.exitCode())
100 << "Output:"
101 << ffprobe.readAll();
102 }
103 if (frames < 0)
104 {
105 qDebug() << "ffprobe execution failed. Details:";
106 qDebug() << dd.str();
107 }
108 }
109 if (frames < 0)
110 {
111 // Fallback to ffmpeg
112 QStringList probeArgs = {"-i", filePath};
113 QProcess ffmpeg;
114 // FFmpeg writes to stderr only for some reason, so we just read both channels together
115 ffmpeg.setProcessChannelMode(QProcess::MergedChannels);
116 ffmpeg.start(ffmpegPath, probeArgs);
117 if (ffmpeg.waitForStarted() == true)
118 {
119 int index = -1;
120 while (ffmpeg.state() == QProcess::Running)
121 {
122 if (!ffmpeg.waitForReadyRead()) break;
123
124 QString output(ffmpeg.readAll());
125#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
126 QStringList sList = output.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts);
127#else
128 QStringList sList = output.split(QRegularExpression("[\r\n]"), QString::SkipEmptyParts);
129#endif
130 for (const QString& s : sList)
131 {
132 index = s.indexOf("Duration: ");
133 if (index >= 0)
134 {
135 QString format("hh:mm:ss.zzz");
136 QString durationString = s.mid(index + 10, format.length()-1) + "0";
137 int curFrames = qCeil(QTime(0, 0).msecsTo(QTime::fromString(durationString, format)) / 1000.0 * fps);
138 frames = qMax(frames, curFrames);
139
140 // We've got what we need, stop running
141 ffmpeg.terminate();
142 ffmpeg.waitForFinished(3000);
143 if (ffmpeg.state() == QProcess::Running) ffmpeg.kill();
144 ffmpeg.waitForFinished();
145 break;
146 }
147 }
148 }
149 }
150 }
151
152 if (frames < 0)
153 {
154 status = Status::FAIL;
155 status.setTitle(tr("Loading video failed"));
156 status.setDescription(tr("Could not get duration from the specified video. Are you sure you are importing a valid video file?"));
157 status.setDetails(dd);
158 return status;
159 }
160
161 *frameEstimate = frames;
162 return status;
163}
164
165Status MovieImporter::run(const QString &filePath, int fps, FileType type,
166 std::function<void(int)> progress,
167 std::function<void(QString)> progressMessage,
168 std::function<bool()> askPermission)
169{
170 if (mCanceled) return Status::CANCELED;
171
172 Status status = Status::OK;
173 DebugDetails dd;
174
175 STATUS_CHECK(verifyFFmpegExists())
176
177 mTempDir = new QTemporaryDir();
178 if (!mTempDir->isValid())
179 {
180 status = Status::FAIL;
181 status.setTitle(tr("Error creating folder"));
182 status.setDescription(tr("Unable to create a temporary folder, cannot import video."));
183 dd << QString("Path: ").append(mTempDir->path())
184 << QString("Error: ").append(mTempDir->errorString());
185 status.setDetails(dd);
186 return status;
187 }
188 mEditor->addTemporaryDir(mTempDir);
189
190 if (type == FileType::MOVIE) {
191 int frames = 0;
192 STATUS_CHECK(estimateFrames(filePath, fps, &frames));
193
194 if (mEditor->currentFrame() + frames > MaxFramesBound) {
195 status = Status::FAIL;
196 status.setTitle(tr("Imported movie too big!"));
197 status.setDescription(tr("The movie clip is too long. Pencil2D can only hold %1 frames, but this movie would go up to about frame %2. "
198 "Please make your video shorter and try again.")
199 .arg(MaxFramesBound)
200 .arg(mEditor->currentFrame() + frames));
201
202 return status;
203 }
204
205 if(frames > 200)
206 {
207 bool canProceed = askPermission();
208
209 if (!canProceed) { return Status::CANCELED; }
210 }
211
212 auto progressCallback = [&progress, this](int prog) -> bool
213 {
214 progress(prog); return !mCanceled;
215 };
216 auto progressMsgCallback = [&progressMessage](QString message)
217 {
218 progressMessage(message);
219 };
220 return importMovieVideo(filePath, fps, frames, progressCallback, progressMsgCallback);
221 }
222 else if (type == FileType::SOUND)
223 {
224 return importMovieAudio(filePath, [&progress, this](int prog) -> bool
225 {
226 progress(prog); return !mCanceled;
227 });
228 }
229 else
230 {
231 Status st = Status::FAIL;
232 st.setTitle(tr("Unknown error"));
233 st.setTitle(tr("This should not happen..."));
234 return st;
235 }
236}
237
238Status MovieImporter::importMovieVideo(const QString &filePath, int fps, int frameEstimate,
239 std::function<bool(int)> progress,
240 std::function<void(QString)> progressMessage)
241{
242 Status status = Status::OK;
243
244 Layer* layer = mEditor->layers()->currentLayer();
245 if (layer->type() != Layer::BITMAP)
246 {
247 status = Status::FAIL;
248 status.setTitle(tr("Bitmap only"));
249 status.setDescription(tr("You need to be on the bitmap layer to import a movie clip"));
250 return status;
251 }
252
253 QStringList args = {"-i", filePath};
254 args << "-r" << QString::number(fps);
255 args << QDir(mTempDir->path()).filePath("%05d.png");
256
257 status = MovieExporter::executeFFmpeg(ffmpegLocation(), args, [&progress, frameEstimate, this] (int frame) {
258 progress(qFloor(qMin(frame / static_cast<double>(frameEstimate), 1.0) * 50)); return !mCanceled; }
259 );
260
261 if (!status.ok() && status != Status::CANCELED) { return status; }
262
263 if(mCanceled) return Status::CANCELED;
264
265 progressMessage(tr("Video processed, adding frames..."));
266
267 progress(50);
268
269 return generateFrames([this, &progress](int prog) -> bool
270 {
271 progress(prog); return mCanceled;
272 });
273}
274
275Status MovieImporter::generateFrames(std::function<bool(int)> progress)
276{
277 Status status = Status::OK;
278 int i = 1;
279 QDir tempDir(mTempDir->path());
280 auto amountOfFrames = tempDir.count();
281 QString currentFile(tempDir.filePath(QString("%1.png").arg(i, 5, 10, QChar('0'))));
282
283 ImportImageConfig importImageConfig;
284 importImageConfig.positionType = ImportImageConfig::CenterOfCameraFollowed;
285 while (QFileInfo::exists(currentFile))
286 {
287 status = mEditor->importImage(currentFile, importImageConfig);
288
289 if (!status.ok()) {
290 break;
291 }
292
293 if (mCanceled) return Status::CANCELED;
294 progress(qFloor(50 + i / static_cast<qreal>(amountOfFrames) * 50));
295 i++;
296 currentFile = tempDir.filePath(QString("%1.png").arg(i, 5, 10, QChar('0')));
297 }
298
299 if (!QFileInfo::exists(tempDir.filePath("00001.png"))) {
300 status = Status::FAIL;
301 status.setTitle(tr("Failed import"));
302 status.setDescription(tr("Was unable to find internal files, import unsuccessful."));
303 return status;
304 }
305
306 return status;
307}
308
309Status MovieImporter::importMovieAudio(const QString& filePath, std::function<bool(int)> progress)
310{
311 Layer* layer = mEditor->layers()->currentLayer();
312
313 Status status = Status::OK;
314 if (layer->type() != Layer::SOUND)
315 {
316 status = Status::FAIL;
317 status.setTitle(tr("Sound only"));
318 status.setDescription(tr("You need to be on a sound layer to import the audio"));
319 return status;
320 }
321
322 int currentFrame = mEditor->currentFrame();
323
324 if (layer->keyExists(currentFrame))
325 {
326 SoundClip* key = static_cast<SoundClip*>(layer->getKeyFrameAt(currentFrame));
327 if (!key->fileName().isEmpty())
328 {
329 status = Status::FAIL;
330 status.setTitle(tr("Move to an empty frame"));
331 status.setDescription(tr("A frame already exists on frame: %1 Move the scrubber to a empty position on the timeline and try again").arg(currentFrame));
332 return status;
333 }
334 layer->removeKeyFrame(currentFrame);
335 }
336
337 QString audioPath = QDir(mTempDir->path()).filePath("audio.wav");
338
339 QStringList args{ "-i", filePath, "-map_metadata", "-1", "-flags", "bitexact", "-fflags", "bitexact", audioPath };
340
341 status = MovieExporter::executeFFmpeg(ffmpegLocation(), args, [&progress, this] (int frame) {
342 Q_UNUSED(frame)
343 progress(50); return !mCanceled;
344 });
345
346 if(mCanceled) return Status::CANCELED;
347 progress(90);
348
349 Q_ASSERT(!layer->keyExists(currentFrame));
350
351 SoundClip* key = new SoundClip;
352 layer->addKeyFrame(currentFrame, key);
353
354 key->setSoundClipName(QFileInfo(filePath).fileName()); // keep the original file name
355 Status st = mEditor->sound()->loadSound(key, audioPath);
356
357 if (!st.ok())
358 {
359 layer->removeKeyFrame(currentFrame);
360 return st;
361 }
362
363 return Status::OK;
364}
365
366
367Status MovieImporter::verifyFFmpegExists()
368{
369 QString ffmpegPath = ffmpegLocation();
370 if (!QFile::exists(ffmpegPath))
371 {
372 Status status = Status::ERROR_FFMPEG_NOT_FOUND;
373 status.setTitle(tr("FFmpeg Not Found"));
374 status.setDescription(tr("Please place the ffmpeg binary in plugins directory and try again"));
375 return status;
376 }
377 return Status::OK;
378}
DebugDetails
Definition: pencilerror.h:25
Layer
Definition: layer.h:33
Layer::addKeyFrame
virtual bool addKeyFrame(int position, KeyFrame *pKeyFrame)
Adds a keyframe at the given position, unless one already exists.
Definition: layer.cpp:191
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
MovieImporter::run
Status run(const QString &filePath, int fps, FileType type, std::function< void(int)> progress, std::function< void(QString)> progressMessage, std::function< bool()> askPermission)
Definition: movieimporter.cpp:165
MovieImporter::estimateFrames
Status estimateFrames(const QString &filePath, int fps, int *frameEstimate)
Attempts to load a video and determine it's duration.
Definition: movieimporter.cpp:46
SoundClip
Definition: soundclip.h:27
Status
Definition: pencilerror.h:40
QChar
QDir
QDir::filePath
QString filePath(const QString &fileName) const const
QFile::exists
bool exists() const const
QFileInfo
QFileInfo::exists
bool exists() const const
QIODevice::readAll
QByteArray readAll()
QObject
QObject::tr
QString tr(const char *sourceText, const char *disambiguation, int n)
QProcess
QProcess::NormalExit
NormalExit
QProcess::StandardOutput
StandardOutput
QProcess::MergedChannels
MergedChannels
QProcess::Running
Running
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)
QRegularExpression
QString::SkipEmptyParts
SkipEmptyParts
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::isEmpty
bool isEmpty() const const
QString::length
int length() const const
QString::mid
QString mid(int position, int n) const const
QString::number
QString number(int n, int base)
QString::toDouble
double toDouble(bool *ok) const const
QStringList
QStringList::indexOf
int indexOf(QStringView str, int from) const const
Qt::SkipEmptyParts
SkipEmptyParts
QTemporaryDir
QTemporaryDir::errorString
QString errorString() const const
QTemporaryDir::isValid
bool isValid() const const
QTemporaryDir::path
QString path() const const
QTime
QTime::fromString
QTime fromString(const QString &string, Qt::DateFormat format)
ImportImageConfig
Definition: importimageconfig.h:22
Generated on Thu May 8 2025 04:47:53 for Pencil2D by doxygen 1.9.6 based on revision 4513250b1d5b1a3676ec0e67b06b7a885ceaae39