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#endif /* Q_OS_LINUX */
410}
411
412QByteArray KisFFMpegWrapper::runProcessAndReturn(const QString &processPath, const QStringList &args, int msecs)
413{
414 QProcess runProcess;
415
416 fixUpNonEmbeddedProcessEnvironment(processPath, runProcess);
417
418 runProcess.start(processPath, args);
419
420 if (runProcess.waitForStarted(msecs)) runProcess.waitForFinished(msecs);
421
422 const bool successfulStart =
423 runProcess.state() == QProcess::NotRunning &&
424 runProcess.error() == QProcess::UnknownError;
425
426 dbgFile << "runProcessAndReturn Success:" << successfulStart;
427
428 if (successfulStart) return runProcess.readAllStandardOutput();
429
430 return "";
431}
432
434{
435 KisConfig cfg(true);
436 return cfg.ffmpegLocation();
437}
438
440{
441 KisConfig cfg(false);
442 cfg.setFFMpegLocation(location);
443}
444
445QJsonObject KisFFMpegWrapper::findProcessPath(const QString &processName, const QString &customLocation, bool includeProcessInfo)
446{
447 QJsonObject resultJsonObj;
448 QStringList proposedPaths;
449
450 // User-specified..
451 if (!customLocation.isEmpty()) {
452 proposedPaths << customLocation;
453 proposedPaths << customLocation + '/' + processName;
454 }
455
456 // Krita-bundled..
457 proposedPaths << KoResourcePaths::getApplicationRoot() + '/' + "bin" + '/' + processName;
458
459 // OS-specific..
460#ifdef Q_OS_WIN
461 // Look for winget-installed ffmpeg packages in C:\users\USERNAME\appdata\local\microsoft\winget\packages\FFMPEGVERSION\bin
462 QDir wingetPackageDir = QDir(QDir::homePath() + "/AppData/Local/Microsoft/WinGet/Packages/");
463 QStringList wingetPackageFolderNames = wingetPackageDir.entryList();
464 for (const auto &folderName : wingetPackageFolderNames) {
465 if (folderName.contains("ffmpeg", Qt::CaseInsensitive)) {
466 QDir ffmpegPackageFolder = QDir(wingetPackageDir.path() + "/" + folderName);
467 QStringList ffmpegRootNames = ffmpegPackageFolder.entryList();
468 for (const auto &ffmpegRootName : ffmpegRootNames) {
469 proposedPaths << ffmpegPackageFolder.path() + "/" + ffmpegRootName + "/bin/";
470 }
471 }
472 }
473#endif
474
475#ifdef Q_OS_MACOS
476 proposedPaths << QCoreApplication::applicationDirPath() + '/' + processName;
477#endif
478
479#ifndef Q_OS_WIN
480 proposedPaths << QDir::homePath() + "/bin/" + processName;
481 proposedPaths << "/usr/bin/" + processName;
482 proposedPaths << "/usr/local/bin/" + processName;
483#endif
484
485 dbgFile << proposedPaths;
486 for (int i = 0; i != proposedPaths.size(); ++i) {
487 if (proposedPaths[i].isEmpty()) continue;
488
489#ifdef Q_OS_WIN
490 proposedPaths[i] = QDir::toNativeSeparators(QDir::cleanPath(proposedPaths[i]));
491
492 if (proposedPaths[i].endsWith('/')) {
493 continue;
494 }
495
496 if (!proposedPaths[i].endsWith(".exe", Qt::CaseInsensitive)) {
497 if (!QFile::exists(proposedPaths[i])) {
498 proposedPaths[i] += ".exe";
499 if (!QFile::exists(proposedPaths[i])) {
500 continue;
501 }
502 }
503 }
504#endif
505
506 QJsonObject processInfoJsonObj = findProcessInfo(processName, proposedPaths[i], includeProcessInfo);
507 dbgFile << "PATH" << proposedPaths[i] << processInfoJsonObj.value("enabled").toBool();
508 if (processInfoJsonObj.value("enabled").toBool()) {
509 processInfoJsonObj["custom"]=(!customLocation.isEmpty() && i <= 1) ? true:false;
510 resultJsonObj = processInfoJsonObj;
511 break;
512 }
513 }
514
515 return resultJsonObj;
516}
517
518QJsonObject KisFFMpegWrapper::findProcessInfo(const QString &processName, const QString &rawProcessPath, bool includeProcessInfo)
519{
520
521 QJsonObject ffmpegInfo {{"path", rawProcessPath},
522 {"enabled",false},
523 {"version", "0"},
524 {"encoder",QJsonValue::Object},
525 {"decoder",QJsonValue::Object}};
526
527 if (!QFile::exists(rawProcessPath)) return ffmpegInfo;
528
529 const QString processPath = QFileInfo(rawProcessPath).absoluteFilePath();
530 ffmpegInfo["path"] = processPath;
531
532 dbgFile << "Found process at:" << processPath;
533 QString processVersion = KisFFMpegWrapper::runProcessAndReturn(processPath, QStringList() << "-version", FFMPEG_TIMEOUT);
534
535 if (!processVersion.isEmpty()) {
536
537 QRegularExpressionMatch versionMatch = ffmpegVersionRX.match(processVersion);
538
539 if (versionMatch.hasMatch() && versionMatch.captured(1) == processName ) {
540 ffmpegInfo["version"] = versionMatch.captured(2);
541 dbgFile << "found version" << ffmpegInfo.value("version").toString();
542 ffmpegInfo["enabled"] = true;
543 }
544
545 if (!includeProcessInfo || !ffmpegInfo["enabled"].toBool()) return ffmpegInfo;
546
547 QString processCodecs = KisFFMpegWrapper::runProcessAndReturn(processPath, QStringList() << "-codecs", FFMPEG_TIMEOUT);
548
549 QJsonObject codecsJson {};
550
551 {
552 // For regular expression advice, check out https://regexr.com/.
553 // Beware: We need double backslashes here for C++, in regular regex it would be a single backslash instead.
554 QRegularExpression ffmpegCodecsRX("(D|\\.)(E|\\.)....\\s+(.+?)\\s+([^\\r\\n]*)");
555 QRegularExpressionMatchIterator codecsMatchList = ffmpegCodecsRX.globalMatch(processCodecs);
556
557 // Find out codec types.. (e.g. H264, VP9, etc)
558 while (codecsMatchList.hasNext()) {
559 QRegularExpressionMatch codecsMatch = codecsMatchList.next();
560
561 if (codecsMatch.hasMatch()) {
562 QJsonObject codecInfoJson {};
563
564 bool encodingSupported = codecsMatch.captured(2) == "E" ? true:false;
565 bool decodingSupported = codecsMatch.captured(1) == "D" ? true:false;
566 QString codecName = codecsMatch.captured(3);
567 QString codecRemainder = codecsMatch.captured(4);
568
569 codecInfoJson.insert("encoding", encodingSupported);
570 codecInfoJson.insert("decoding", decodingSupported);
571
572
573 // Regular expression for grouping specific encoders and decoders..
574 QRegularExpression ffmpegSpecificCodecRX("\\(decoders:(.+?)\\)|\\(encoders:(.+?)\\)");
575 QRegularExpressionMatchIterator specificCodecMatchList = ffmpegSpecificCodecRX.globalMatch(codecRemainder);
576
577 QJsonArray encodersList;
578 QJsonArray decodersList;
579
580 while (specificCodecMatchList.hasNext()) {
581 QRegularExpressionMatch specificCodecMatch = specificCodecMatchList.next();
582 if (specificCodecMatch.hasMatch()) {
583 // Add specific decoders..
584 QStringList decoders = specificCodecMatch.captured(1).split(" ");
585 Q_FOREACH(const QString& string, decoders) {
586 if (!string.isEmpty()) {
587 decodersList.push_back(string);
588 }
589 }
590
591 // Add specific encoders.. (e.g. for h264: h264_vaapi, libopenh264, etc )
592 QStringList encoders = specificCodecMatch.captured(2).split(" ");
593 Q_FOREACH(const QString& string, encoders) {
594 if (!string.isEmpty()) {
595 encodersList.push_back(string);
596 }
597 }
598 }
599 }
600
601 codecInfoJson.insert("encoders", encodersList);
602 codecInfoJson.insert("decoders", decodersList);
603 codecsJson.insert(codecName, codecInfoJson);
604 }
605 }
606 }
607
608 ffmpegInfo.insert("codecs", codecsJson);
609
610
611 dbgFile << "codec support:" << ffmpegInfo;
612 } else {
613 dbgFile << "Not a valid process at:" << processPath;
614 }
615
616 return ffmpegInfo;
617
618}
619
620QStringList KisFFMpegWrapper::getSupportedCodecs(const QJsonObject& ffmpegProcessInfo) {
621 // TODO: I really don't like having to deal with JSON in C++ code as frequently as we are here.
622 // We should make a proper datatype for FFMPEG Process Information!
623
624 QStringList encodersToReturn = {};
625
626 // For now, I'm just treating this as strictly typed using KIS_SAFE_ASSERT_RECOVER_RETURN
627 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(ffmpegProcessInfo["enabled"].toBool(), encodersToReturn);
628
629 QJsonObject encoders = ffmpegProcessInfo["codecs"].toObject();
630 Q_FOREACH( const QString& key, encoders.keys()) {
631 if (encoders[key].toObject()["encoding"].toBool()) {
632 encodersToReturn << key;
633 }
634 }
635
636 return encodersToReturn;
637}
638
639QJsonObject KisFFMpegWrapper::findFFMpeg(const QString &customLocation)
640{
641 return findProcessPath("ffmpeg", customLocation, true);
642}
643
644QJsonObject KisFFMpegWrapper::findFFProbe(const QString &customLocation)
645{
646 return findProcessPath("ffprobe", customLocation, false);
647}
648
649QJsonObject KisFFMpegWrapper::ffprobe(const QString &inputFile, const QString &ffprobePath)
650{
651 struct KisFFMpegWrapperSettings ffprobeSettings;
652
653 ffprobeSettings.processPath = ffprobePath;
654 ffprobeSettings.storeOutput = true;
655 ffprobeSettings.defaultPrependArgs.clear();
656
657 ffprobeSettings.args << "-hide_banner"
658 << "-v" << "warning"
659 << "-of" << "json=compact=1"
660 << "-show_format"
661 << "-show_streams"
662 << "-i" << inputFile;
663 startNonBlocking(ffprobeSettings);
665
666 QString ffprobeSTDOUT = m_processSTDOUT;
667 QString ffprobeSTDERR = m_processSTDERR;
668
669 QJsonDocument ffprobeJsonDoc = QJsonDocument::fromJson(ffprobeSTDOUT.toUtf8());
670 QJsonObject ffprobeJsonObj;
671
672 if (ffprobeJsonDoc.isNull() || !ffprobeJsonDoc.isObject()) {
673 ffprobeJsonObj["error"] = FFProbeErrorCodes::INVALID_JSON;
674 return ffprobeJsonObj;
675 }
676
677 ffprobeJsonObj = ffprobeJsonDoc.object();
678
679 const bool hasValidStreams = ffprobeCheckStreamsValid(ffprobeJsonObj, ffprobeSTDERR);
680 ffprobeJsonObj["error"] = hasValidStreams ? FFProbeErrorCodes::NONE : FFProbeErrorCodes::UNSUPPORTED_CODEC;
681
682
683 return ffprobeJsonObj;
684}
685
686QJsonObject KisFFMpegWrapper::ffmpegProbe(const QString &inputFile, const QString &ffmpegPath, bool batchMode)
687{
688 struct KisFFMpegWrapperSettings ffmpegSettings;
689
690 ffmpegSettings.processPath = ffmpegPath;
691 ffmpegSettings.storeOutput = true;
692 ffmpegSettings.progressMessage = i18nc("Video information probing dialog. arg1: frame number.", "Loading video data... %1 frames examined.", "[progress]");
693 ffmpegSettings.batchMode = batchMode;
694
695 ffmpegSettings.args << "-stats"
696 << "-v" << "info"
697 << "-progress" << "pipe:1"
698 << "-map" << "0:v:0"
699 << "-c" << "copy"
700 << "-f" << "null" << "pipe:1"
701 << "-i" << inputFile;
702
703
704 this->startNonBlocking(ffmpegSettings);
705 this->waitForFinished();
706
707
708 QString ffmpegSTDOUT = m_processSTDERR + "\n" + m_processSTDOUT;
709
710 dbgFile << "ffmpegProbe stdout:" << ffmpegSTDOUT;
711
712 QJsonObject ffmpegJsonObj;
713 QJsonArray ffmpegStreamsJsonArr;
714 QJsonObject ffmpegFormatJsonObj;
715 QJsonObject ffmpegProgressJsonObj;
716
717 QStringList stdoutLines = ffmpegSTDOUT.split('\n');
718
719 ffmpegJsonObj["error"] = FFProbeErrorCodes::UNSUPPORTED_CODEC;
720
721 for (const QString &line : stdoutLines) {
722 dbgFile << "ffmpeg probe stdout" << line;
723 QRegularExpression videoInfoInputRX("Duration: (\\d+):(\\d+):([\\d\\.]+),");
724 QRegularExpressionMatch durationMatch = videoInfoInputRX.match(line);
725
726 if (ffmpegFormatJsonObj.value("duration").isUndefined() && durationMatch.hasMatch()) {
727 ffmpegFormatJsonObj["duration"] = QString::number( (durationMatch.captured(1).toInt() * 60 * 60)
728 + (durationMatch.captured(2).toInt() * 60)
729 + durationMatch.captured(3).toFloat()
730 );
731 } else {
732 QRegularExpression videoInfoStreamRX(
733 QString("Stream #(\\d+):(\\d+)(?:[ ]*?\\((\\w+)\\)|): Video: (\\w+?)(?:[ ]*?\\((.+?)\\)|),[ ]*?")
734 .append("(\\w+?)(?:[ ]*?\\((.+?)\\)|),[ ]*?")
735 .append("(\\d+)x(\\d+)([ ]*?\\[.+?\\]|.*?),[ ]*?")
736 .append("(?:([\\d\\.]+) fps,|) (\\S+?) tbr, (.+?) tbn, (.+?) tbc")
737 );
738
739 QRegularExpressionMatch streamMatch = videoInfoStreamRX.match(line);
740
741 if (streamMatch.hasMatch() && ffmpegStreamsJsonArr[streamMatch.captured(1).toInt()].isUndefined() ) {
742 int index = streamMatch.captured(1).toInt();
743 QJsonObject ffmpegJsonOnStreamObj;
744
745 ffmpegJsonOnStreamObj["index"] = index;
746 ffmpegJsonOnStreamObj["codec_name"] = streamMatch.captured(4);
747 ffmpegJsonOnStreamObj["profile"] = streamMatch.captured(5);
748 ffmpegJsonOnStreamObj["pix_fmt"] = streamMatch.captured(6);
749 ffmpegJsonOnStreamObj["width"] = streamMatch.captured(8).toInt();
750 ffmpegJsonOnStreamObj["height"] = streamMatch.captured(9).toInt();
751
752 ffmpegJsonOnStreamObj["codec_type"] = "video";
753
754 if (streamMatch.captured(11).toFloat() > 0) {
755 float fps = streamMatch.captured(11).toFloat();
756
757 ffmpegProgressJsonObj["ffmpeg_fps"] = QString::number(fps);
758 ffmpegJsonOnStreamObj["r_frame_rate"] = QString::number( fps * 10000 ).append(QString("/10000"));
759 } else {
760 ffmpegProgressJsonObj["ffmpeg_fps"] = 0;
761 }
762
763 ffmpegJsonObj["error"] = FFProbeErrorCodes::NONE;
764
765 dbgFile << "ffmpegProbe stream:" << ffmpegJsonOnStreamObj;
766
767 ffmpegStreamsJsonArr.insert(index, ffmpegJsonOnStreamObj);
768
769 } else {
770
771 QRegularExpression videoInfoFrameRX("^(\\w+?)=([\\w\\./:]+?)$");
772 QRegularExpressionMatch frameMatch = videoInfoFrameRX.match(line);
773
774 if (frameMatch.hasMatch()) ffmpegProgressJsonObj[frameMatch.captured(1)] = frameMatch.captured(2);
775
776 }
777 }
778 }
779
780 ffmpegJsonObj.insert("streams",ffmpegStreamsJsonArr);
781 ffmpegJsonObj.insert("format",ffmpegFormatJsonObj);
782 ffmpegJsonObj.insert("progress",ffmpegProgressJsonObj);
783
784 return ffmpegJsonObj;
785}
786
788{
789 if (name == "bt709") {
791 }
792 if (name == "bt470m") {
794 }
795 if (name == "bt470bg") {
797 }
798 if (name == "smpte170m") {
800 }
801 if (name == "smpte240m") {
803 }
804 if (name == "film") {
806 }
807 if (name == "bt2020") {
809 }
810 if (name.startsWith("smpte428")) {
812 }
813 if (name == "smpte431") {
815 }
816 if (name == "smpte432") {
818 }
819 if (name == "jedec-p22") {
821 }
822
824}
825
827{
828 if (name == "bt709") {
829 return TRC_ITU_R_BT_709_5;
830 }
831 if (name == "gamma22") {
833 }
834 if (name == "gamma28") {
836 }
837 if (name == "smpte170m") {
838 return TRC_ITU_R_BT_601_6;
839 }
840 if (name == "smpte240m") {
841 return TRC_SMPTE_240M;
842 }
843 if (name == "linear") {
844 return TRC_LINEAR;
845 }
846 if (name == "log" || name == "log100") {
847 return TRC_LOGARITHMIC_100;
848 }
849 if (name == "log316" || name == "log_sqrt") {
851 }
852 if (name == "iec61966_2_4" || name == "iec61966-2-4") {
853 return TRC_IEC_61966_2_4;
854 }
855 if (name.startsWith("bt1361")) {
856 return TRC_ITU_R_BT_1361;
857 }
858 if (name == "iec61966_2_1" || name == "iec61966-2-1") {
859 return TRC_IEC_61966_2_1;
860 }
861 if (name.startsWith("bt2020_10")) {
863 }
864 if (name.startsWith("bt2020_12")) {
866 }
867 if (name == "smpte2084") {
869 }
870 if (name == "smpte240m") {
871 return TRC_SMPTE_240M;
872 }
873 if (name.startsWith("smpte428")) {
874 return TRC_SMPTE_ST_428_1;
875 }
876 if (name == "arib-std-b67") {
878 }
879
880 return TRC_UNSPECIFIED;
881}
@ 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
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
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)