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 renderLog.open(QIODevice::ReadOnly);
102 sessionRenderLog.open(QIODevice::WriteOnly | QIODevice::Append);
106 while (!(buffer = renderLog.read(chunksize)).isEmpty()) {
107 sessionRenderLog.write(buffer);
116 progressText.replace(
"[progress]",
"0");
120 m_progress->setWindowModality(Qt::ApplicationModal);
135 dbgFile <<
"Open progress dialog!";
149 args << settings.
args;
164 struct ProcessResults {
166 QString error = QString();
172 processResults->finish =
true;
173 processResults->error = errMsg;
177 processResults->finish =
true;
183 if (processResults->finish ==
true) {
184 if (processResults->error.isEmpty()) {
200 return (
m_process->waitForFinished(msecs) &&
m_process->exitStatus() == QProcess::ExitStatus::NormalExit);
216 progressText.replace(
"[progress]", QString::number(progressValue));
218 progressText.replace(
"[suffix]", suffix );
224 QApplication::processEvents();
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);
236 while (invalidStreamMatchList.hasNext()) {
237 QRegularExpressionMatch invalidStreamMatch = invalidStreamMatchList.next();
239 if (invalidStreamMatch.hasMatch()) {
240 const int invalidStreamId = invalidStreamMatch.captured(1).toInt();
243 if (ffprobeJsonObj[
"streams"].toArray()[invalidStreamId].toObject()[
"codec_type"] ==
"video") {
258 if (
m_process->state() != QProcess::NotRunning) {
266 QByteArray stderrRawBuffer =
m_process->readAllStandardError();
275 while ((endPos =
m_stderrBuffer.indexOf(lineDelimiter, startPos)) != -1) {
276 const QString &line =
m_stderrBuffer.mid(startPos, endPos - startPos).trimmed();
282 for (
const QString &word : errorWords) {
283 if (line.contains(word)) {
289 const QRegularExpressionMatch &match = frameRegexp.match(line);
291 if (match.hasMatch()) {
292 frameNo = match.captured(1).toInt();
296 dbgFile <<
"ffmpeg stderr:" << line;
297 startPos = endPos + 1;
311 QByteArray stdoutRawBuffer =
m_process->readAllStandardOutput();
328 while ((endPos =
m_stdoutBuffer.indexOf(lineDelimiter, startPos)) != -1) {
329 const QString &line =
m_stdoutBuffer.mid(startPos, endPos - startPos).trimmed();
331 dbgFile <<
"ffmpeg stdout:" << line;
333 startPos = endPos + 1;
344 dbgFile <<
"ffmpeg process started!";
351 dbgFile <<
"FFMpeg finished with code" << exitCode;
358 if (
m_process->exitStatus() == QProcess::CrashExit) {
380 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
382 const QStringList libraryPaths = env.value(
"LD_LIBRARY_PATH").split(
':');
383 const QString processAbsPath = QFileInfo(QFileInfo(processPath).absolutePath() +
"/../").absoluteFilePath();
385 bool isCustomBuildOfFFmpeg =
false;
387 Q_FOREACH (
const QString &path, libraryPaths) {
388 const QString absPath1 = QFileInfo(path +
"/").absoluteFilePath();
389 const QString absPath2 = QFileInfo(path +
"/../").absoluteFilePath();
391 if (absPath1 == processAbsPath || absPath2 == processAbsPath) {
392 dbgFile <<
"Detected embedded ffmpeg:" << processPath;
397 isCustomBuildOfFFmpeg =
true;
403 if (!isCustomBuildOfFFmpeg) {
404 dbgFile <<
"Removing LD_LIBRARY_PATH for running" << processPath;
406 env.remove(
"LD_LIBRARY_PATH");
407 process.setProcessEnvironment(env);
410 Q_UNUSED(processPath);
421 runProcess.start(processPath, args);
423 if (runProcess.waitForStarted(msecs)) runProcess.waitForFinished(msecs);
425 const bool successfulStart =
426 runProcess.state() == QProcess::NotRunning &&
427 runProcess.error() == QProcess::UnknownError;
429 dbgFile <<
"runProcessAndReturn Success:" << successfulStart;
431 if (successfulStart)
return runProcess.readAllStandardOutput();
450 QJsonObject resultJsonObj;
454 if (!customLocation.isEmpty()) {
455 proposedPaths << customLocation;
456 proposedPaths << customLocation +
'/' + processName;
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/";
479 proposedPaths << QCoreApplication::applicationDirPath() +
'/' + processName;
483 proposedPaths << QDir::homePath() +
"/bin/" + processName;
484 proposedPaths <<
"/usr/bin/" + processName;
485 proposedPaths <<
"/usr/local/bin/" + processName;
489 for (
int i = 0; i != proposedPaths.size(); ++i) {
490 if (proposedPaths[i].isEmpty())
continue;
493 proposedPaths[i] = QDir::toNativeSeparators(QDir::cleanPath(proposedPaths[i]));
495 if (proposedPaths[i].endsWith(
'/')) {
499 if (!proposedPaths[i].endsWith(
".exe", Qt::CaseInsensitive)) {
500 if (!QFile::exists(proposedPaths[i])) {
501 proposedPaths[i] +=
".exe";
502 if (!QFile::exists(proposedPaths[i])) {
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;
518 return resultJsonObj;
524 QJsonObject ffmpegInfo {{
"path", rawProcessPath},
527 {
"encoder",QJsonValue::Object},
528 {
"decoder",QJsonValue::Object}};
530 if (!QFile::exists(rawProcessPath))
return ffmpegInfo;
532 const QString processPath = QFileInfo(rawProcessPath).absoluteFilePath();
533 ffmpegInfo[
"path"] = processPath;
535 dbgFile <<
"Found process at:" << processPath;
538 if (!processVersion.isEmpty()) {
540 QRegularExpressionMatch versionMatch = ffmpegVersionRX.match(processVersion);
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;
548 if (!includeProcessInfo || !ffmpegInfo[
"enabled"].toBool())
return ffmpegInfo;
552 QJsonObject codecsJson {};
557 QRegularExpression ffmpegCodecsRX(
"(D|\\.)(E|\\.)....\\s+(.+?)\\s+([^\\r\\n]*)");
558 QRegularExpressionMatchIterator codecsMatchList = ffmpegCodecsRX.globalMatch(processCodecs);
561 while (codecsMatchList.hasNext()) {
562 QRegularExpressionMatch codecsMatch = codecsMatchList.next();
564 if (codecsMatch.hasMatch()) {
565 QJsonObject codecInfoJson {};
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);
572 codecInfoJson.insert(
"encoding", encodingSupported);
573 codecInfoJson.insert(
"decoding", decodingSupported);
577 QRegularExpression ffmpegSpecificCodecRX(
"\\(decoders:(.+?)\\)|\\(encoders:(.+?)\\)");
578 QRegularExpressionMatchIterator specificCodecMatchList = ffmpegSpecificCodecRX.globalMatch(codecRemainder);
580 QJsonArray encodersList;
581 QJsonArray decodersList;
583 while (specificCodecMatchList.hasNext()) {
584 QRegularExpressionMatch specificCodecMatch = specificCodecMatchList.next();
585 if (specificCodecMatch.hasMatch()) {
587 QStringList decoders = specificCodecMatch.captured(1).split(
" ");
588 Q_FOREACH(
const QString&
string, decoders) {
589 if (!
string.isEmpty()) {
590 decodersList.push_back(
string);
595 QStringList encoders = specificCodecMatch.captured(2).split(
" ");
596 Q_FOREACH(
const QString&
string, encoders) {
597 if (!
string.isEmpty()) {
598 encodersList.push_back(
string);
604 codecInfoJson.insert(
"encoders", encodersList);
605 codecInfoJson.insert(
"decoders", decodersList);
606 codecsJson.insert(codecName, codecInfoJson);
611 ffmpegInfo.insert(
"codecs", codecsJson);
614 dbgFile <<
"codec support:" << ffmpegInfo;
616 dbgFile <<
"Not a valid process at:" << processPath;
632 QJsonObject encoders = ffmpegProcessInfo[
"codecs"].toObject();
633 Q_FOREACH(
const QString& key, encoders.keys()) {
634 if (encoders[key].toObject()[
"encoding"].toBool()) {
635 encodersToReturn << key;
639 return encodersToReturn;
660 ffprobeSettings.
args <<
"-hide_banner"
662 <<
"-of" <<
"json=compact=1"
665 <<
"-i" << inputFile;
672 QJsonDocument ffprobeJsonDoc = QJsonDocument::fromJson(ffprobeSTDOUT.toUtf8());
673 QJsonObject ffprobeJsonObj;
675 if (ffprobeJsonDoc.isNull() || !ffprobeJsonDoc.isObject()) {
677 return ffprobeJsonObj;
680 ffprobeJsonObj = ffprobeJsonDoc.object();
686 return ffprobeJsonObj;
695 ffmpegSettings.
progressMessage = i18nc(
"Video information probing dialog. arg1: frame number.",
"Loading video data... %1 frames examined.",
"[progress]");
698 ffmpegSettings.
args <<
"-stats"
700 <<
"-progress" <<
"pipe:1"
703 <<
"-f" <<
"null" <<
"pipe:1"
704 <<
"-i" << inputFile;
713 dbgFile <<
"ffmpegProbe stdout:" << ffmpegSTDOUT;
715 QJsonObject ffmpegJsonObj;
716 QJsonArray ffmpegStreamsJsonArr;
717 QJsonObject ffmpegFormatJsonObj;
718 QJsonObject ffmpegProgressJsonObj;
720 QStringList stdoutLines = ffmpegSTDOUT.split(
'\n');
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);
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()
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")
742 QRegularExpressionMatch streamMatch = videoInfoStreamRX.match(line);
744 if (streamMatch.hasMatch() && ffmpegStreamsJsonArr[streamMatch.captured(1).toInt()].isUndefined() ) {
745 int index = streamMatch.captured(1).toInt();
746 QJsonObject ffmpegJsonOnStreamObj;
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();
755 ffmpegJsonOnStreamObj[
"codec_type"] =
"video";
757 if (streamMatch.captured(11).toFloat() > 0) {
758 float fps = streamMatch.captured(11).toFloat();
760 ffmpegProgressJsonObj[
"ffmpeg_fps"] = QString::number(fps);
761 ffmpegJsonOnStreamObj[
"r_frame_rate"] = QString::number( fps * 10000 ).append(QString(
"/10000"));
763 ffmpegProgressJsonObj[
"ffmpeg_fps"] = 0;
768 dbgFile <<
"ffmpegProbe stream:" << ffmpegJsonOnStreamObj;
770 ffmpegStreamsJsonArr.insert(index, ffmpegJsonOnStreamObj);
774 QRegularExpression videoInfoFrameRX(
"^(\\w+?)=([\\w\\./:]+?)$");
775 QRegularExpressionMatch frameMatch = videoInfoFrameRX.match(line);
777 if (frameMatch.hasMatch()) ffmpegProgressJsonObj[frameMatch.captured(1)] = frameMatch.captured(2);
783 ffmpegJsonObj.insert(
"streams",ffmpegStreamsJsonArr);
784 ffmpegJsonObj.insert(
"format",ffmpegFormatJsonObj);
785 ffmpegJsonObj.insert(
"progress",ffmpegProgressJsonObj);
787 return ffmpegJsonObj;
792 if (name ==
"bt709") {
795 if (name ==
"bt470m") {
798 if (name ==
"bt470bg") {
801 if (name ==
"smpte170m") {
804 if (name ==
"smpte240m") {
807 if (name ==
"film") {
810 if (name ==
"bt2020") {
813 if (name.startsWith(
"smpte428")) {
816 if (name ==
"smpte431") {
819 if (name ==
"smpte432") {
822 if (name ==
"jedec-p22") {
831 if (name ==
"bt709") {
834 if (name ==
"gamma22") {
837 if (name ==
"gamma28") {
840 if (name ==
"smpte170m") {
843 if (name ==
"smpte240m") {
846 if (name ==
"linear") {
849 if (name ==
"log" || name ==
"log100") {
852 if (name ==
"log316" || name ==
"log_sqrt") {
855 if (name ==
"iec61966_2_4" || name ==
"iec61966-2-4") {
858 if (name.startsWith(
"bt1361")) {
861 if (name ==
"iec61966_2_1" || name ==
"iec61966-2-1") {
864 if (name.startsWith(
"bt2020_10")) {
867 if (name.startsWith(
"bt2020_12")) {
870 if (name ==
"smpte2084") {
873 if (name ==
"smpte240m") {
876 if (name.startsWith(
"smpte428")) {
879 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