9#include <klocalizedstring.h>
13#include <QJsonDocument>
18#include <QRegularExpression>
19#include <QApplication>
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");
63 if (QFile::exists(renderLogPath)) {
64 QFile existingFile(renderLogPath);
65 existingFile.remove();
68 QFile renderLog(renderLogPath);
71 if (renderLog.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
73 renderLog.write(command.toUtf8());
74 renderLog.write(
"\n");
75 renderLog.write(
"=====================================================\n");
80 QFile renderLog(renderLogPath);
81 if (renderLog.open(QIODevice::WriteOnly | QIODevice::Append)) {
82 renderLog.write(stderrBuffer);
87 if (!settings.
logPath.isEmpty()) {
88 QString sessionRenderLogPath(settings.
logPath);
90 QFile sessionRenderLog(sessionRenderLogPath);
93 const QString logDirPath = QFileInfo(sessionRenderLogPath).dir().path();
94 QDir().mkpath(logDirPath);
96 if (sessionRenderLog.open(QIODevice::WriteOnly | QIODevice::Append)) {
99 QFile renderLog(renderLogPath);
100 QFile sessionRenderLog(sessionRenderLogPath);
101 if (renderLog.open(QIODevice::ReadOnly) &&
102 sessionRenderLog.open(QIODevice::WriteOnly | QIODevice::Append)) {
106 while (!(buffer = renderLog.read(chunksize)).isEmpty()) {
107 sessionRenderLog.write(buffer);
117 progressText.replace(
"[progress]",
"0");
121 m_progress->setWindowModality(Qt::ApplicationModal);
136 dbgFile <<
"Open progress dialog!";
150 args << settings.
args;
165 struct ProcessResults {
167 QString error = QString();
173 processResults->finish =
true;
174 processResults->error = errMsg;
178 processResults->finish =
true;
184 if (processResults->finish ==
true) {
185 if (processResults->error.isEmpty()) {
201 return (
m_process->waitForFinished(msecs) &&
m_process->exitStatus() == QProcess::ExitStatus::NormalExit);
217 progressText.replace(
"[progress]", QString::number(progressValue));
219 progressText.replace(
"[suffix]", suffix );
225 QApplication::processEvents();
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);
237 while (invalidStreamMatchList.hasNext()) {
238 QRegularExpressionMatch invalidStreamMatch = invalidStreamMatchList.next();
240 if (invalidStreamMatch.hasMatch()) {
241 const int invalidStreamId = invalidStreamMatch.captured(1).toInt();
244 if (ffprobeJsonObj[
"streams"].toArray()[invalidStreamId].toObject()[
"codec_type"] ==
"video") {
259 if (
m_process->state() != QProcess::NotRunning) {
267 QByteArray stderrRawBuffer =
m_process->readAllStandardError();
276 while ((endPos =
m_stderrBuffer.indexOf(lineDelimiter, startPos)) != -1) {
277 const QString &line =
m_stderrBuffer.mid(startPos, endPos - startPos).trimmed();
283 for (
const QString &word : errorWords) {
284 if (line.contains(word)) {
290 const QRegularExpressionMatch &match = frameRegexp.match(line);
292 if (match.hasMatch()) {
293 frameNo = match.captured(1).toInt();
297 dbgFile <<
"ffmpeg stderr:" << line;
298 startPos = endPos + 1;
312 QByteArray stdoutRawBuffer =
m_process->readAllStandardOutput();
329 while ((endPos =
m_stdoutBuffer.indexOf(lineDelimiter, startPos)) != -1) {
330 const QString &line =
m_stdoutBuffer.mid(startPos, endPos - startPos).trimmed();
332 dbgFile <<
"ffmpeg stdout:" << line;
334 startPos = endPos + 1;
345 dbgFile <<
"ffmpeg process started!";
352 dbgFile <<
"FFMpeg finished with code" << exitCode;
359 if (
m_process->exitStatus() == QProcess::CrashExit) {
381 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
383 const QStringList libraryPaths = env.value(
"LD_LIBRARY_PATH").split(
':');
384 const QString processAbsPath = QFileInfo(QFileInfo(processPath).absolutePath() +
"/../").absoluteFilePath();
386 bool isCustomBuildOfFFmpeg =
false;
388 Q_FOREACH (
const QString &path, libraryPaths) {
389 const QString absPath1 = QFileInfo(path +
"/").absoluteFilePath();
390 const QString absPath2 = QFileInfo(path +
"/../").absoluteFilePath();
392 if (absPath1 == processAbsPath || absPath2 == processAbsPath) {
393 dbgFile <<
"Detected embedded ffmpeg:" << processPath;
398 isCustomBuildOfFFmpeg =
true;
404 if (!isCustomBuildOfFFmpeg) {
405 dbgFile <<
"Removing LD_LIBRARY_PATH for running" << processPath;
407 env.remove(
"LD_LIBRARY_PATH");
408 process.setProcessEnvironment(env);
411 Q_UNUSED(processPath);
422 runProcess.start(processPath, args);
424 if (runProcess.waitForStarted(msecs)) runProcess.waitForFinished(msecs);
426 const bool successfulStart =
427 runProcess.state() == QProcess::NotRunning &&
428 runProcess.error() == QProcess::UnknownError;
430 dbgFile <<
"runProcessAndReturn Success:" << successfulStart;
432 if (successfulStart)
return runProcess.readAllStandardOutput();
451 QJsonObject resultJsonObj;
455 if (!customLocation.isEmpty()) {
456 proposedPaths << customLocation;
457 proposedPaths << customLocation +
'/' + processName;
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/";
480 proposedPaths << QCoreApplication::applicationDirPath() +
'/' + processName;
484 proposedPaths << QDir::homePath() +
"/bin/" + processName;
485 proposedPaths <<
"/usr/bin/" + processName;
486 proposedPaths <<
"/usr/local/bin/" + processName;
490 for (
int i = 0; i != proposedPaths.size(); ++i) {
491 if (proposedPaths[i].isEmpty())
continue;
494 proposedPaths[i] = QDir::toNativeSeparators(QDir::cleanPath(proposedPaths[i]));
496 if (proposedPaths[i].endsWith(
'/')) {
500 if (!proposedPaths[i].endsWith(
".exe", Qt::CaseInsensitive)) {
501 if (!QFile::exists(proposedPaths[i])) {
502 proposedPaths[i] +=
".exe";
503 if (!QFile::exists(proposedPaths[i])) {
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;
519 return resultJsonObj;
525 QJsonObject ffmpegInfo {{
"path", rawProcessPath},
528 {
"encoder",QJsonValue::Object},
529 {
"decoder",QJsonValue::Object}};
531 if (!QFile::exists(rawProcessPath))
return ffmpegInfo;
533 const QString processPath = QFileInfo(rawProcessPath).absoluteFilePath();
534 ffmpegInfo[
"path"] = processPath;
536 dbgFile <<
"Found process at:" << processPath;
539 if (!processVersion.isEmpty()) {
541 QRegularExpressionMatch versionMatch = ffmpegVersionRX.match(processVersion);
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;
549 if (!includeProcessInfo || !ffmpegInfo[
"enabled"].toBool())
return ffmpegInfo;
553 QJsonObject codecsJson {};
558 QRegularExpression ffmpegCodecsRX(
"(D|\\.)(E|\\.)....\\s+(.+?)\\s+([^\\r\\n]*)");
559 QRegularExpressionMatchIterator codecsMatchList = ffmpegCodecsRX.globalMatch(processCodecs);
562 while (codecsMatchList.hasNext()) {
563 QRegularExpressionMatch codecsMatch = codecsMatchList.next();
565 if (codecsMatch.hasMatch()) {
566 QJsonObject codecInfoJson {};
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);
573 codecInfoJson.insert(
"encoding", encodingSupported);
574 codecInfoJson.insert(
"decoding", decodingSupported);
578 QRegularExpression ffmpegSpecificCodecRX(
"\\(decoders:(.+?)\\)|\\(encoders:(.+?)\\)");
579 QRegularExpressionMatchIterator specificCodecMatchList = ffmpegSpecificCodecRX.globalMatch(codecRemainder);
581 QJsonArray encodersList;
582 QJsonArray decodersList;
584 while (specificCodecMatchList.hasNext()) {
585 QRegularExpressionMatch specificCodecMatch = specificCodecMatchList.next();
586 if (specificCodecMatch.hasMatch()) {
588 QStringList decoders = specificCodecMatch.captured(1).split(
" ");
589 Q_FOREACH(
const QString&
string, decoders) {
590 if (!
string.isEmpty()) {
591 decodersList.push_back(
string);
596 QStringList encoders = specificCodecMatch.captured(2).split(
" ");
597 Q_FOREACH(
const QString&
string, encoders) {
598 if (!
string.isEmpty()) {
599 encodersList.push_back(
string);
605 codecInfoJson.insert(
"encoders", encodersList);
606 codecInfoJson.insert(
"decoders", decodersList);
607 codecsJson.insert(codecName, codecInfoJson);
612 ffmpegInfo.insert(
"codecs", codecsJson);
615 dbgFile <<
"codec support:" << ffmpegInfo;
617 dbgFile <<
"Not a valid process at:" << processPath;
633 QJsonObject encoders = ffmpegProcessInfo[
"codecs"].toObject();
634 Q_FOREACH(
const QString& key, encoders.keys()) {
635 if (encoders[key].toObject()[
"encoding"].toBool()) {
636 encodersToReturn << key;
640 return encodersToReturn;
661 ffprobeSettings.
args <<
"-hide_banner"
663 <<
"-of" <<
"json=compact=1"
666 <<
"-i" << inputFile;
673 QJsonDocument ffprobeJsonDoc = QJsonDocument::fromJson(ffprobeSTDOUT.toUtf8());
674 QJsonObject ffprobeJsonObj;
676 if (ffprobeJsonDoc.isNull() || !ffprobeJsonDoc.isObject()) {
678 return ffprobeJsonObj;
681 ffprobeJsonObj = ffprobeJsonDoc.object();
687 return ffprobeJsonObj;
696 ffmpegSettings.
progressMessage = i18nc(
"Video information probing dialog. arg1: frame number.",
"Loading video data... %1 frames examined.",
"[progress]");
699 ffmpegSettings.
args <<
"-stats"
701 <<
"-progress" <<
"pipe:1"
704 <<
"-f" <<
"null" <<
"pipe:1"
705 <<
"-i" << inputFile;
714 dbgFile <<
"ffmpegProbe stdout:" << ffmpegSTDOUT;
716 QJsonObject ffmpegJsonObj;
717 QJsonArray ffmpegStreamsJsonArr;
718 QJsonObject ffmpegFormatJsonObj;
719 QJsonObject ffmpegProgressJsonObj;
721 QStringList stdoutLines = ffmpegSTDOUT.split(
'\n');
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);
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()
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")
743 QRegularExpressionMatch streamMatch = videoInfoStreamRX.match(line);
745 if (streamMatch.hasMatch() && ffmpegStreamsJsonArr[streamMatch.captured(1).toInt()].isUndefined() ) {
746 int index = streamMatch.captured(1).toInt();
747 QJsonObject ffmpegJsonOnStreamObj;
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();
756 ffmpegJsonOnStreamObj[
"codec_type"] =
"video";
758 if (streamMatch.captured(11).toFloat() > 0) {
759 float fps = streamMatch.captured(11).toFloat();
761 ffmpegProgressJsonObj[
"ffmpeg_fps"] = QString::number(fps);
762 ffmpegJsonOnStreamObj[
"r_frame_rate"] = QString::number( fps * 10000 ).append(QString(
"/10000"));
764 ffmpegProgressJsonObj[
"ffmpeg_fps"] = 0;
769 dbgFile <<
"ffmpegProbe stream:" << ffmpegJsonOnStreamObj;
771 ffmpegStreamsJsonArr.insert(index, ffmpegJsonOnStreamObj);
775 QRegularExpression videoInfoFrameRX(
"^(\\w+?)=([\\w\\./:]+?)$");
776 QRegularExpressionMatch frameMatch = videoInfoFrameRX.match(line);
778 if (frameMatch.hasMatch()) ffmpegProgressJsonObj[frameMatch.captured(1)] = frameMatch.captured(2);
784 ffmpegJsonObj.insert(
"streams",ffmpegStreamsJsonArr);
785 ffmpegJsonObj.insert(
"format",ffmpegFormatJsonObj);
786 ffmpegJsonObj.insert(
"progress",ffmpegProgressJsonObj);
788 return ffmpegJsonObj;
793 if (name ==
"bt709") {
796 if (name ==
"bt470m") {
799 if (name ==
"bt470bg") {
802 if (name ==
"smpte170m") {
805 if (name ==
"smpte240m") {
808 if (name ==
"film") {
811 if (name ==
"bt2020") {
814 if (name.startsWith(
"smpte428")) {
817 if (name ==
"smpte431") {
820 if (name ==
"smpte432") {
823 if (name ==
"jedec-p22") {
832 if (name ==
"bt709") {
835 if (name ==
"gamma22") {
838 if (name ==
"gamma28") {
841 if (name ==
"smpte170m") {
844 if (name ==
"smpte240m") {
847 if (name ==
"linear") {
850 if (name ==
"log" || name ==
"log100") {
853 if (name ==
"log316" || name ==
"log_sqrt") {
856 if (name ==
"iec61966_2_4" || name ==
"iec61966-2-4") {
859 if (name.startsWith(
"bt1361")) {
862 if (name ==
"iec61966_2_1" || name ==
"iec61966-2-1") {
865 if (name.startsWith(
"bt2020_10")) {
868 if (name.startsWith(
"bt2020_12")) {
871 if (name ==
"smpte2084") {
874 if (name ==
"smpte240m") {
877 if (name.startsWith(
"smpte428")) {
880 if (name ==
"arib-std-b67") {
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_ITU_R_BT_470_6_SYSTEM_B_G
@ PRIMARIES_ITU_R_BT_601_6
@ PRIMARIES_SMPTE_RP_431_2
@ 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_ITU_R_BT_2020_2_10bit
@ TRC_ITU_R_BT_470_6_SYSTEM_M
@ TRC_ITU_R_BT_470_6_SYSTEM_B_G
@ TRC_ITU_R_BT_2100_0_HLG
@ TRC_LOGARITHMIC_100_sqrt10
@ TRC_ITU_R_BT_2020_2_12bit
void setFFMpegLocation(const QString &value)
QString ffmpegLocation(bool defaultValue=false) const
void slotReadyReadSTDERR()
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)
void slotReadyReadSTDOUT()
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)
QSharedPointer< T > toQShared(T *ptr)
QStringList defaultPrependArgs
bool progressIndeterminate