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