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