17#include "movieimporter.h"
20#include <QTemporaryDir>
22#include <QRegularExpression>
27#include "movieexporter.h"
28#include "layermanager.h"
29#include "viewmanager.h"
30#include "soundmanager.h"
33#include "bitmapimage.h"
42MovieImporter::~MovieImporter()
48 Status status = Status::OK;
50 Layer* layer = mEditor->layers()->currentLayer();
51 if (layer->type() != Layer::BITMAP)
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"));
60 STATUS_CHECK(verifyFFmpegExists());
61 QString ffmpegPath = ffmpegLocation();
62 dd <<
"ffmpeg path:" << ffmpegPath;
67 QString ffprobePath = ffprobeLocation();
68 dd <<
"ffprobe path:" << ffprobePath;
71 QStringList probeArgs = {
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1", filePath};
74 ffprobe.
start(ffprobePath, probeArgs);
79 double seconds = output.
toDouble(&ok);
82 frames = qCeil(seconds * fps);
87 dd <<
"FFprobe output could not be parsed"
97 dd <<
"FFprobe did not exit normally"
105 qDebug() <<
"ffprobe execution failed. Details:";
106 qDebug() << dd.str();
116 ffmpeg.
start(ffmpegPath, probeArgs);
125#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
132 index = s.
indexOf(
"Duration: ");
135 QString format(
"hh:mm:ss.zzz");
138 frames = qMax(frames, curFrames);
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);
161 *frameEstimate = frames;
166 std::function<
void(
int)> progress,
167 std::function<
void(
QString)> progressMessage,
168 std::function<
bool()> askPermission)
170 if (mCanceled)
return Status::CANCELED;
172 Status status = Status::OK;
175 STATUS_CHECK(verifyFFmpegExists())
180 status = Status::FAIL;
181 status.setTitle(
tr(
"Error creating folder"));
182 status.setDescription(
tr(
"Unable to create a temporary folder, cannot import video."));
185 status.setDetails(dd);
188 mEditor->addTemporaryDir(mTempDir);
190 if (type == FileType::MOVIE) {
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.")
200 .arg(mEditor->currentFrame() + frames));
207 bool canProceed = askPermission();
209 if (!canProceed) {
return Status::CANCELED; }
212 auto progressCallback = [&progress,
this](
int prog) ->
bool
214 progress(prog);
return !mCanceled;
216 auto progressMsgCallback = [&progressMessage](
QString message)
218 progressMessage(message);
220 return importMovieVideo(filePath, fps, frames, progressCallback, progressMsgCallback);
222 else if (type == FileType::SOUND)
224 return importMovieAudio(filePath, [&progress,
this](
int prog) ->
bool
226 progress(prog);
return !mCanceled;
232 st.setTitle(
tr(
"Unknown error"));
233 st.setTitle(
tr(
"This should not happen..."));
238Status MovieImporter::importMovieVideo(
const QString &filePath,
int fps,
int frameEstimate,
239 std::function<
bool(
int)> progress,
240 std::function<
void(
QString)> progressMessage)
242 Status status = Status::OK;
244 Layer* layer = mEditor->layers()->currentLayer();
245 if (layer->type() != Layer::BITMAP)
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"));
258 progress(qFloor(qMin(frame /
static_cast<double>(frameEstimate), 1.0) * 50));
return !mCanceled; }
261 if (!status.ok() && status != Status::CANCELED) {
return status; }
263 if(mCanceled)
return Status::CANCELED;
265 progressMessage(
tr(
"Video processed, adding frames..."));
269 return generateFrames([
this, &progress](
int prog) ->
bool
271 progress(prog);
return mCanceled;
275Status MovieImporter::generateFrames(std::function<
bool(
int)> progress)
277 Layer* layer = mEditor->layers()->currentLayer();
278 Status status = Status::OK;
281 auto amountOfFrames = tempDir.count();
289 int currentFrame = mEditor->currentFrame();
290 if(layer->keyExists(mEditor->currentFrame())) {
291 mEditor->importImage(currentFile);
296 imgTopLeft.
setX(
static_cast<int>(viewMan->getImportView().
dx()) - bitmapImage->image()->
width() / 2);
297 imgTopLeft.
setY(
static_cast<int>(viewMan->getImportView().
dy()) - bitmapImage->image()->
height() / 2);
298 bitmapImage->moveTopLeft(imgTopLeft);
302 mEditor->scrubTo(currentFrame + 1);
304 if (mCanceled)
return Status::CANCELED;
305 progress(qFloor(50 + i /
static_cast<qreal
>(amountOfFrames) * 50));
307 currentFile = tempDir.filePath(
QString(
"%1.png").arg(i, 5, 10,
QChar(
'0')));
311 status = Status::FAIL;
312 status.setTitle(
tr(
"Failed import"));
313 status.setDescription(
tr(
"Was unable to find internal files, import unsuccessful."));
320Status MovieImporter::importMovieAudio(
const QString& filePath, std::function<
bool(
int)> progress)
322 Layer* layer = mEditor->layers()->currentLayer();
324 Status status = Status::OK;
325 if (layer->type() != Layer::SOUND)
327 status = Status::FAIL;
328 status.setTitle(
tr(
"Sound only"));
329 status.setDescription(
tr(
"You need to be on a sound layer to import the audio"));
333 int currentFrame = mEditor->currentFrame();
335 if (layer->keyExists(currentFrame))
338 if (!key->fileName().
isEmpty())
340 status = Status::FAIL;
341 status.setTitle(
tr(
"Move to an empty frame"));
342 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));
345 layer->removeKeyFrame(currentFrame);
350 QStringList args{
"-i", filePath,
"-map_metadata",
"-1",
"-flags",
"bitexact",
"-fflags",
"bitexact", audioPath };
354 progress(50);
return !mCanceled;
357 if(mCanceled)
return Status::CANCELED;
360 Q_ASSERT(!layer->keyExists(currentFrame));
365 key->setSoundClipName(
QFileInfo(filePath).fileName());
366 Status st = mEditor->sound()->loadSound(key, audioPath);
370 layer->removeKeyFrame(currentFrame);
378Status MovieImporter::verifyFFmpegExists()
380 QString ffmpegPath = ffmpegLocation();
383 Status status = Status::ERROR_FFMPEG_NOT_FOUND;
384 status.setTitle(
tr(
"FFmpeg Not Found"));
385 status.setDescription(
tr(
"Please place the ffmpeg binary in plugins directory and try again"));
virtual bool addKeyFrame(int position, KeyFrame *pKeyFrame)
Adds a keyframe at the given position, unless one already exists.
void notifyAnimationLengthChanged()
This should be emitted whenever the animation length frames, eg.
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.
Status run(const QString &filePath, int fps, FileType type, std::function< void(int)> progress, std::function< void(QString)> progressMessage, std::function< bool()> askPermission)
Status estimateFrames(const QString &filePath, int fps, int *frameEstimate)
Attempts to load a video and determine it's duration.
QString filePath(const QString &fileName) const const
bool exists() const const
bool exists() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
bool isNull() const const
int exitCode() const const
QProcess::ExitStatus exitStatus() const const
void setProcessChannelMode(QProcess::ProcessChannelMode mode)
void setReadChannel(QProcess::ProcessChannel channel)
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
QProcess::ProcessState state() const const
bool waitForFinished(int msecs)
virtual bool waitForReadyRead(int msecs) override
bool waitForStarted(int msecs)
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString & append(QChar ch)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
bool isEmpty() const const
QString mid(int position, int n) const const
QString number(int n, int base)
double toDouble(bool *ok) const const
int indexOf(QStringView str, int from) const const
QString errorString() const const
bool isValid() const const
QString path() const const
QTime fromString(const QString &string, Qt::DateFormat format)