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 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
114 QString progressText = m_processSettings.progressMessage;
115
116 progressText.replace("[progress]", "0");
117
118 m_progress = toQShared(new QProgressDialog(progressText, "", 0, 0));
119
120 m_progress->setWindowModality(Qt::ApplicationModal);
121 m_progress->setCancelButton(0);
122 m_progress->setMinimumDuration(0);
123 m_progress->setValue(0);
124
125 if (settings.progressIndeterminate) {
126 m_progress->setRange(0,0);
127 } else {
128 m_progress->setRange(0, 100);
129 }
130
131 connect(m_progress.data(), SIGNAL(canceled()), m_process.data(), SLOT(kill()));
132
133 m_progress->show();
134
135 dbgFile << "Open progress dialog!";
136 }
137
138 connect(m_process.data(), SIGNAL(readyReadStandardOutput()), SLOT(slotReadyReadSTDOUT()));
139 connect(m_process.data(), SIGNAL(readyReadStandardError()), SLOT(slotReadyReadSTDERR()));
140 connect(m_process.data(), SIGNAL(started()), SLOT(slotStarted()));
141 connect(m_process.data(), SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(slotFinished(int)));
142
143 QStringList args;
144
145 if ( !settings.defaultPrependArgs.isEmpty() ) {
146 args << settings.defaultPrependArgs;
147 }
148
149 args << settings.args;
150
151 if ( !settings.outputFile.isEmpty() ) {
152 args << settings.outputFile;
153 }
154
155 dbgFile << "starting process: " << qUtf8Printable(settings.processPath) << args;
156
158
159 m_process->start(settings.processPath, args);
160}
161
163{
164 struct ProcessResults {
165 bool finish = false;
166 QString error = QString();
167 };
168
169 QSharedPointer<ProcessResults> processResults = toQShared(new ProcessResults);
170
171 connect( this, &KisFFMpegWrapper::sigFinishedWithError, [processResults](const QString& errMsg){
172 processResults->finish = true;
173 processResults->error = errMsg;
174 });
175
176 connect( this, &KisFFMpegWrapper::sigFinished, [processResults](){
177 processResults->finish = true;
178 });
179
180 startNonBlocking(settings);
182
183 if (processResults->finish == true) {
184 if (processResults->error.isEmpty()) {
186 } else {
188 }
189 } else {
190 reset(); // Ensure process isn't running before returning failure here.
192 }
193}
194
196{
197 if (!m_process) return false;
198
199 if (m_process->waitForStarted(msecs)) {
200 return (m_process->waitForFinished(msecs) && m_process->exitStatus() == QProcess::ExitStatus::NormalExit);
201 }
202
203 return false;
204}
205
207
208 dbgFile << "Update Progress" << progressValue << "/" << m_processSettings.totalFrames;
209
210 if (!m_progress) return;
211
212 QString progressText = m_processSettings.progressMessage;
213 QStringList outputFileParts = m_processSettings.outputFile.split(".");
214 QString suffix = outputFileParts.size() == 2 ? outputFileParts[1] : m_processSettings.outputFile;
215
216 progressText.replace("[progress]", QString::number(progressValue));
217 progressText.replace("[framecount]", QString::number(m_processSettings.totalFrames));
218 progressText.replace("[suffix]", suffix );
219 m_progress->setLabelText(progressText);
220
221 if (m_processSettings.totalFrames > 0) m_progress->setValue(100 * progressValue / m_processSettings.totalFrames);
222
223 if (m_process && m_process->state() == QProcess::Running) {
224 QApplication::processEvents();
225 }
226}
227
228bool KisFFMpegWrapper::ffprobeCheckStreamsValid(const QJsonObject& ffprobeJsonObj, const QString& ffprobeSTDERR)
229{
230 // Sanity checks..
231 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(ffprobeJsonObj.contains("streams"), false);
232
233 QRegularExpression invalidStreamRX("(?:Unsupported codec with id .+? for input stream|Could not find codec parameters for stream) ([0-9]+)");
234 QRegularExpressionMatchIterator invalidStreamMatchList = invalidStreamRX.globalMatch(ffprobeSTDERR);
235
236 while (invalidStreamMatchList.hasNext()) {
237 QRegularExpressionMatch invalidStreamMatch = invalidStreamMatchList.next();
238
239 if (invalidStreamMatch.hasMatch()) {
240 const int invalidStreamId = invalidStreamMatch.captured(1).toInt();
241
242 // We make sure we're talking about video streams here.
243 if (ffprobeJsonObj["streams"].toArray()[invalidStreamId].toObject()["codec_type"] == "video") {
244 return false;
245 }
246 }
247 }
248
249 return true;
250}
251
253{
254 if (m_process == nullptr)
255 return;
256
257 m_process->disconnect(this);
258 if (m_process->state() != QProcess::NotRunning) {
259 m_process->kill();
260 }
261 m_process.reset();
262}
263
265{
266 QByteArray stderrRawBuffer = m_process->readAllStandardError();
267
268 Q_EMIT sigReadSTDERR(stderrRawBuffer);
269 m_stderrBuffer += stderrRawBuffer;
270
271 int frameNo = -1;
272 int startPos = 0;
273 int endPos = 0;
274
275 while ((endPos = m_stderrBuffer.indexOf(lineDelimiter, startPos)) != -1) {
276 const QString &line = m_stderrBuffer.mid(startPos, endPos - startPos).trimmed();
277
279
280 Q_EMIT sigReadLine(2,line);
281
282 for (const QString &word : errorWords) {
283 if (line.contains(word)) {
284 m_errorMessage += line % "\n";
285 break;
286 }
287 }
288
289 const QRegularExpressionMatch &match = frameRegexp.match(line);
290
291 if (match.hasMatch()) {
292 frameNo = match.captured(1).toInt();
293 }
294
295
296 dbgFile << "ffmpeg stderr:" << line;
297 startPos = endPos + 1;
298 }
299
300 m_stderrBuffer.remove(0, startPos);
301
302 if (frameNo != -1) {
303 updateProgressDialog(frameNo);
304 Q_EMIT sigProgressUpdated(frameNo);
305 }
306
307}
308
310{
311 QByteArray stdoutRawBuffer = m_process->readAllStandardOutput();
312
313 Q_EMIT sigReadSTDOUT(stdoutRawBuffer);
314 m_stdoutBuffer += stdoutRawBuffer;
315
316
318 if (m_processSettings.storeOutput) m_processSTDOUT += stdoutRawBuffer;
319 } else {
320
321 int startPos = 0;
322 int endPos = 0;
323 QString str;
324
325 if (m_processSettings.storeOutput) m_processSTDOUT += stdoutRawBuffer + "\n";
326
327 // ffmpeg splits normal lines by '\n' and progress data lines by '\r'
328 while ((endPos = m_stdoutBuffer.indexOf(lineDelimiter, startPos)) != -1) {
329 const QString &line = m_stdoutBuffer.mid(startPos, endPos - startPos).trimmed();
330
331 dbgFile << "ffmpeg stdout:" << line;
332 Q_EMIT sigReadLine(1,line);
333 startPos = endPos + 1;
334 }
335
336 m_stdoutBuffer.remove(0, startPos);
337 }
338
339
340}
341
343{
344 dbgFile << "ffmpeg process started!";
345
346 Q_EMIT sigStarted();
347}
348
350{
351 dbgFile << "FFMpeg finished with code" << exitCode;
353 m_progress->setValue(100);
354 }
355
356 if (exitCode != 0) {
357 m_errorMessage.remove(junkRegex);
358 if (m_process->exitStatus() == QProcess::CrashExit) {
359 m_errorMessage = i18n("FFMpeg Crashed") % "\n" % m_errorMessage;
360 }
361
363 } else {
364 Q_EMIT sigFinished();
365 }
366}
367
368void KisFFMpegWrapper::fixUpNonEmbeddedProcessEnvironment(const QString &processPath, QProcess &process)
369{
370#ifdef Q_OS_LINUX
371
380 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
381
382 const QStringList libraryPaths = env.value("LD_LIBRARY_PATH").split(':');
383 const QString processAbsPath = QFileInfo(QFileInfo(processPath).absolutePath() + "/../").absoluteFilePath();
384
385 bool isCustomBuildOfFFmpeg = false;
386
387 Q_FOREACH (const QString &path, libraryPaths) {
388 const QString absPath1 = QFileInfo(path + "/").absoluteFilePath();
389 const QString absPath2 = QFileInfo(path + "/../").absoluteFilePath();
390
391 if (absPath1 == processAbsPath || absPath2 == processAbsPath) {
392 dbgFile << "Detected embedded ffmpeg:" << processPath;
393 dbgFile << " " << ppVar(processAbsPath);
394 dbgFile << " " << ppVar(absPath1);
395 dbgFile << " " << ppVar(absPath2);
396
397 isCustomBuildOfFFmpeg = true;
398
399 break;
400 }
401 }
402
403 if (!isCustomBuildOfFFmpeg) {
404 dbgFile << "Removing LD_LIBRARY_PATH for running" << processPath;
405
406 env.remove("LD_LIBRARY_PATH");
407 process.setProcessEnvironment(env);
408 }
409#else
410 Q_UNUSED(processPath);
411 Q_UNUSED(process);
412#endif /* Q_OS_LINUX */
413}
414
415QByteArray KisFFMpegWrapper::runProcessAndReturn(const QString &processPath, const QStringList &args, int msecs)
416{
417 QProcess runProcess;
418
419 fixUpNonEmbeddedProcessEnvironment(processPath, runProcess);
420
421 runProcess.start(processPath, args);
422
423 if (runProcess.waitForStarted(msecs)) runProcess.waitForFinished(msecs);
424
425 const bool successfulStart =
426 runProcess.state() == QProcess::NotRunning &&
427 runProcess.error() == QProcess::UnknownError;
428
429 dbgFile << "runProcessAndReturn Success:" << successfulStart;
430
431 if (successfulStart) return runProcess.readAllStandardOutput();
432
433 return "";
434}
435
437{
438 KisConfig cfg(true);
439 return cfg.ffmpegLocation();
440}
441
443{
444 KisConfig cfg(false);
445 cfg.setFFMpegLocation(location);
446}
447
448QJsonObject KisFFMpegWrapper::findProcessPath(const QString &processName, const QString &customLocation, bool includeProcessInfo)
449{
450 QJsonObject resultJsonObj;
451 QStringList proposedPaths;
452
453 // User-specified..
454 if (!customLocation.isEmpty()) {
455 proposedPaths << customLocation;
456 proposedPaths << customLocation + '/' + processName;
457 }
458
459 // Krita-bundled..
460 proposedPaths << KoResourcePaths::getApplicationRoot() + '/' + "bin" + '/' + processName;
461
462 // OS-specific..
463#ifdef Q_OS_WIN
464 // Look for winget-installed ffmpeg packages in C:\users\USERNAME\appdata\local\microsoft\winget\packages\FFMPEGVERSION\bin
465 QDir wingetPackageDir = QDir(QDir::homePath() + "/AppData/Local/Microsoft/WinGet/Packages/");
466 QStringList wingetPackageFolderNames = wingetPackageDir.entryList();
467 for (const auto &folderName : wingetPackageFolderNames) {
468 if (folderName.contains("ffmpeg", Qt::CaseInsensitive)) {
469 QDir ffmpegPackageFolder = QDir(wingetPackageDir.path() + "/" + folderName);
470 QStringList ffmpegRootNames = ffmpegPackageFolder.entryList();
471 for (const auto &ffmpegRootName : ffmpegRootNames) {
472 proposedPaths << ffmpegPackageFolder.path() + "/" + ffmpegRootName + "/bin/";
473 }
474 }
475 }
476#endif
477
478#ifdef Q_OS_MACOS
479 proposedPaths << QCoreApplication::applicationDirPath() + '/' + processName;
480#endif
481
482#ifndef Q_OS_WIN
483 proposedPaths << QDir::homePath() + "/bin/" + processName;
484 proposedPaths << "/usr/bin/" + processName;
485 proposedPaths << "/usr/local/bin/" + processName;
486#endif
487
488 dbgFile << proposedPaths;
489 for (int i = 0; i != proposedPaths.size(); ++i) {
490 if (proposedPaths[i].isEmpty()) continue;
491
492#ifdef Q_OS_WIN
493 proposedPaths[i] = QDir::toNativeSeparators(QDir::cleanPath(proposedPaths[i]));
494
495 if (proposedPaths[i].endsWith('/')) {
496 continue;
497 }
498
499 if (!proposedPaths[i].endsWith(".exe", Qt::CaseInsensitive)) {
500 if (!QFile::exists(proposedPaths[i])) {
501 proposedPaths[i] += ".exe";
502 if (!QFile::exists(proposedPaths[i])) {
503 continue;
504 }
505 }
506 }
507#endif
508
509 QJsonObject processInfoJsonObj = findProcessInfo(processName, proposedPaths[i], includeProcessInfo);
510 dbgFile << "PATH" << proposedPaths[i] << processInfoJsonObj.value("enabled").toBool();
511 if (processInfoJsonObj.value("enabled").toBool()) {
512 processInfoJsonObj["custom"]=(!customLocation.isEmpty() && i <= 1) ? true:false;
513 resultJsonObj = processInfoJsonObj;
514 break;
515 }
516 }
517
518 return resultJsonObj;
519}
520
521QJsonObject KisFFMpegWrapper::findProcessInfo(const QString &processName, const QString &rawProcessPath, bool includeProcessInfo)
522{
523
524 QJsonObject ffmpegInfo {{"path", rawProcessPath},
525 {"enabled",false},
526 {"version", "0"},
527 {"encoder",QJsonValue::Object},
528 {"decoder",QJsonValue::Object}};
529
530 if (!QFile::exists(rawProcessPath)) return ffmpegInfo;
531
532 const QString processPath = QFileInfo(rawProcessPath).absoluteFilePath();
533 ffmpegInfo["path"] = processPath;
534
535 dbgFile << "Found process at:" << processPath;
536 QString processVersion = KisFFMpegWrapper::runProcessAndReturn(processPath, QStringList() << "-version", FFMPEG_TIMEOUT);
537
538 if (!processVersion.isEmpty()) {
539
540 QRegularExpressionMatch versionMatch = ffmpegVersionRX.match(processVersion);
541
542 if (versionMatch.hasMatch() && versionMatch.captured(1) == processName ) {
543 ffmpegInfo["version"] = versionMatch.captured(2);
544 dbgFile << "found version" << ffmpegInfo.value("version").toString();
545 ffmpegInfo["enabled"] = true;
546 }
547
548 if (!includeProcessInfo || !ffmpegInfo["enabled"].toBool()) return ffmpegInfo;
549
550 QString processCodecs = KisFFMpegWrapper::runProcessAndReturn(processPath, QStringList() << "-codecs", FFMPEG_TIMEOUT);
551
552 QJsonObject codecsJson {};
553
554 {
555 // For regular expression advice, check out https://regexr.com/.
556 // Beware: We need double backslashes here for C++, in regular regex it would be a single backslash instead.
557 QRegularExpression ffmpegCodecsRX("(D|\\.)(E|\\.)....\\s+(.+?)\\s+([^\\r\\n]*)");
558 QRegularExpressionMatchIterator codecsMatchList = ffmpegCodecsRX.globalMatch(processCodecs);
559
560 // Find out codec types.. (e.g. H264, VP9, etc)
561 while (codecsMatchList.hasNext()) {
562 QRegularExpressionMatch codecsMatch = codecsMatchList.next();
563
564 if (codecsMatch.hasMatch()) {
565 QJsonObject codecInfoJson {};
566
567 bool encodingSupported = codecsMatch.captured(2) == "E" ? true:false;
568 bool decodingSupported = codecsMatch.captured(1) == "D" ? true:false;
569 QString codecName = codecsMatch.captured(3);
570 QString codecRemainder = codecsMatch.captured(4);
571
572 codecInfoJson.insert("encoding", encodingSupported);
573 codecInfoJson.insert("decoding", decodingSupported);
574
575
576 // Regular expression for grouping specific encoders and decoders..
577 QRegularExpression ffmpegSpecificCodecRX("\\(decoders:(.+?)\\)|\\(encoders:(.+?)\\)");
578 QRegularExpressionMatchIterator specificCodecMatchList = ffmpegSpecificCodecRX.globalMatch(codecRemainder);
579
580 QJsonArray encodersList;
581 QJsonArray decodersList;
582
583 while (specificCodecMatchList.hasNext()) {
584 QRegularExpressionMatch specificCodecMatch = specificCodecMatchList.next();
585 if (specificCodecMatch.hasMatch()) {
586 // Add specific decoders..
587 QStringList decoders = specificCodecMatch.captured(1).split(" ");
588 Q_FOREACH(const QString& string, decoders) {
589 if (!string.isEmpty()) {
590 decodersList.push_back(string);
591 }
592 }
593
594 // Add specific encoders.. (e.g. for h264: h264_vaapi, libopenh264, etc )
595 QStringList encoders = specificCodecMatch.captured(2).split(" ");
596 Q_FOREACH(const QString& string, encoders) {
597 if (!string.isEmpty()) {
598 encodersList.push_back(string);
599 }
600 }
601 }
602 }
603
604 codecInfoJson.insert("encoders", encodersList);
605 codecInfoJson.insert("decoders", decodersList);
606 codecsJson.insert(codecName, codecInfoJson);
607 }
608 }
609 }
610
611 ffmpegInfo.insert("codecs", codecsJson);
612
613
614 dbgFile << "codec support:" << ffmpegInfo;
615 } else {
616 dbgFile << "Not a valid process at:" << processPath;
617 }
618
619 return ffmpegInfo;
620
621}
622
623QStringList KisFFMpegWrapper::getSupportedCodecs(const QJsonObject& ffmpegProcessInfo) {
624 // TODO: I really don't like having to deal with JSON in C++ code as frequently as we are here.
625 // We should make a proper datatype for FFMPEG Process Information!
626
627 QStringList encodersToReturn = {};
628
629 // For now, I'm just treating this as strictly typed using KIS_SAFE_ASSERT_RECOVER_RETURN
630 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(ffmpegProcessInfo["enabled"].toBool(), encodersToReturn);
631
632 QJsonObject encoders = ffmpegProcessInfo["codecs"].toObject();
633 Q_FOREACH( const QString& key, encoders.keys()) {
634 if (encoders[key].toObject()["encoding"].toBool()) {
635 encodersToReturn << key;
636 }
637 }
638
639 return encodersToReturn;
640}
641
642QJsonObject KisFFMpegWrapper::findFFMpeg(const QString &customLocation)
643{
644 return findProcessPath("ffmpeg", customLocation, true);
645}
646
647QJsonObject KisFFMpegWrapper::findFFProbe(const QString &customLocation)
648{
649 return findProcessPath("ffprobe", customLocation, false);
650}
651
652QJsonObject KisFFMpegWrapper::ffprobe(const QString &inputFile, const QString &ffprobePath)
653{
654 struct KisFFMpegWrapperSettings ffprobeSettings;
655
656 ffprobeSettings.processPath = ffprobePath;
657 ffprobeSettings.storeOutput = true;
658 ffprobeSettings.defaultPrependArgs.clear();
659
660 ffprobeSettings.args << "-hide_banner"
661 << "-v" << "warning"
662 << "-of" << "json=compact=1"
663 << "-show_format"
664 << "-show_streams"
665 << "-i" << inputFile;
666 startNonBlocking(ffprobeSettings);
668
669 QString ffprobeSTDOUT = m_processSTDOUT;
670 QString ffprobeSTDERR = m_processSTDERR;
671
672 QJsonDocument ffprobeJsonDoc = QJsonDocument::fromJson(ffprobeSTDOUT.toUtf8());
673 QJsonObject ffprobeJsonObj;
674
675 if (ffprobeJsonDoc.isNull() || !ffprobeJsonDoc.isObject()) {
676 ffprobeJsonObj["error"] = FFProbeErrorCodes::INVALID_JSON;
677 return ffprobeJsonObj;
678 }
679
680 ffprobeJsonObj = ffprobeJsonDoc.object();
681
682 const bool hasValidStreams = ffprobeCheckStreamsValid(ffprobeJsonObj, ffprobeSTDERR);
683 ffprobeJsonObj["error"] = hasValidStreams ? FFProbeErrorCodes::NONE : FFProbeErrorCodes::UNSUPPORTED_CODEC;
684
685
686 return ffprobeJsonObj;
687}
688
689QJsonObject KisFFMpegWrapper::ffmpegProbe(const QString &inputFile, const QString &ffmpegPath, bool batchMode)
690{
691 struct KisFFMpegWrapperSettings ffmpegSettings;
692
693 ffmpegSettings.processPath = ffmpegPath;
694 ffmpegSettings.storeOutput = true;
695 ffmpegSettings.progressMessage = i18nc("Video information probing dialog. arg1: frame number.", "Loading video data... %1 frames examined.", "[progress]");
696 ffmpegSettings.batchMode = batchMode;
697
698 ffmpegSettings.args << "-stats"
699 << "-v" << "info"
700 << "-progress" << "pipe:1"
701 << "-map" << "0:v:0"
702 << "-c" << "copy"
703 << "-f" << "null" << "pipe:1"
704 << "-i" << inputFile;
705
706
707 this->startNonBlocking(ffmpegSettings);
708 this->waitForFinished();
709
710
711 QString ffmpegSTDOUT = m_processSTDERR + "\n" + m_processSTDOUT;
712
713 dbgFile << "ffmpegProbe stdout:" << ffmpegSTDOUT;
714
715 QJsonObject ffmpegJsonObj;
716 QJsonArray ffmpegStreamsJsonArr;
717 QJsonObject ffmpegFormatJsonObj;
718 QJsonObject ffmpegProgressJsonObj;
719
720 QStringList stdoutLines = ffmpegSTDOUT.split('\n');
721
722 ffmpegJsonObj["error"] = FFProbeErrorCodes::UNSUPPORTED_CODEC;
723
724 for (const QString &line : stdoutLines) {
725 dbgFile << "ffmpeg probe stdout" << line;
726 QRegularExpression videoInfoInputRX("Duration: (\\d+):(\\d+):([\\d\\.]+),");
727 QRegularExpressionMatch durationMatch = videoInfoInputRX.match(line);
728
729 if (ffmpegFormatJsonObj.value("duration").isUndefined() && durationMatch.hasMatch()) {
730 ffmpegFormatJsonObj["duration"] = QString::number( (durationMatch.captured(1).toInt() * 60 * 60)
731 + (durationMatch.captured(2).toInt() * 60)
732 + durationMatch.captured(3).toFloat()
733 );
734 } else {
735 QRegularExpression videoInfoStreamRX(
736 QString("Stream #(\\d+):(\\d+)(?:[ ]*?\\((\\w+)\\)|): Video: (\\w+?)(?:[ ]*?\\((.+?)\\)|),[ ]*?")
737 .append("(\\w+?)(?:[ ]*?\\((.+?)\\)|),[ ]*?")
738 .append("(\\d+)x(\\d+)([ ]*?\\[.+?\\]|.*?),[ ]*?")
739 .append("(?:([\\d\\.]+) fps,|) (\\S+?) tbr, (.+?) tbn, (.+?) tbc")
740 );
741
742 QRegularExpressionMatch streamMatch = videoInfoStreamRX.match(line);
743
744 if (streamMatch.hasMatch() && ffmpegStreamsJsonArr[streamMatch.captured(1).toInt()].isUndefined() ) {
745 int index = streamMatch.captured(1).toInt();
746 QJsonObject ffmpegJsonOnStreamObj;
747
748 ffmpegJsonOnStreamObj["index"] = index;
749 ffmpegJsonOnStreamObj["codec_name"] = streamMatch.captured(4);
750 ffmpegJsonOnStreamObj["profile"] = streamMatch.captured(5);
751 ffmpegJsonOnStreamObj["pix_fmt"] = streamMatch.captured(6);
752 ffmpegJsonOnStreamObj["width"] = streamMatch.captured(8).toInt();
753 ffmpegJsonOnStreamObj["height"] = streamMatch.captured(9).toInt();
754
755 ffmpegJsonOnStreamObj["codec_type"] = "video";
756
757 if (streamMatch.captured(11).toFloat() > 0) {
758 float fps = streamMatch.captured(11).toFloat();
759
760 ffmpegProgressJsonObj["ffmpeg_fps"] = QString::number(fps);
761 ffmpegJsonOnStreamObj["r_frame_rate"] = QString::number( fps * 10000 ).append(QString("/10000"));
762 } else {
763 ffmpegProgressJsonObj["ffmpeg_fps"] = 0;
764 }
765
766 ffmpegJsonObj["error"] = FFProbeErrorCodes::NONE;
767
768 dbgFile << "ffmpegProbe stream:" << ffmpegJsonOnStreamObj;
769
770 ffmpegStreamsJsonArr.insert(index, ffmpegJsonOnStreamObj);
771
772 } else {
773
774 QRegularExpression videoInfoFrameRX("^(\\w+?)=([\\w\\./:]+?)$");
775 QRegularExpressionMatch frameMatch = videoInfoFrameRX.match(line);
776
777 if (frameMatch.hasMatch()) ffmpegProgressJsonObj[frameMatch.captured(1)] = frameMatch.captured(2);
778
779 }
780 }
781 }
782
783 ffmpegJsonObj.insert("streams",ffmpegStreamsJsonArr);
784 ffmpegJsonObj.insert("format",ffmpegFormatJsonObj);
785 ffmpegJsonObj.insert("progress",ffmpegProgressJsonObj);
786
787 return ffmpegJsonObj;
788}
789
791{
792 if (name == "bt709") {
794 }
795 if (name == "bt470m") {
797 }
798 if (name == "bt470bg") {
800 }
801 if (name == "smpte170m") {
803 }
804 if (name == "smpte240m") {
806 }
807 if (name == "film") {
809 }
810 if (name == "bt2020") {
812 }
813 if (name.startsWith("smpte428")) {
815 }
816 if (name == "smpte431") {
818 }
819 if (name == "smpte432") {
821 }
822 if (name == "jedec-p22") {
824 }
825
827}
828
830{
831 if (name == "bt709") {
832 return TRC_ITU_R_BT_709_5;
833 }
834 if (name == "gamma22") {
836 }
837 if (name == "gamma28") {
839 }
840 if (name == "smpte170m") {
841 return TRC_ITU_R_BT_601_6;
842 }
843 if (name == "smpte240m") {
844 return TRC_SMPTE_240M;
845 }
846 if (name == "linear") {
847 return TRC_LINEAR;
848 }
849 if (name == "log" || name == "log100") {
850 return TRC_LOGARITHMIC_100;
851 }
852 if (name == "log316" || name == "log_sqrt") {
854 }
855 if (name == "iec61966_2_4" || name == "iec61966-2-4") {
856 return TRC_IEC_61966_2_4;
857 }
858 if (name.startsWith("bt1361")) {
859 return TRC_ITU_R_BT_1361;
860 }
861 if (name == "iec61966_2_1" || name == "iec61966-2-1") {
862 return TRC_IEC_61966_2_1;
863 }
864 if (name.startsWith("bt2020_10")) {
866 }
867 if (name.startsWith("bt2020_12")) {
869 }
870 if (name == "smpte2084") {
872 }
873 if (name == "smpte240m") {
874 return TRC_SMPTE_240M;
875 }
876 if (name.startsWith("smpte428")) {
877 return TRC_SMPTE_ST_428_1;
878 }
879 if (name == "arib-std-b67") {
881 }
882
883 return TRC_UNSPECIFIED;
884}
@ 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)