// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "ffmpegutils.h" #include "screenrecordersettings.h" #include "screenrecordertr.h" #ifdef WITH_TESTS #include "screenrecorder_test.h" #include #endif // WITH_TESTS #include #include #include #include #include #include #include #include #include #include #include using namespace Utils; namespace ScreenRecorder { int ClipInfo::framesCount() const { return int(duration * rFrameRate); } qreal ClipInfo::secondForFrame(int frame) const { return frame / rFrameRate; } QString ClipInfo::timeStamp(int frame) const { const qreal seconds = secondForFrame(frame); const QString format = QLatin1String(seconds >= 60 * 60 ? "HH:mm:ss.zzz" : "mm:ss.zzz"); return QTime::fromMSecsSinceStartOfDay(int(seconds * 1000)).toString(format); } bool ClipInfo::isNull() const { return qFuzzyCompare(duration, -1); } bool ClipInfo::isCompleteArea(const QRect &rect) const { return rect == QRect(QPoint(), dimensions); } bool ClipInfo::isCompleteRange(FrameRange range) const { return (range.first == 0 && (range.second == 0 || range.second == framesCount())); } bool ClipInfo::isLossless() const { return codec == "qtrle" && pixFmt == "rgb24"; // TODO: Find out how to properly determine "lossless" via ffprobe } TimeLabel::TimeLabel(const ClipInfo &clipInfo, QWidget *parent) : QLabel(parent) , m_clipInfo(clipInfo) { setFrame(0); } void TimeLabel::setFrame(int frame) { m_frame = frame; const QString timeStamp = m_clipInfo.timeStamp(m_frame); const int maxFrameDigits = qCeil(log10(double(m_clipInfo.framesCount() + 1))); const QString label = QString("%1 (%2)") .arg(m_frame, maxFrameDigits, 10, QLatin1Char('0')) .arg(timeStamp); setText(label); } int TimeLabel::frame() const { return m_frame; } constexpr QSize warningIconSize(16, 16); CropSizeWarningIcon::CropSizeWarningIcon(IconVariant backgroundType, QWidget *parent) : QWidget(parent) , m_iconVariant(backgroundType) { setMinimumSize(warningIconSize); setToolTip(Tr::tr("Width and height are not both divisible by 2. " "The video export for some of the lossy formats will not work.")); m_updateTimer = new QTimer(this); m_updateTimer->setInterval(350); m_updateTimer->setSingleShot(true); m_updateTimer->callOnTimeout(this, &CropSizeWarningIcon::updateVisibility); } void CropSizeWarningIcon::setCropSize(const QSize &size) { m_cropSize = size; m_updateTimer->stop(); if (needsWarning()) m_updateTimer->start(); else setVisible(false); } void CropSizeWarningIcon::paintEvent(QPaintEvent*) { static const QIcon standardIcon = Icons::WARNING.icon(); static const QIcon toolBarIcon = Icons::WARNING_TOOLBAR.icon(); QRect iconRect(QPoint(), warningIconSize); iconRect.moveCenter(rect().center()); QPainter p(this); (m_iconVariant == StandardVariant ? standardIcon : toolBarIcon).paint(&p, iconRect); } void CropSizeWarningIcon::updateVisibility() { setVisible(needsWarning()); } bool CropSizeWarningIcon::needsWarning() const { return (m_cropSize.width() % 2 == 1) || (m_cropSize.height() % 2 == 1); } namespace FFmpegUtils { static ClipInfo parseClipInfo(const QByteArray &toolOutput) { ClipInfo result; const QJsonObject jsonObject = QJsonDocument::fromJson(toolOutput).object(); if (const QJsonArray streams = jsonObject.value("streams").toArray(); !streams.isEmpty()) { // With more than 1 video stream, the first one is often just a 1-frame thumbnail const int streamIndex = int(qMin(streams.count() - 1, 1)); const QJsonObject stream = streams.at(streamIndex).toObject(); if (const QJsonValue index = stream.value("index"); !index.isUndefined()) result.streamIdex = index.toInt(); if (const QJsonValue width = stream.value("width"); !width.isUndefined()) result.dimensions.setWidth(width.toInt()); if (const QJsonValue height = stream.value("height"); !height.isUndefined()) result.dimensions.setHeight(height.toInt()); if (const QJsonValue rFrameRate = stream.value("r_frame_rate"); !rFrameRate.isUndefined()) { const QStringList frNumbers = rFrameRate.toString().split('/'); result.rFrameRate = frNumbers.count() == 2 ? frNumbers.first().toDouble() / qMax(1, frNumbers.last().toInt()) : frNumbers.first().toInt(); } if (const QJsonValue codecName = stream.value("codec_name"); !codecName.isUndefined()) result.codec = codecName.toString(); if (const QJsonValue pixFmt = stream.value("pix_fmt"); !pixFmt.isUndefined()) result.pixFmt = pixFmt.toString(); } if (const QJsonObject format = jsonObject.value("format").toObject(); !format.isEmpty()) { if (const QJsonValue duration = format.value("duration"); !duration.isUndefined()) result.duration = duration.toString().toDouble(); } return result; } ClipInfo clipInfo(const FilePath &path) { Process proc; const CommandLine cl{ Internal::settings().ffprobeTool(), { "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-select_streams", "V", path.toUserOutput() } }; proc.setCommand(cl); proc.runBlocking(); const QByteArray output = proc.rawStdOut(); ClipInfo result = parseClipInfo(output); result.file = path; return result; } int parseFrameProgressFromOutput(const QByteArray &output) { static const QRegularExpression re(R"(^frame=\s*(?\d+))"); const QRegularExpressionMatch match = re.match(QString::fromUtf8(output)); if (match.hasMatch()) if (const QString frame = match.captured("frame"); !frame.isEmpty()) return frame.toInt(); return -1; } void sendQuitCommand(Process *proc) { if (proc && proc->processMode() == ProcessMode::Writer && proc->isRunning()) proc->writeRaw("q"); } void killFfmpegProcess(Process *proc) { sendQuitCommand(proc); if (proc->isRunning()) proc->kill(); } void reportError(const CommandLine &cmdLn, const QByteArray &error) { if (!Internal::settings().logFfmpegCommandline()) Core::MessageManager::writeSilently(cmdLn.toUserOutput()); Core::MessageManager::writeDisrupting("\n" + QString::fromUtf8(error)); } void logFfmpegCall(const CommandLine &cmdLn) { if (Internal::settings().logFfmpegCommandline()) Core::MessageManager::writeSilently(cmdLn.toUserOutput()); } } // namespace FFmpegUtils } // namespace ScreenRecorder #ifdef WITH_TESTS using namespace ScreenRecorder::FFmpegUtils; namespace ScreenRecorder::Internal { static QVersionNumber parseVersionNumber(const QByteArray &toolOutput) { QVersionNumber result; const QJsonObject jsonObject = QJsonDocument::fromJson(toolOutput).object(); if (const QJsonObject program_version = jsonObject.value("program_version").toObject(); !program_version.isEmpty()) { if (const QJsonValue version = program_version.value("version"); !version.isUndefined()) result = QVersionNumber::fromString(version.toString()); } return result; } void FFmpegOutputParserTest::testVersionParser_data() { QTest::addColumn("ffprobeVersionOutput"); QTest::addColumn("versionNumber"); QTest::newRow("4.2.3") << QByteArray( R"_({ "program_version": { "version": "4.4.2-0ubuntu0.22.04.1", "copyright": "Copyright (c) 2007-2021 the FFmpeg developers", "compiler_ident": "gcc 11 (Ubuntu 11.2.0-19ubuntu1)", "configuration": "--prefix=/usr --extra-version=0ubuntu0.22.04.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-pocketsphinx --enable-librsvg --enable-libmfx --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared" } })_") << QVersionNumber(4, 4, 2); } void FFmpegOutputParserTest::testVersionParser() { QFETCH(QByteArray, ffprobeVersionOutput); QFETCH(QVersionNumber, versionNumber); const QVersionNumber v = parseVersionNumber(ffprobeVersionOutput); QCOMPARE(v, versionNumber); } void FFmpegOutputParserTest::testClipInfoParser_data() { QTest::addColumn("ffmpegVersionOutput"); QTest::addColumn("clipInfo"); // ffprobe -v quiet -print_format json -show_format -show_streams -select_streams V