Krita Source Code Documentation
Loading...
Searching...
No Matches
KisFFMpegWrapper.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2020 Dmitrii Utkin <loentar@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.1-only
5 */
6
7#include "KisFFMpegWrapper.h"
8
9#include <klocalizedstring.h>
10
11#include <KoResourcePaths.h>
12#include <QProcess>
13#include <QJsonDocument>
14#include <QJsonObject>
15#include <QJsonArray>
16#include <QDir>
17#include <QDebug>
18#include <QRegularExpression>
19#include <QApplication>
20#include <QThread>
21
22#include "kis_config.h"
23
24#include "KisPart.h"
25
26namespace
27{
28QRegularExpression frameRegexp("^frame=[ ]*([0-9]+) .*$");
29QRegularExpression lineDelimiter("[\n\r]");
30QRegularExpression junkRegex("\\[[a-zA-Z0-9]+ @ 0x[a-fA-F0-9]*\\][ ]*");
31QStringList errorWords = { "Unable", "Invalid", "Error", "failed", "NULL", "No such", "divisible", "not" };
32QRegularExpression ffmpegVersionRX("(ffmpeg|ffprobe) version (.+?)\\s");
33
34}
35
37 : QObject(parent)
38{
39}
40
44
45
47{
48 KIS_ASSERT(m_process == nullptr);
49
50 m_stdoutBuffer.clear();
51 m_errorMessage.clear();
52 m_processSTDOUT.clear();
53 m_processSTDERR.clear();
54
55 m_process.reset(new QProcess(this));
56 QStringList env(m_process->systemEnvironment());
57
58 m_processSettings = settings;
59
60 const QString renderLogPath = m_processSettings.outputFile + ".log";
61
62 // Create a new log per each ffmpeg operation..
63 if (QFile::exists(renderLogPath)) {
64 QFile existingFile(renderLogPath);
65 existingFile.remove();
66 }
67
68 QFile renderLog(renderLogPath); // Logs ONLY this render operation.
69
70 // Log ffmpeg command info..
71 if (renderLog.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
72 QString command = m_processSettings.processPath + " " + settings.defaultPrependArgs.join(" ") + " " + m_processSettings.args.join(" ") + " " + m_processSettings.outputFile;
73 renderLog.write(command.toUtf8());
74 renderLog.write("\n");
75 renderLog.write("=====================================================\n");
76
77 // Handle logged errors..
78 // Due to various reasons (including image preview), ffmpeg uses STDERR and not STDOUT for general output logging.
79 connect(this, &KisFFMpegWrapper::sigReadSTDERR, [renderLogPath](QByteArray stderrBuffer){
80 QFile renderLog(renderLogPath);
81 if (renderLog.open(QIODevice::WriteOnly | QIODevice::Append)) {
82 renderLog.write(stderrBuffer);
83 }
84 });
85 }
86
87 if (!settings.logPath.isEmpty()) {
88 QString sessionRenderLogPath(settings.logPath);
89 // Logs FULL history of Krita session render operations.
90 QFile sessionRenderLog(sessionRenderLogPath);
91
92 // Make directory..
93 const QString logDirPath = QFileInfo(sessionRenderLogPath).dir().path();
94 QDir().mkpath(logDirPath);
95
96 if (sessionRenderLog.open(QIODevice::WriteOnly | QIODevice::Append)) {
97 // Append finished renderlog to sessionlog..
98 connect(this, &KisFFMpegWrapper::sigFinishedWithError, [renderLogPath, sessionRenderLogPath](QString){
99 QFile renderLog(renderLogPath);
100 QFile sessionRenderLog(sessionRenderLogPath);
101 if (renderLog.open(QIODevice::ReadOnly) &&
102 sessionRenderLog.open(QIODevice::WriteOnly | QIODevice::Append)) {
103
104 QByteArray buffer;
105 int chunksize = 256;
106 while (!(buffer = renderLog.read(chunksize)).isEmpty()) {
107 sessionRenderLog.write(buffer);
108 }
109 }
110 });
111 }
112 }
113
115 QString progressText = m_processSettings.progressMessage;
116
117 progressText.replace("[progress]", "0");
118
119 m_progress = toQShared(new QProgressDialog(progressText, "", 0, 0));
120
121 m_progress->setWindowModality(Qt::ApplicationModal);
122 m_progress->setCancelButton(0);
123 m_progress->setMinimumDuration(0);
124 m_progress->setValue(0);
125
126 if (settings.progressIndeterminate) {
127 m_progress->setRange(0,0);
128 } else {
129 m_progress->setRange(0, 100);
130 }
131
132 connect(m_progress.data(), SIGNAL(canceled()), m_process.data(), SLOT(kill()));
133
134 m_progress->show();
135
136 dbgFile << "Open progress dialog!";
137 }
138
139 connect(m_process.data(), SIGNAL(readyReadStandardOutput()), SLOT(slotReadyReadSTDOUT()));
140 connect(m_process.data(), SIGNAL(readyReadStandardError()), SLOT(slotReadyReadSTDERR()));
141 connect(m_process.data(), SIGNAL(started()), SLOT(slotStarted()));
142 connect(m_process.data(), SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(slotFinished(int)));
143
144 QStringList args;
145
146 if ( !settings.defaultPrependArgs.isEmpty() ) {
147 args << settings.defaultPrependArgs;
148 }
149
150 args << settings.args;
151
152 if ( !settings.outputFile.isEmpty() ) {
153 args << settings.outputFile;
154 }
155
156 dbgFile << "starting process: " << qUtf8Printable(settings.processPath) << args;
157
159
160 m_process->start(settings.processPath, args);
161}
162
164{
165 struct ProcessResults {
166 bool finish = false;
167 QString error = QString();
168 };
169
170 QSharedPointer<ProcessResults> processResults = toQShared(new ProcessResults);
171
172 connect( this, &KisFFMpegWrapper::sigFinishedWithError, [processResults](const QString& errMsg){
173 processResults->finish = true;
174 processResults->error = errMsg;
175 });
176
177 connect( this, &KisFFMpegWrapper::sigFinished, [processResults](){
178 processResults->finish = true;
179 });
180
181 startNonBlocking(settings);
183
184 if (processResults->finish == true) {
185 if (processResults->error.isEmpty()) {
187 } else {
189 }
190 } else {
191 reset(); // Ensure process isn't running before returning failure here.
193 }
194}
195
197{
198 if (!m_process) return false;
199
200 if (m_process->waitForStarted(msecs)) {
201 return (m_process->waitForFinished(msecs) && m_process->exitStatus() == QProcess::ExitStatus::NormalExit);
202 }
203
204 return false;
205}
206
208
209 dbgFile << "Update Progress" << progressValue << "/" << m_processSettings.totalFrames;
210
211 if (!m_progress) return;
212
213 QString progressText = m_processSettings.progressMessage;
214 QStringList outputFileParts = m_processSettings.outputFile.split(".");
215 QString suffix = outputFileParts.size() == 2 ? outputFileParts[1] : m_processSettings.outputFile;
216
217 progressText.replace("[progress]", QString::number(progressValue));
218 progressText.replace("[framecount]", QString::number(m_processSettings.totalFrames));
219 progressText.replace("[suffix]", suffix );
220 m_progress->setLabelText(progressText);
221
222 if (m_processSettings.totalFrames > 0) m_progress->setValue(100 * progressValue / m_processSettings.totalFrames);
223
224 if (m_process && m_process->state() == QProcess::Running) {
225 QApplication::processEvents();
226 }
227}
228
229bool KisFFMpegWrapper::ffprobeCheckStreamsValid(const QJsonObject& ffprobeJsonObj, const QString& ffprobeSTDERR)
230{
231 // Sanity checks..
232 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(ffprobeJsonObj.contains("streams"), false);
233
234 QRegularExpression invalidStreamRX("(?:Unsupported codec with id .+? for input stream|Could not find codec parameters for stream) ([0-9]+)");
235 QRegularExpressionMatchIterator invalidStreamMatchList = invalidStreamRX.globalMatch(ffprobeSTDERR);
236
237 while (invalidStreamMatchList.hasNext()) {
238 QRegularExpressionMatch invalidStreamMatch = invalidStreamMatchList.next();
239
240 if (invalidStreamMatch.hasMatch()) {
241 const int invalidStreamId = invalidStreamMatch.captured(1).toInt();
242
243 // We make sure we're talking about video streams here.
244 if (ffprobeJsonObj["streams"].toArray()[invalidStreamId].toObject()["codec_type"] == "video") {
245 return false;
246 }
247 }
248 }
249
250 return true;
251}
252
254{
255 if (m_process == nullptr)
256 return;
257
258 m_process->disconnect(this);
259 if (m_process->state() != QProcess::NotRunning) {
260 m_process->kill();
261 }
262 m_process.reset();
263}
264
266{
267 QByteArray stderrRawBuffer = m_process->readAllStandardError();
268
269 Q_EMIT sigReadSTDERR(stderrRawBuffer);
270 m_stderrBuffer += stderrRawBuffer;
271
272 int frameNo = -1;
273 int startPos = 0;
274 int endPos = 0;
275
276 while ((endPos = m_stderrBuffer.indexOf(lineDelimiter, startPos)) != -1) {
277 const QString &line = m_stderrBuffer.mid(startPos, endPos - startPos).trimmed();
278
280
281 Q_EMIT sigReadLine(2,line);
282
283 for (const QString &word : errorWords) {
284 if (line.contains(word)) {
285 m_errorMessage += line % "\n";
286 break;
287 }
288 }
289
290 const QRegularExpressionMatch &match = frameRegexp.match(line);
291
292 if (match.hasMatch()) {
293 frameNo = match.captured(1).toInt();
294 }
295
296
297 dbgFile << "ffmpeg stderr:" << line;
298 startPos = endPos + 1;
299 }
300
301 m_stderrBuffer.remove(0, startPos);
302
303 if (frameNo != -1) {
304 updateProgressDialog(frameNo);
305 Q_EMIT sigProgressUpdated(frameNo);
306 }
307
308}
309
311{
312 QByteArray stdoutRawBuffer = m_process->readAllStandardOutput();
313
314 Q_EMIT sigReadSTDOUT(stdoutRawBuffer);
315 m_stdoutBuffer += stdoutRawBuffer;
316
317
319 if (m_processSettings.storeOutput) m_processSTDOUT += stdoutRawBuffer;
320 } else {
321
322 int startPos = 0;
323 int endPos = 0;
324 QString str;
325
326 if (m_processSettings.storeOutput) m_processSTDOUT += stdoutRawBuffer + "\n";
327
328 // ffmpeg splits normal lines by '\n' and progress data lines by '\r'
329 while ((endPos = m_stdoutBuffer.indexOf(lineDelimiter, startPos)) != -1) {
330 const QString &line = m_stdoutBuffer.mid(startPos, endPos - startPos).trimmed();
331
332 dbgFile << "ffmpeg stdout:" << line;
333 Q_EMIT sigReadLine(1,line);
334 startPos = endPos + 1;
335 }
336
337 m_stdoutBuffer.remove(0, startPos);
338 }
339
340
341}
342
344{
345 dbgFile << "ffmpeg process started!";
346
347 Q_EMIT sigStarted();
348}
349
351{
352 dbgFile << "FFMpeg finished with code" << exitCode;
354 m_progress->setValue(100);
355 }
356
357 if (exitCode != 0) {
358 m_errorMessage.remove(junkRegex);
359 if (m_process->exitStatus() == QProcess::CrashExit) {
360 m_errorMessage = i18n("FFMpeg Crashed") % "\n" % m_errorMessage;
361 }
362
364 } else {
365 Q_EMIT sigFinished();
366 }
367}
368
369void KisFFMpegWrapper::fixUpNonEmbeddedProcessEnvironment(const QString &processPath, QProcess &process)
370{
371#ifdef Q_OS_LINUX
372
381 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
382
383 const QStringList libraryPaths = env.value("LD_LIBRARY_PATH").split(':');
384 const QString processAbsPath = QFileInfo(QFileInfo(processPath).absolutePath() + "/../").absoluteFilePath();
385
386 bool isCustomBuildOfFFmpeg = false;
387
388 Q_FOREACH (const QString &path, libraryPaths) {
389 const QString absPath1 = QFileInfo(path + "/").absoluteFilePath();
390 const QString absPath2 = QFileInfo(path + "/../").absoluteFilePath();
391
392 if (absPath1 == processAbsPath || absPath2 == processAbsPath) {
393 dbgFile << "Detected embedded ffmpeg:" << processPath;
394 dbgFile << " " << ppVar(processAbsPath);
395 dbgFile << " " << ppVar(absPath1);
396 dbgFile << " " << ppVar(absPath2);
397
398 isCustomBuildOfFFmpeg = true;
399
400 break;
401 }
402 }
403
404 if (!isCustomBuildOfFFmpeg) {
405 dbgFile << "Removing LD_LIBRARY_PATH for running" << processPath;
406
407 env.remove("LD_LIBRARY_PATH");
408 process.setProcessEnvironment(env);
409 }
410#else
411 Q_UNUSED(processPath);
412 Q_UNUSED(process);
413#endif /* Q_OS_LINUX */
414}
415
416QByteArray KisFFMpegWrapper::runProcessAndReturn(const QString &processPath, const QStringList &args, int msecs)
417{
418 QProcess runProcess;
419
420 fixUpNonEmbeddedProcessEnvironment(processPath, runProcess);
421
422 runProcess.start(processPath, args);
423
424 if (runProcess.waitForStarted(msecs)) runProcess.waitForFinished(msecs);
425
426 const bool successfulStart =
427 runProcess.state() == QProcess::NotRunning &&
428 runProcess.error() == QProcess::UnknownError;
429
430 dbgFile << "runProcessAndReturn Success:" << successfulStart;
431
432 if (successfulStart) return runProcess.readAllStandardOutput();
433
434 return "";
435}
436
438{
439 KisConfig cfg(true);
440 return cfg.ffmpegLocation();
441}
442
444{
445 KisConfig cfg(false);
446 cfg.setFFMpegLocation(location);
447}
448
449QJsonObject KisFFMpegWrapper::findProcessPath(const QString &processName, const QString &customLocation, bool includeProcessInfo)
450{
451 QJsonObject resultJsonObj;
452 QStringList proposedPaths;
453
454 // User-specified..
455 if (!customLocation.isEmpty()) {
456 proposedPaths << customLocation;
457 proposedPaths << customLocation + '/' + processName;
458 }
459
460 // Krita-bundled..
461 proposedPaths << KoResourcePaths::getApplicationRoot() + '/' + "bin" + '/' + processName;
462
463 // OS-specific..
464#ifdef Q_OS_WIN
465 // Look for winget-installed ffmpeg packages in C:\users\USERNAME\appdata\local\microsoft\winget\packages\FFMPEGVERSION\bin
466 QDir wingetPackageDir = QDir(QDir::homePath() + "/AppData/Local/Microsoft/WinGet/Packages/");
467 QStringList wingetPackageFolderNames = wingetPackageDir.entryList();
468 for (const auto &folderName : wingetPackageFolderNames) {
469 if (folderName.contains("ffmpeg", Qt::CaseInsensitive)) {
470 QDir ffmpegPackageFolder = QDir(wingetPackageDir.path() + "/" + folderName);
471 QStringList ffmpegRootNames = ffmpegPackageFolder.entryList();
472 for (const auto &ffmpegRootName : ffmpegRootNames) {
473 proposedPaths << ffmpegPackageFolder.path() + "/" + ffmpegRootName + "/bin/";
474 }
475 }
476 }
477#endif
478
479#ifdef Q_OS_MACOS
480 proposedPaths << QCoreApplication::applicationDirPath() + '/' + processName;
481#endif
482
483#ifndef Q_OS_WIN
484 proposedPaths << QDir::homePath() + "/bin/" + processName;
485 proposedPaths << "/usr/bin/" + processName;
486 proposedPaths << "/usr/local/bin/" + processName;
487#endif
488
489 dbgFile << proposedPaths;
490 for (int i = 0; i != proposedPaths.size(); ++i) {
491 if (proposedPaths[i].isEmpty()) continue;
492
493#ifdef Q_OS_WIN
494 proposedPaths[i] = QDir::toNativeSeparators(QDir::cleanPath(proposedPaths[i]));
495
496 if (proposedPaths[i].endsWith('/')) {
497 continue;
498 }
499
500 if (!proposedPaths[i].endsWith(".exe", Qt::CaseInsensitive)) {
501 if (!QFile::exists(proposedPaths[i])) {
502 proposedPaths[i] += ".exe";
503 if (!QFile::exists(proposedPaths[i])) {
504 continue;
505 }
506 }
507 }
508#endif
509
510 QJsonObject processInfoJsonObj = findProcessInfo(processName, proposedPaths[i], includeProcessInfo);
511 dbgFile << "PATH" << proposedPaths[i] << processInfoJsonObj.value("enabled").toBool();
512 if (processInfoJsonObj.value("enabled").toBool()) {
513 processInfoJsonObj["custom"]=(!customLocation.isEmpty() && i <= 1) ? true:false;
514 resultJsonObj = processInfoJsonObj;
515 break;
516 }
517 }
518
519 return resultJsonObj;
520}
521
522QJsonObject KisFFMpegWrapper::findProcessInfo(const QString &processName, const QString &rawProcessPath, bool includeProcessInfo)
523{
524
525 QJsonObject ffmpegInfo {{"path", rawProcessPath},
526 {"enabled",false},
527 {"version", "0"},
528 {"encoder",QJsonValue::Object},
529 {"decoder",QJsonValue::Object}};
530
531 if (!QFile::exists(rawProcessPath)) return ffmpegInfo;
532
533 const QString processPath = QFileInfo(rawProcessPath).absoluteFilePath();
534 ffmpegInfo["path"] = processPath;
535
536 dbgFile << "Found process at:" << processPath;
537 QString processVersion = KisFFMpegWrapper::runProcessAndReturn(processPath, QStringList() << "-version", FFMPEG_TIMEOUT);
538
539 if (!processVersion.isEmpty()) {
540
541 QRegularExpressionMatch versionMatch = ffmpegVersionRX.match(processVersion);
542
543 if (versionMatch.hasMatch() && versionMatch.captured(1) == processName ) {
544 ffmpegInfo["version"] = versionMatch.captured(2);
545 dbgFile << "found version" << ffmpegInfo.value("version").toString();
546 ffmpegInfo["enabled"] = true;
547 }
548
549 if (!includeProcessInfo || !ffmpegInfo["enabled"].toBool()) return ffmpegInfo;
550
551 QString processCodecs = KisFFMpegWrapper::runProcessAndReturn(processPath, QStringList() << "-codecs", FFMPEG_TIMEOUT);
552
553 QJsonObject codecsJson {};
554
555 {
556 // For regular expression advice, check out https://regexr.com/.
557 // Beware: We need double backslashes here for C++, in regular regex it would be a single backslash instead.
558 QRegularExpression ffmpegCodecsRX("(D|\\.)(E|\\.)....\\s+(.+?)\\s+([^\\r\\n]*)");
559 QRegularExpressionMatchIterator codecsMatchList = ffmpegCodecsRX.globalMatch(processCodecs);
560
561 // Find out codec types.. (e.g. H264, VP9, etc)
562 while (codecsMatchList.hasNext()) {
563 QRegularExpressionMatch codecsMatch = codecsMatchList.next();
564
565 if (codecsMatch.hasMatch()) {
566 QJsonObject codecInfoJson {};
567
568 bool encodingSupported = codecsMatch.captured(2) == "E" ? true:false;
569 bool decodingSupported = codecsMatch.captured(1) == "D" ? true:false;
570 QString codecName = codecsMatch.captured(3);
571 QString codecRemainder = codecsMatch.captured(4);
572
573 codecInfoJson.insert("encoding", encodingSupported);
574 codecInfoJson.insert("decoding", decodingSupported);
575
576
577 // Regular expression for grouping specific encoders and decoders..
578 QRegularExpression ffmpegSpecificCodecRX("\\(decoders:(.+?)\\)|\\(encoders:(.+?)\\)");
579 QRegularExpressionMatchIterator specificCodecMatchList = ffmpegSpecificCodecRX.globalMatch(codecRemainder);
580
581 QJsonArray encodersList;
582 QJsonArray decodersList;
583
584 while (specificCodecMatchList.hasNext()) {
585 QRegularExpressionMatch specificCodecMatch = specificCodecMatchList.next();
586 if (specificCodecMatch.hasMatch()) {
587 // Add specific decoders..
588 QStringList decoders = specificCodecMatch.captured(1).split(" ");
589 Q_FOREACH(const QString& string, decoders) {
590 if (!string.isEmpty()) {
591 decodersList.push_back(string);
592 }
593 }
594
595 // Add specific encoders.. (e.g. for h264: h264_vaapi, libopenh264, etc )
596 QStringList encoders = specificCodecMatch.captured(2).split(" ");
597 Q_FOREACH(const QString& string, encoders) {
598 if (!string.isEmpty()) {
599 encodersList.push_back(string);
600 }
601 }
602 }
603 }
604
605 codecInfoJson.insert("encoders", encodersList);
606 codecInfoJson.insert("decoders", decodersList);
607 codecsJson.insert(codecName, codecInfoJson);
608 }
609 }
610 }
611
612 ffmpegInfo.insert("codecs", codecsJson);
613
614
615 dbgFile << "codec support:" << ffmpegInfo;
616 } else {
617 dbgFile << "Not a valid process at:" << processPath;
618 }
619
620 return ffmpegInfo;
621
622}
623
624QStringList KisFFMpegWrapper::getSupportedCodecs(const QJsonObject& ffmpegProcessInfo) {
625 // TODO: I really don't like having to deal with JSON in C++ code as frequently as we are here.
626 // We should make a proper datatype for FFMPEG Process Information!
627
628 QStringList encodersToReturn = {};
629
630 // For now, I'm just treating this as strictly typed using KIS_SAFE_ASSERT_RECOVER_RETURN
631 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(ffmpegProcessInfo["enabled"].toBool(), encodersToReturn);
632
633 QJsonObject encoders = ffmpegProcessInfo["codecs"].toObject();
634 Q_FOREACH( const QString& key, encoders.keys()) {
635 if (encoders[key].toObject()["encoding"].toBool()) {
636 encodersToReturn << key;
637 }
638 }
639
640 return encodersToReturn;
641}
642
643QJsonObject KisFFMpegWrapper::findFFMpeg(const QString &customLocation)
644{
645 return findProcessPath("ffmpeg", customLocation, true);
646}
647
648QJsonObject KisFFMpegWrapper::findFFProbe(const QString &customLocation)
649{
650 return findProcessPath("ffprobe", customLocation, false);
651}
652
653QJsonObject KisFFMpegWrapper::ffprobe(const QString &inputFile, const QString &ffprobePath)
654{
655 struct KisFFMpegWrapperSettings ffprobeSettings;
656
657 ffprobeSettings.processPath = ffprobePath;
658 ffprobeSettings.storeOutput = true;
659 ffprobeSettings.defaultPrependArgs.clear();
660
661 ffprobeSettings.args << "-hide_banner"
662 << "-v" << "warning"
663 << "-of" << "json=compact=1"
664 << "-show_format"
665 << "-show_streams"
666 << "-i" << inputFile;
667 startNonBlocking(ffprobeSettings);
669
670 QString ffprobeSTDOUT = m_processSTDOUT;
671 QString ffprobeSTDERR = m_processSTDERR;
672
673 QJsonDocument ffprobeJsonDoc = QJsonDocument::fromJson(ffprobeSTDOUT.toUtf8());
674 QJsonObject ffprobeJsonObj;
675
676 if (ffprobeJsonDoc.isNull() || !ffprobeJsonDoc.isObject()) {
677 ffprobeJsonObj["error"] = FFProbeErrorCodes::INVALID_JSON;
678 return ffprobeJsonObj;
679 }
680
681 ffprobeJsonObj = ffprobeJsonDoc.object();
682
683 const bool hasValidStreams = ffprobeCheckStreamsValid(ffprobeJsonObj, ffprobeSTDERR);
684 ffprobeJsonObj["error"] = hasValidStreams ? FFProbeErrorCodes::NONE : FFProbeErrorCodes::UNSUPPORTED_CODEC;
685
686
687 return ffprobeJsonObj;
688}
689
690QJsonObject KisFFMpegWrapper::ffmpegProbe(const QString &inputFile, const QString &ffmpegPath, bool batchMode)
691{
692 struct KisFFMpegWrapperSettings ffmpegSettings;
693
694 ffmpegSettings.processPath = ffmpegPath;
695 ffmpegSettings.storeOutput = true;
696 ffmpegSettings.progressMessage = i18nc("Video information probing dialog. arg1: frame number.", "Loading video data... %1 frames examined.", "[progress]");
697 ffmpegSettings.batchMode = batchMode;
698
699 ffmpegSettings.args << "-stats"
700 << "-v" << "info"
701 << "-progress" << "pipe:1"
702 << "-map" << "0:v:0"
703 << "-c" << "copy"
704 << "-f" << "null" << "pipe:1"
705 << "-i" << inputFile;
706
707
708 this->startNonBlocking(ffmpegSettings);
709 this->waitForFinished();
710
711
712 QString ffmpegSTDOUT = m_processSTDERR + "\n" + m_processSTDOUT;
713
714 dbgFile << "ffmpegProbe stdout:" << ffmpegSTDOUT;
715
716 QJsonObject ffmpegJsonObj;
717 QJsonArray ffmpegStreamsJsonArr;
718 QJsonObject ffmpegFormatJsonObj;
719 QJsonObject ffmpegProgressJsonObj;
720
721 QStringList stdoutLines = ffmpegSTDOUT.split('\n');
722
723 ffmpegJsonObj["error"] = FFProbeErrorCodes::UNSUPPORTED_CODEC;
724
725 for (const QString &line : stdoutLines) {
726 dbgFile << "ffmpeg probe stdout" << line;
727 QRegularExpression videoInfoInputRX("Duration: (\\d+):(\\d+):([\\d\\.]+),");
728 QRegularExpressionMatch durationMatch = videoInfoInputRX.match(line);
729
730 if (ffmpegFormatJsonObj.value("duration").isUndefined() && durationMatch.hasMatch()) {
731 ffmpegFormatJsonObj["duration"] = QString::number( (durationMatch.captured(1).toInt() * 60 * 60)
732 + (durationMatch.captured(2).toInt() * 60)
733 + durationMatch.captured(3).toFloat()
734 );
735 } else {
736 QRegularExpression videoInfoStreamRX(
737 QString("Stream #(\\d+):(\\d+)(?:[ ]*?\\((\\w+)\\)|): Video: (\\w+?)(?:[ ]*?\\((.+?)\\)|),[ ]*?")
738 .append("(\\w+?)(?:[ ]*?\\((.+?)\\)|),[ ]*?")
739 .append("(\\d+)x(\\d+)([ ]*?\\[.+?\\]|.*?),[ ]*?")
740 .append("(?:([\\d\\.]+) fps,|) (\\S+?) tbr, (.+?) tbn, (.+?) tbc")
741 );
742
743 QRegularExpressionMatch streamMatch = videoInfoStreamRX.match(line);
744
745 if (streamMatch.hasMatch() && ffmpegStreamsJsonArr[streamMatch.captured(1).toInt()].isUndefined() ) {
746 int index = streamMatch.captured(1).toInt();
747 QJsonObject ffmpegJsonOnStreamObj;
748
749 ffmpegJsonOnStreamObj["index"] = index;
750 ffmpegJsonOnStreamObj["codec_name"] = streamMatch.captured(4);
751 ffmpegJsonOnStreamObj["profile"] = streamMatch.captured(5);
752 ffmpegJsonOnStreamObj["pix_fmt"] = streamMatch.captured(6);
753 ffmpegJsonOnStreamObj["width"] = streamMatch.captured(8).toInt();
754 ffmpegJsonOnStreamObj["height"] = streamMatch.captured(9).toInt();
755
756 ffmpegJsonOnStreamObj["codec_type"] = "video";
757
758 if (streamMatch.captured(11).toFloat() > 0) {
759 float fps = streamMatch.captured(11).toFloat();
760
761 ffmpegProgressJsonObj["ffmpeg_fps"] = QString::number(fps);
762 ffmpegJsonOnStreamObj["r_frame_rate"] = QString::number( fps * 10000 ).append(QString("/10000"));
763 } else {
764 ffmpegProgressJsonObj["ffmpeg_fps"] = 0;
765 }
766
767 ffmpegJsonObj["error"] = FFProbeErrorCodes::NONE;
768
769 dbgFile << "ffmpegProbe stream:" << ffmpegJsonOnStreamObj;
770
771 ffmpegStreamsJsonArr.insert(index, ffmpegJsonOnStreamObj);
772
773 } else {
774
775 QRegularExpression videoInfoFrameRX("^(\\w+?)=([\\w\\./:]+?)$");
776 QRegularExpressionMatch frameMatch = videoInfoFrameRX.match(line);
777
778 if (frameMatch.hasMatch()) ffmpegProgressJsonObj[frameMatch.captured(1)] = frameMatch.captured(2);
779
780 }
781 }
782 }
783
784 ffmpegJsonObj.insert("streams",ffmpegStreamsJsonArr);
785 ffmpegJsonObj.insert("format",ffmpegFormatJsonObj);
786 ffmpegJsonObj.insert("progress",ffmpegProgressJsonObj);
787
788 return ffmpegJsonObj;
789}
790
792{
793 if (name == "bt709") {
795 }
796 if (name == "bt470m") {
798 }
799 if (name == "bt470bg") {
801 }
802 if (name == "smpte170m") {
804 }
805 if (name == "smpte240m") {
807 }
808 if (name == "film") {
810 }
811 if (name == "bt2020") {
813 }
814 if (name.startsWith("smpte428")) {
816 }
817 if (name == "smpte431") {
819 }
820 if (name == "smpte432") {
822 }
823 if (name == "jedec-p22") {
825 }
826
828}
829
831{
832 if (name == "bt709") {
833 return TRC_ITU_R_BT_709_5;
834 }
835 if (name == "gamma22") {
837 }
838 if (name == "gamma28") {
840 }
841 if (name == "smpte170m") {
842 return TRC_ITU_R_BT_601_6;
843 }
844 if (name == "smpte240m") {
845 return TRC_SMPTE_240M;
846 }
847 if (name == "linear") {
848 return TRC_LINEAR;
849 }
850 if (name == "log" || name == "log100") {
851 return TRC_LOGARITHMIC_100;
852 }
853 if (name == "log316" || name == "log_sqrt") {
855 }
856 if (name == "iec61966_2_4" || name == "iec61966-2-4") {
857 return TRC_IEC_61966_2_4;
858 }
859 if (name.startsWith("bt1361")) {
860 return TRC_ITU_R_BT_1361;
861 }
862 if (name == "iec61966_2_1" || name == "iec61966-2-1") {
863 return TRC_IEC_61966_2_1;
864 }
865 if (name.startsWith("bt2020_10")) {
867 }
868 if (name.startsWith("bt2020_12")) {
870 }
871 if (name == "smpte2084") {
873 }
874 if (name == "smpte240m") {
875 return TRC_SMPTE_240M;
876 }
877 if (name.startsWith("smpte428")) {
878 return TRC_SMPTE_ST_428_1;
879 }
880 if (name == "arib-std-b67") {
882 }
883
884 return TRC_UNSPECIFIED;
885}
@ INVALID_JSON
@ UNSUPPORTED_CODEC
@ NONE
const int FFMPEG_TIMEOUT
QList< QString > QStringList
ColorPrimaries
The colorPrimaries enum Enum of colorants, follows ITU H.273 for values 0 to 255, and has extra known...
@ PRIMARIES_ITU_R_BT_2020_2_AND_2100_0
@ PRIMARIES_ITU_R_BT_470_6_SYSTEM_M
@ PRIMARIES_EBU_Tech_3213_E
@ PRIMARIES_UNSPECIFIED
@ PRIMARIES_ITU_R_BT_470_6_SYSTEM_B_G
@ PRIMARIES_SMPTE_240M
@ PRIMARIES_ITU_R_BT_601_6
@ PRIMARIES_SMPTE_RP_431_2
@ PRIMARIES_GENERIC_FILM
@ PRIMARIES_SMPTE_EG_432_1
@ PRIMARIES_ITU_R_BT_709_5
@ PRIMARIES_SMPTE_ST_428_1
TransferCharacteristics
The transferCharacteristics enum Enum of transfer characteristics, follows ITU H.273 for values 0 to ...
@ TRC_IEC_61966_2_4
@ TRC_ITU_R_BT_2020_2_10bit
@ TRC_LOGARITHMIC_100
@ TRC_ITU_R_BT_470_6_SYSTEM_M
@ TRC_ITU_R_BT_470_6_SYSTEM_B_G
@ TRC_ITU_R_BT_1361
@ TRC_ITU_R_BT_2100_0_HLG
@ TRC_ITU_R_BT_2100_0_PQ
@ TRC_ITU_R_BT_601_6
@ TRC_IEC_61966_2_1
@ TRC_ITU_R_BT_709_5
@ TRC_SMPTE_ST_428_1
@ TRC_LOGARITHMIC_100_sqrt10
@ TRC_ITU_R_BT_2020_2_12bit
void setFFMpegLocation(const QString &value)
QString ffmpegLocation(bool defaultValue=false) const
KisFFMpegWrapperSettings m_processSettings
void startNonBlocking(const KisFFMpegWrapperSettings &settings)
KisFFMpegWrapper(QObject *parent=nullptr)
static TransferCharacteristics transferCharacteristicsFromName(QString name)
void updateProgressDialog(int progressValue)
QSharedPointer< QProgressDialog > m_progress
static QJsonObject findFFProbe(const QString &customLocation)
void sigProgressUpdated(int frameNo)
void sigReadLine(int pipe, QString line)
QByteArray m_processSTDOUT
QJsonObject ffprobe(const QString &inputFile, const QString &ffprobePath)
static void fixUpNonEmbeddedProcessEnvironment(const QString &processPath, QProcess &process)
static QJsonObject findProcessInfo(const QString &processName, const QString &processPath, bool includeProcessInfo)
void sigReadSTDOUT(QByteArray stdoutBuffer)
bool ffprobeCheckStreamsValid(const QJsonObject &ffprobeJsonObj, const QString &ffprobeSTDERR)
ffprobeCheckStreamsValid
void sigReadSTDERR(QByteArray stderrBuffer)
static QString configuredFFMpegLocation()
static QJsonObject findProcessPath(const QString &processName, const QString &customLocation, bool processInfo)
QJsonObject ffmpegProbe(const QString &inputFile, const QString &ffmpegPath, bool batchMode)
static QStringList getSupportedCodecs(const QJsonObject &ffmpegJsonProcessInput)
KisImportExportErrorCode start(const KisFFMpegWrapperSettings &settings)
void sigFinishedWithError(QString message)
static ColorPrimaries colorPrimariesFromName(QString name)
QScopedPointer< QProcess > m_process
void slotFinished(int exitCode)
static QJsonObject findFFMpeg(const QString &customLocation)
bool waitForFinished(int msecs=FFMPEG_TIMEOUT)
static QByteArray runProcessAndReturn(const QString &processPath, const QStringList &args, int msecs=FFMPEG_TIMEOUT)
static void setConfiguredFFMpegLocation(QString &location)
static QString getApplicationRoot()
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
#define ppVar(var)
Definition kis_debug.h:155
#define dbgFile
Definition kis_debug.h:53
QSharedPointer< T > toQShared(T *ptr)