forked from qt-creator/qt-creator
Introduce the Screen Recorder Plugin
The screen recorder plugin allows a user to record the contents of a screen (or part thereof), and in a second step to export the result as various lossless or lossy animated picture or video formats. Before exporting, the recorded video can be trimmend in length and be cropped in size. All functionality relies on the ffmpeg/ffprobe tools, which need to be present on the user's system. This initial version of the plugin introduces a settings page, and a recording/exporting dialog with sub dialogs for recording and export options. Some autottests for ffmpeg/ffprobe output parsing are included. Task-number: QTCREATORBUG-29366 Change-Id: Iaf16d369fd9088d69b1bd130185ca920d07b34c6 Reviewed-by: hjk <hjk@qt.io> Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
This commit is contained in:
@@ -7,6 +7,7 @@ add_subdirectory(serialterminal)
|
|||||||
add_subdirectory(helloworld)
|
add_subdirectory(helloworld)
|
||||||
add_subdirectory(imageviewer)
|
add_subdirectory(imageviewer)
|
||||||
add_subdirectory(marketplace)
|
add_subdirectory(marketplace)
|
||||||
|
add_subdirectory(screenrecorder)
|
||||||
add_subdirectory(updateinfo)
|
add_subdirectory(updateinfo)
|
||||||
add_subdirectory(welcome)
|
add_subdirectory(welcome)
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ Project {
|
|||||||
"remotelinux/remotelinux.qbs",
|
"remotelinux/remotelinux.qbs",
|
||||||
"resourceeditor/resourceeditor.qbs",
|
"resourceeditor/resourceeditor.qbs",
|
||||||
"saferenderer/saferenderer.qbs",
|
"saferenderer/saferenderer.qbs",
|
||||||
|
"screenrecorder/screenrecorder.qbs",
|
||||||
"scxmleditor/scxmleditor.qbs",
|
"scxmleditor/scxmleditor.qbs",
|
||||||
"serialterminal/serialterminal.qbs",
|
"serialterminal/serialterminal.qbs",
|
||||||
"silversearcher/silversearcher.qbs",
|
"silversearcher/silversearcher.qbs",
|
||||||
|
|||||||
19
src/plugins/screenrecorder/CMakeLists.txt
Normal file
19
src/plugins/screenrecorder/CMakeLists.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
add_qtc_plugin(ScreenRecorder
|
||||||
|
PLUGIN_DEPENDS Core
|
||||||
|
SOURCES
|
||||||
|
cropandtrim.cpp cropandtrim.h
|
||||||
|
export.cpp export.h
|
||||||
|
ffmpegutils.cpp ffmpegutils.h
|
||||||
|
record.cpp record.h
|
||||||
|
screenrecorder.qrc
|
||||||
|
screenrecorderconstants.h
|
||||||
|
screenrecorderplugin.cpp
|
||||||
|
screenrecordersettings.cpp screenrecordersettings.h
|
||||||
|
)
|
||||||
|
|
||||||
|
extend_qtc_plugin(ScreenRecorder
|
||||||
|
CONDITION WITH_TESTS
|
||||||
|
SOURCES
|
||||||
|
screenrecorder_test.cpp screenrecorder_test.h
|
||||||
|
EXPLICIT_MOC screenrecorder_test.h
|
||||||
|
)
|
||||||
19
src/plugins/screenrecorder/ScreenRecorder.json.in
Normal file
19
src/plugins/screenrecorder/ScreenRecorder.json.in
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Name" : "ScreenRecorder",
|
||||||
|
"Version" : "${IDE_VERSION}",
|
||||||
|
"CompatVersion" : "${IDE_VERSION_COMPAT}",
|
||||||
|
"Vendor" : "The Qt Company Ltd",
|
||||||
|
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||||
|
"License" : [ "Commercial Usage",
|
||||||
|
"",
|
||||||
|
"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and The Qt Company.",
|
||||||
|
"",
|
||||||
|
"GNU General Public License Usage",
|
||||||
|
"",
|
||||||
|
"Alternatively, this plugin may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this plugin. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html."
|
||||||
|
],
|
||||||
|
"Experimental" : true,
|
||||||
|
"Description" : "Screen recording.",
|
||||||
|
"Url" : "http://www.qt.io",
|
||||||
|
${IDE_PLUGIN_DEPENDENCIES}
|
||||||
|
}
|
||||||
746
src/plugins/screenrecorder/cropandtrim.cpp
Normal file
746
src/plugins/screenrecorder/cropandtrim.cpp
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
// 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 "cropandtrim.h"
|
||||||
|
|
||||||
|
#include "ffmpegutils.h"
|
||||||
|
#include "screenrecordersettings.h"
|
||||||
|
#include "screenrecordertr.h"
|
||||||
|
|
||||||
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/process.h>
|
||||||
|
#include <utils/qtcsettings.h>
|
||||||
|
#include <utils/styledbar.h>
|
||||||
|
#include <utils/stylehelper.h>
|
||||||
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QSlider>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QStyleOptionSlider>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
using namespace Utils;
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
CropScene::CropScene(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setMouseTracking(true);
|
||||||
|
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::paintEvent(QPaintEvent *event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event)
|
||||||
|
QPainter p(this);
|
||||||
|
p.drawImage(QPointF(), m_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::initMouseInteraction(const QPoint &imagePos)
|
||||||
|
{
|
||||||
|
static const auto inGripRange = [](int grip, int pos, int &clickOffset) -> bool {
|
||||||
|
const bool inRange = pos - m_gripWidth <= grip && pos + m_gripWidth >= grip;
|
||||||
|
if (inRange)
|
||||||
|
clickOffset = grip - pos;
|
||||||
|
return inRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inGripRange(imagePos.x(), m_cropRect.left(), m_mouse.clickOffset)) {
|
||||||
|
m_mouse.margin = EdgeLeft;
|
||||||
|
m_mouse.cursorShape = Qt::SizeHorCursor;
|
||||||
|
} else if (inGripRange(imagePos.x(), m_cropRect.right(),
|
||||||
|
m_mouse.clickOffset)) {
|
||||||
|
m_mouse.margin = EdgeRight;
|
||||||
|
m_mouse.cursorShape = Qt::SizeHorCursor;
|
||||||
|
} else if (inGripRange(imagePos.y(), m_cropRect.top(), m_mouse.clickOffset)) {
|
||||||
|
m_mouse.margin = EdgeTop;
|
||||||
|
m_mouse.cursorShape = Qt::SizeVerCursor;
|
||||||
|
} else if (inGripRange(imagePos.y(), m_cropRect.bottom(),
|
||||||
|
m_mouse.clickOffset)) {
|
||||||
|
m_mouse.margin = EdgeBottom;
|
||||||
|
m_mouse.cursorShape = Qt::SizeVerCursor;
|
||||||
|
} else {
|
||||||
|
m_mouse.margin = Free;
|
||||||
|
m_mouse.cursorShape = Qt::ArrowCursor;
|
||||||
|
m_mouse.clickOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::updateBuffer()
|
||||||
|
{
|
||||||
|
if (m_buffer.isNull())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_buffer.fill(palette().window().color());
|
||||||
|
const qreal dpr = m_image->devicePixelRatioF();
|
||||||
|
QPainter p(&m_buffer);
|
||||||
|
p.drawImage(lineWidth, lineWidth, *m_image);
|
||||||
|
|
||||||
|
const qreal lineOffset = lineWidth / 2.0;
|
||||||
|
const QRectF r = {
|
||||||
|
m_cropRect.x() / dpr + lineOffset,
|
||||||
|
m_cropRect.y() / dpr + lineOffset,
|
||||||
|
m_cropRect.width() / dpr + lineWidth,
|
||||||
|
m_cropRect.height() / dpr + lineWidth
|
||||||
|
};
|
||||||
|
|
||||||
|
p.save();
|
||||||
|
p.setClipRegion(QRegion(m_buffer.rect()).subtracted(QRegion(r.toRect())));
|
||||||
|
p.setOpacity(0.85);
|
||||||
|
p.fillRect(m_buffer.rect(), qRgb(0x30, 0x30, 0x30));
|
||||||
|
p.restore();
|
||||||
|
|
||||||
|
const auto paintLine = [&p](const QLineF &line)
|
||||||
|
{
|
||||||
|
const QPen blackPen(Qt::black, lineWidth);
|
||||||
|
p.setPen(blackPen);
|
||||||
|
p.drawLine(line);
|
||||||
|
const QPen whiteDotPen(Qt::white, lineWidth, Qt::DotLine);
|
||||||
|
p.setPen(whiteDotPen);
|
||||||
|
p.drawLine(line);
|
||||||
|
};
|
||||||
|
paintLine(QLineF(r.left(), 0, r.left(), m_buffer.height()));
|
||||||
|
paintLine(QLineF(0, r.top(), m_buffer.width(), r.top()));
|
||||||
|
paintLine(QLineF(r.right(), 0, r.right(), m_buffer.height()));
|
||||||
|
paintLine(QLineF(0, r.bottom(), m_buffer.width(), r.bottom()));
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint CropScene::toImagePos(const QPoint &widgetPos)
|
||||||
|
{
|
||||||
|
const int dpr = int(m_image->devicePixelRatio());
|
||||||
|
return {(widgetPos.x() - lineWidth) * dpr, (widgetPos.y() - lineWidth) * dpr};
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect CropScene::cropRect() const
|
||||||
|
{
|
||||||
|
return m_cropRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::setCropRect(const QRect &rect)
|
||||||
|
{
|
||||||
|
m_cropRect = rect;
|
||||||
|
updateBuffer();
|
||||||
|
emit cropRectChanged(m_cropRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CropScene::fullySelected() const
|
||||||
|
{
|
||||||
|
return m_image && m_image->rect() == m_cropRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::setFullySelected()
|
||||||
|
{
|
||||||
|
if (!m_image)
|
||||||
|
return;
|
||||||
|
setCropRect(m_image->rect());
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect CropScene::fullRect() const
|
||||||
|
{
|
||||||
|
return m_image ? m_image->rect() : QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::setImage(const QImage &image)
|
||||||
|
{
|
||||||
|
m_image = ℑ
|
||||||
|
const QSize sceneSize = m_image->deviceIndependentSize().toSize()
|
||||||
|
.grownBy({lineWidth, lineWidth, lineWidth, lineWidth});
|
||||||
|
const QSize sceneSizeDpr = sceneSize * m_image->devicePixelRatio();
|
||||||
|
m_buffer = QImage(sceneSizeDpr, QImage::Format_RGB32);
|
||||||
|
m_buffer.setDevicePixelRatio(m_image->devicePixelRatio());
|
||||||
|
updateBuffer();
|
||||||
|
resize(sceneSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::mouseMoveEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
const QPoint imagePos = toImagePos(event->pos());
|
||||||
|
|
||||||
|
if (m_mouse.dragging) {
|
||||||
|
switch (m_mouse.margin) {
|
||||||
|
case EdgeLeft:
|
||||||
|
m_cropRect.setLeft(qBound(0, imagePos.x() - m_mouse.clickOffset,
|
||||||
|
m_cropRect.right()));
|
||||||
|
break;
|
||||||
|
case EdgeTop:
|
||||||
|
m_cropRect.setTop(qBound(0, imagePos.y() - m_mouse.clickOffset,
|
||||||
|
m_cropRect.bottom()));
|
||||||
|
break;
|
||||||
|
case EdgeRight:
|
||||||
|
m_cropRect.setRight(qBound(m_cropRect.left(), imagePos.x() - m_mouse.clickOffset,
|
||||||
|
m_image->width() - 1));
|
||||||
|
break;
|
||||||
|
case EdgeBottom:
|
||||||
|
m_cropRect.setBottom(qBound(m_cropRect.top(), imagePos.y() - m_mouse.clickOffset,
|
||||||
|
m_image->height() - 1));
|
||||||
|
break;
|
||||||
|
case Free: {
|
||||||
|
m_cropRect = QRect(m_mouse.startImagePos, imagePos).normalized()
|
||||||
|
.intersected(m_image->rect());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit cropRectChanged(m_cropRect);
|
||||||
|
updateBuffer();
|
||||||
|
} else {
|
||||||
|
initMouseInteraction(imagePos);
|
||||||
|
setCursor(m_mouse.cursorShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget::mouseMoveEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::mousePressEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
const QPoint imagePos = toImagePos(event->pos());
|
||||||
|
|
||||||
|
m_mouse.dragging = true;
|
||||||
|
m_mouse.startImagePos = imagePos;
|
||||||
|
|
||||||
|
QWidget::mousePressEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropScene::mouseReleaseEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
m_mouse.dragging = false;
|
||||||
|
setCursor(Qt::ArrowCursor);
|
||||||
|
|
||||||
|
QWidget::mouseReleaseEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CropWidget : public QWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit CropWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
QRect cropRect() const;
|
||||||
|
void setCropRect(const QRect &rect);
|
||||||
|
|
||||||
|
void setImage(const QImage &image);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateWidgets();
|
||||||
|
void onSpinBoxChanged();
|
||||||
|
void onCropRectChanged();
|
||||||
|
|
||||||
|
CropScene *m_cropScene;
|
||||||
|
|
||||||
|
QSpinBox *m_xSpinBox;
|
||||||
|
QSpinBox *m_ySpinBox;
|
||||||
|
QSpinBox *m_widthSpinBox;
|
||||||
|
QSpinBox *m_heightSpinBox;
|
||||||
|
|
||||||
|
CropSizeWarningIcon *m_warningIcon;
|
||||||
|
QToolButton *m_resetButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
CropWidget::CropWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
m_cropScene = new CropScene;
|
||||||
|
|
||||||
|
auto scrollArea = new QScrollArea;
|
||||||
|
scrollArea->setWidget(m_cropScene);
|
||||||
|
|
||||||
|
for (auto s : {&m_xSpinBox, &m_ySpinBox, &m_widthSpinBox, &m_heightSpinBox}) {
|
||||||
|
*s = new QSpinBox;
|
||||||
|
(*s)->setMaximum(99999); // Will be adjusted in CropWidget::setImage
|
||||||
|
(*s)->setSuffix(" px");
|
||||||
|
}
|
||||||
|
m_widthSpinBox->setMinimum(1);
|
||||||
|
m_heightSpinBox->setMinimum(1);
|
||||||
|
|
||||||
|
m_resetButton = new QToolButton;
|
||||||
|
m_resetButton->setIcon(Icons::RESET.icon());
|
||||||
|
|
||||||
|
m_warningIcon = new CropSizeWarningIcon(CropSizeWarningIcon::StandardVariant);
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Column {
|
||||||
|
scrollArea,
|
||||||
|
Row {
|
||||||
|
Tr::tr("X:"), m_xSpinBox,
|
||||||
|
Space(4), Tr::tr("Y:"), m_ySpinBox,
|
||||||
|
Space(16), Tr::tr("Width:"), m_widthSpinBox,
|
||||||
|
Space(4), Tr::tr("Height:"), m_heightSpinBox,
|
||||||
|
m_resetButton,
|
||||||
|
m_warningIcon,
|
||||||
|
st,
|
||||||
|
},
|
||||||
|
noMargin(),
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
connect(m_xSpinBox, &QSpinBox::valueChanged, this, &CropWidget::onSpinBoxChanged);
|
||||||
|
connect(m_ySpinBox, &QSpinBox::valueChanged, this, &CropWidget::onSpinBoxChanged);
|
||||||
|
connect(m_widthSpinBox, &QSpinBox::valueChanged, this, &CropWidget::onSpinBoxChanged);
|
||||||
|
connect(m_heightSpinBox, &QSpinBox::valueChanged, this, &CropWidget::onSpinBoxChanged);
|
||||||
|
connect(m_cropScene, &CropScene::cropRectChanged, this, &CropWidget::onCropRectChanged);
|
||||||
|
connect(m_resetButton, &QToolButton::pressed, this, [this] {
|
||||||
|
m_cropScene->setFullySelected();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect CropWidget::cropRect() const
|
||||||
|
{
|
||||||
|
return m_cropScene->cropRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropWidget::setCropRect(const QRect &rect)
|
||||||
|
{
|
||||||
|
m_cropScene->setCropRect(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropWidget::setImage(const QImage &image)
|
||||||
|
{
|
||||||
|
const QRect rBefore = m_cropScene->fullRect();
|
||||||
|
m_cropScene->setImage(image);
|
||||||
|
const QRect rAfter = m_cropScene->fullRect();
|
||||||
|
if (rBefore != rAfter) {
|
||||||
|
m_xSpinBox->setMaximum(rAfter.width() - 1);
|
||||||
|
m_ySpinBox->setMaximum(rAfter.height() - 1);
|
||||||
|
m_widthSpinBox->setMaximum(rAfter.width());
|
||||||
|
m_heightSpinBox->setMaximum(rAfter.height());
|
||||||
|
}
|
||||||
|
updateWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropWidget::updateWidgets()
|
||||||
|
{
|
||||||
|
m_resetButton->setEnabled(!m_cropScene->fullySelected());
|
||||||
|
const QRect r = m_cropScene->cropRect();
|
||||||
|
m_warningIcon->setCropSize(r.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropWidget::onSpinBoxChanged()
|
||||||
|
{
|
||||||
|
const QRect spinBoxRect = {
|
||||||
|
m_xSpinBox->value(),
|
||||||
|
m_ySpinBox->value(),
|
||||||
|
m_widthSpinBox->value(),
|
||||||
|
m_heightSpinBox->value()
|
||||||
|
};
|
||||||
|
m_cropScene->setCropRect(spinBoxRect.intersected(m_cropScene->fullRect()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropWidget::onCropRectChanged()
|
||||||
|
{
|
||||||
|
const QRect rect = m_cropScene->cropRect();
|
||||||
|
const struct { QSpinBox *spinBox; int value; } updates[] = {
|
||||||
|
{m_xSpinBox, rect.x()},
|
||||||
|
{m_ySpinBox, rect.y()},
|
||||||
|
{m_widthSpinBox, rect.width()},
|
||||||
|
{m_heightSpinBox, rect.height()},
|
||||||
|
};
|
||||||
|
for (const auto &update : updates) {
|
||||||
|
update.spinBox->blockSignals(true);
|
||||||
|
update.spinBox->setValue(update.value);
|
||||||
|
update.spinBox->blockSignals(false);
|
||||||
|
}
|
||||||
|
updateWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectionSlider : public QSlider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SelectionSlider(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setSelectionRange(FrameRange range);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FrameRange m_range;
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectionSlider::SelectionSlider(QWidget *parent)
|
||||||
|
: QSlider(Qt::Horizontal, parent)
|
||||||
|
, m_range({-1, -1})
|
||||||
|
{
|
||||||
|
setPageStep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionSlider::setSelectionRange(FrameRange range)
|
||||||
|
{
|
||||||
|
m_range = range;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionSlider::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QStyleOptionSlider opt;
|
||||||
|
initStyleOption(&opt);
|
||||||
|
opt.subControls = QStyle::SC_SliderHandle; // Draw only the handle. We draw the rest, here.
|
||||||
|
|
||||||
|
const int tickOffset = style()->pixelMetric(QStyle::PM_SliderTickmarkOffset, &opt, this);
|
||||||
|
QRect grooveRect = style()->subControlRect(QStyle::CC_Slider, &opt,
|
||||||
|
QStyle::SC_SliderGroove, this)
|
||||||
|
.adjusted(tickOffset, 0, -tickOffset, 0);
|
||||||
|
grooveRect.setTop(rect().top());
|
||||||
|
grooveRect.setBottom(rect().bottom());
|
||||||
|
const QColor bgColor = palette().window().color();
|
||||||
|
const QColor fgColor = palette().text().color();
|
||||||
|
const QColor grooveColor = StyleHelper::mergedColors(bgColor, fgColor, 80);
|
||||||
|
const QColor selectionColor = StyleHelper::mergedColors(bgColor, fgColor, 45);
|
||||||
|
QPainter p(this);
|
||||||
|
p.fillRect(grooveRect, grooveColor);
|
||||||
|
const qreal pixelsPerFrame = grooveRect.width() / qreal(maximum());
|
||||||
|
const int startPixels = int(m_range.first * pixelsPerFrame);
|
||||||
|
const int endPixels = int((maximum() - m_range.second) * pixelsPerFrame);
|
||||||
|
p.fillRect(grooveRect.adjusted(startPixels, 0, -endPixels, 0), selectionColor);
|
||||||
|
style()->drawComplexControl(QStyle::CC_Slider, &opt, &p, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrimWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TrimWidget(const ClipInfo &clip, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setCurrentFrame(int frame);
|
||||||
|
int currentFrame() const;
|
||||||
|
void setTrimRange(FrameRange range);
|
||||||
|
FrameRange trimRange() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void positionChanged();
|
||||||
|
void trimRangeChanged(FrameRange range);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resetTrimRange();
|
||||||
|
void updateTrimWidgets();
|
||||||
|
void emitTrimRangeChange();
|
||||||
|
|
||||||
|
ClipInfo m_clipInfo;
|
||||||
|
SelectionSlider *m_frameSlider;
|
||||||
|
TimeLabel *m_currentTime;
|
||||||
|
TimeLabel *m_clipDuration;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
QPushButton *button;
|
||||||
|
TimeLabel *timeLabel;
|
||||||
|
} m_trimStart, m_trimEnd;
|
||||||
|
TimeLabel *m_trimRange;
|
||||||
|
QToolButton *m_trimResetButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
TrimWidget::TrimWidget(const ClipInfo &clip, QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_clipInfo(clip)
|
||||||
|
{
|
||||||
|
m_frameSlider = new SelectionSlider;
|
||||||
|
|
||||||
|
m_currentTime = new TimeLabel(m_clipInfo);
|
||||||
|
|
||||||
|
m_clipDuration = new TimeLabel(m_clipInfo);
|
||||||
|
|
||||||
|
m_trimStart.button = new QPushButton(Tr::tr("Start:"));
|
||||||
|
m_trimStart.timeLabel = new TimeLabel(m_clipInfo);
|
||||||
|
|
||||||
|
m_trimEnd.button = new QPushButton(Tr::tr("End:"));
|
||||||
|
m_trimEnd.timeLabel = new TimeLabel(m_clipInfo);
|
||||||
|
|
||||||
|
m_trimRange = new TimeLabel(m_clipInfo);
|
||||||
|
|
||||||
|
m_trimResetButton = new QToolButton;
|
||||||
|
m_trimResetButton->setIcon(Icons::RESET.icon());
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Column {
|
||||||
|
Row { m_frameSlider, m_currentTime, "/", m_clipDuration },
|
||||||
|
Group {
|
||||||
|
title(Tr::tr("Trimming")),
|
||||||
|
Row {
|
||||||
|
m_trimStart.button, m_trimStart.timeLabel,
|
||||||
|
Space(20),
|
||||||
|
m_trimEnd.button, m_trimEnd.timeLabel,
|
||||||
|
Stretch(), Space(20),
|
||||||
|
Tr::tr("Range:"), m_trimRange,
|
||||||
|
m_trimResetButton,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
noMargin(),
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
connect(m_frameSlider, &QSlider::valueChanged, this, [this]() {
|
||||||
|
m_currentTime->setFrame(currentFrame());
|
||||||
|
updateTrimWidgets();
|
||||||
|
emit positionChanged();
|
||||||
|
});
|
||||||
|
connect(m_trimStart.button, &QPushButton::clicked, this, [this] (){
|
||||||
|
m_trimStart.timeLabel->setFrame(currentFrame());
|
||||||
|
updateTrimWidgets();
|
||||||
|
emitTrimRangeChange();
|
||||||
|
});
|
||||||
|
connect(m_trimEnd.button, &QPushButton::clicked, this, [this] (){
|
||||||
|
m_trimEnd.timeLabel->setFrame(currentFrame());
|
||||||
|
updateTrimWidgets();
|
||||||
|
emitTrimRangeChange();
|
||||||
|
});
|
||||||
|
connect(m_trimResetButton, &QToolButton::clicked, this, &TrimWidget::resetTrimRange);
|
||||||
|
|
||||||
|
m_frameSlider->setMaximum(m_clipInfo.framesCount());
|
||||||
|
m_currentTime->setFrame(currentFrame());
|
||||||
|
m_clipDuration->setFrame(m_clipInfo.framesCount());
|
||||||
|
resetTrimRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrimWidget::setCurrentFrame(int frame)
|
||||||
|
{
|
||||||
|
m_frameSlider->setValue(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
int TrimWidget::currentFrame() const
|
||||||
|
{
|
||||||
|
return m_frameSlider->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrimWidget::setTrimRange(FrameRange range)
|
||||||
|
{
|
||||||
|
m_trimStart.timeLabel->setFrame(range.first);
|
||||||
|
m_trimEnd.timeLabel->setFrame(range.second);
|
||||||
|
m_frameSlider->setSelectionRange(trimRange());
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameRange TrimWidget::trimRange() const
|
||||||
|
{
|
||||||
|
return { m_trimStart.timeLabel->frame(), m_trimEnd.timeLabel->frame() };
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrimWidget::resetTrimRange()
|
||||||
|
{
|
||||||
|
setTrimRange({0, m_clipInfo.framesCount()});
|
||||||
|
emitTrimRangeChange();
|
||||||
|
updateTrimWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrimWidget::updateTrimWidgets()
|
||||||
|
{
|
||||||
|
const int current = currentFrame();
|
||||||
|
const int trimStart = m_trimStart.timeLabel->frame();
|
||||||
|
const int trimEnd = m_trimEnd.timeLabel->frame();
|
||||||
|
m_trimStart.button->setEnabled(current < m_clipInfo.framesCount() && current < trimEnd);
|
||||||
|
m_trimEnd.button->setEnabled(current > 0 && current > trimStart);
|
||||||
|
m_trimRange->setFrame(trimEnd - trimStart);
|
||||||
|
m_frameSlider->setSelectionRange(trimRange());
|
||||||
|
m_trimResetButton->setEnabled(!m_clipInfo.isCompleteRange(trimRange()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrimWidget::emitTrimRangeChange()
|
||||||
|
{
|
||||||
|
emit trimRangeChanged(trimRange());
|
||||||
|
}
|
||||||
|
|
||||||
|
class CropAndTrimDialog : public QDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit CropAndTrimDialog(const ClipInfo &clip, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setCropRect(const QRect &rect);
|
||||||
|
QRect cropRect() const;
|
||||||
|
void setTrimRange(FrameRange range);
|
||||||
|
FrameRange trimRange() const;
|
||||||
|
|
||||||
|
int currentFrame() const;
|
||||||
|
void setCurrentFrame(int frame);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onSeekPositionChanged();
|
||||||
|
void startFrameFetch();
|
||||||
|
|
||||||
|
ClipInfo m_clipInfo;
|
||||||
|
CropWidget *m_cropWidget;
|
||||||
|
TrimWidget *m_trimWidget;
|
||||||
|
QImage m_previewImage;
|
||||||
|
|
||||||
|
Process *m_process;
|
||||||
|
int m_nextFetchFrame = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
CropAndTrimDialog::CropAndTrimDialog(const ClipInfo &clip, QWidget *parent)
|
||||||
|
: QDialog(parent, Qt::Window)
|
||||||
|
, m_clipInfo(clip)
|
||||||
|
{
|
||||||
|
setWindowTitle(Tr::tr("Crop and Trim"));
|
||||||
|
|
||||||
|
m_cropWidget = new CropWidget;
|
||||||
|
|
||||||
|
m_trimWidget = new TrimWidget(m_clipInfo);
|
||||||
|
|
||||||
|
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Column {
|
||||||
|
Group {
|
||||||
|
title("Cropping"),
|
||||||
|
Column { m_cropWidget },
|
||||||
|
},
|
||||||
|
Space(16),
|
||||||
|
m_trimWidget,
|
||||||
|
buttonBox,
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
m_process = new Process(this);
|
||||||
|
connect(m_process, &Process::done, this, [this] {
|
||||||
|
if (m_process->exitCode() != 0) {
|
||||||
|
FFmpegUtils::reportError(m_process->commandLine(),
|
||||||
|
m_process->readAllRawStandardError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray &imageData = m_process->rawStdOut();
|
||||||
|
startFrameFetch();
|
||||||
|
if (imageData.isEmpty())
|
||||||
|
return;
|
||||||
|
m_previewImage = QImage(reinterpret_cast<const uchar*>(imageData.constData()),
|
||||||
|
m_clipInfo.dimensions.width(), m_clipInfo.dimensions.height(),
|
||||||
|
QImage::Format_RGB32);
|
||||||
|
m_previewImage.detach();
|
||||||
|
m_cropWidget->setImage(m_previewImage);
|
||||||
|
});
|
||||||
|
connect(m_trimWidget, &TrimWidget::positionChanged,
|
||||||
|
this, &CropAndTrimDialog::onSeekPositionChanged);
|
||||||
|
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
|
||||||
|
onSeekPositionChanged();
|
||||||
|
resize(1000, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropAndTrimDialog::onSeekPositionChanged()
|
||||||
|
{
|
||||||
|
// -1, because frame numbers are 0-based
|
||||||
|
m_nextFetchFrame = qMin(m_trimWidget->currentFrame(), m_clipInfo.framesCount() - 1);
|
||||||
|
if (!m_process->isRunning())
|
||||||
|
startFrameFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropAndTrimDialog::startFrameFetch()
|
||||||
|
{
|
||||||
|
if (m_nextFetchFrame == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const CommandLine cl = {
|
||||||
|
Internal::settings().ffmpegTool(),
|
||||||
|
{
|
||||||
|
"-v", "error",
|
||||||
|
"-ss", m_clipInfo.timeStamp(m_nextFetchFrame),
|
||||||
|
"-i", m_clipInfo.file.toUserOutput(),
|
||||||
|
"-threads", "1",
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-f", "rawvideo",
|
||||||
|
"-pix_fmt", "bgra",
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
m_process->close();
|
||||||
|
m_nextFetchFrame = -1;
|
||||||
|
m_process->setCommand(cl);
|
||||||
|
m_process->setWorkingDirectory(Internal::settings().ffmpegTool().parentDir());
|
||||||
|
m_process->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropAndTrimDialog::setCropRect(const QRect &rect)
|
||||||
|
{
|
||||||
|
m_cropWidget->setCropRect(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect CropAndTrimDialog::cropRect() const
|
||||||
|
{
|
||||||
|
return m_cropWidget->cropRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropAndTrimDialog::setTrimRange(FrameRange range)
|
||||||
|
{
|
||||||
|
m_trimWidget->setTrimRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameRange CropAndTrimDialog::trimRange() const
|
||||||
|
{
|
||||||
|
return m_trimWidget->trimRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
int CropAndTrimDialog::currentFrame() const
|
||||||
|
{
|
||||||
|
return m_trimWidget->currentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropAndTrimDialog::setCurrentFrame(int frame)
|
||||||
|
{
|
||||||
|
m_trimWidget->setCurrentFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
CropAndTrimWidget::CropAndTrimWidget(QWidget *parent)
|
||||||
|
: StyledBar(parent)
|
||||||
|
{
|
||||||
|
m_button = new QToolButton;
|
||||||
|
|
||||||
|
m_cropSizeWarningIcon = new CropSizeWarningIcon(CropSizeWarningIcon::ToolBarVariant);
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Row {
|
||||||
|
m_button,
|
||||||
|
m_cropSizeWarningIcon,
|
||||||
|
noMargin(), spacing(0),
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
connect(m_button, &QPushButton::clicked, this, [this] {
|
||||||
|
CropAndTrimDialog dlg(m_clipInfo, Core::ICore::dialogParent());
|
||||||
|
dlg.setCropRect(m_cropRect);
|
||||||
|
dlg.setTrimRange(m_trimRange);
|
||||||
|
dlg.setCurrentFrame(m_currentFrame);
|
||||||
|
if (dlg.exec() == QDialog::Accepted) {
|
||||||
|
m_cropRect = dlg.cropRect();
|
||||||
|
m_trimRange = dlg.trimRange();
|
||||||
|
m_currentFrame = dlg.currentFrame();
|
||||||
|
emit cropRectChanged(m_cropRect);
|
||||||
|
emit trimRangeChanged(m_trimRange);
|
||||||
|
updateWidgets();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropAndTrimWidget::setClip(const ClipInfo &clip)
|
||||||
|
{
|
||||||
|
m_clipInfo = clip;
|
||||||
|
m_cropRect = {QPoint(), clip.dimensions};
|
||||||
|
m_currentFrame = 0;
|
||||||
|
m_trimRange = {m_currentFrame, m_clipInfo.framesCount()};
|
||||||
|
updateWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CropAndTrimWidget::updateWidgets()
|
||||||
|
{
|
||||||
|
const QString cropText =
|
||||||
|
!m_clipInfo.isCompleteArea(m_cropRect)
|
||||||
|
? Tr::tr("Crop to %1x%2px.").arg(m_cropRect.width()).arg(m_cropRect.height())
|
||||||
|
: Tr::tr("Complete area.");
|
||||||
|
|
||||||
|
const QString trimText =
|
||||||
|
!m_clipInfo.isCompleteRange(m_trimRange)
|
||||||
|
? Tr::tr("Frames %1 to %2.").arg(m_trimRange.first).arg(m_trimRange.second)
|
||||||
|
: Tr::tr("Complete clip.");
|
||||||
|
|
||||||
|
m_button->setText(cropText + " " + trimText + "..");
|
||||||
|
m_cropSizeWarningIcon->setCropSize(m_cropRect.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder
|
||||||
|
|
||||||
|
#include "cropandtrim.moc"
|
||||||
94
src/plugins/screenrecorder/cropandtrim.h
Normal file
94
src/plugins/screenrecorder/cropandtrim.h
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ffmpegutils.h"
|
||||||
|
|
||||||
|
#include <utils/styledbar.h>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QToolButton;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
class CropScene : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
CropScene(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
QRect cropRect() const;
|
||||||
|
void setCropRect(const QRect &rect);
|
||||||
|
bool fullySelected() const;
|
||||||
|
void setFullySelected();
|
||||||
|
QRect fullRect() const;
|
||||||
|
void setImage(const QImage &image);
|
||||||
|
|
||||||
|
const static int lineWidth = 1;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void cropRectChanged(const QRect &cropRect);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum MarginEditing {
|
||||||
|
EdgeLeft,
|
||||||
|
EdgeTop,
|
||||||
|
EdgeRight,
|
||||||
|
EdgeBottom,
|
||||||
|
Free
|
||||||
|
};
|
||||||
|
|
||||||
|
void initMouseInteraction(const QPoint &pos);
|
||||||
|
void updateBuffer();
|
||||||
|
QPoint toImagePos(const QPoint &widgetCoordinate);
|
||||||
|
|
||||||
|
const static int m_gripWidth = 8;
|
||||||
|
QRect m_cropRect;
|
||||||
|
const QImage *m_image = nullptr;
|
||||||
|
QImage m_buffer;
|
||||||
|
|
||||||
|
struct MouseInteraction {
|
||||||
|
bool dragging = false;
|
||||||
|
MarginEditing margin;
|
||||||
|
QPoint startImagePos;
|
||||||
|
int clickOffset = 0; // Due to m_gripWidth, the mouse pointer is not precicely on the drag
|
||||||
|
// line. Maintain the offset while dragging, to avoid an initial jump.
|
||||||
|
Qt::CursorShape cursorShape = Qt::ArrowCursor;
|
||||||
|
} m_mouse;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CropAndTrimWidget : public Utils::StyledBar
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
CropAndTrimWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setClip(const ClipInfo &clip);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void cropRectChanged(const QRect &rect);
|
||||||
|
void trimRangeChanged(FrameRange range);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateWidgets();
|
||||||
|
|
||||||
|
QToolButton *m_button;
|
||||||
|
|
||||||
|
ClipInfo m_clipInfo;
|
||||||
|
QRect m_cropRect;
|
||||||
|
int m_currentFrame = 0;
|
||||||
|
FrameRange m_trimRange;
|
||||||
|
CropSizeWarningIcon *m_cropSizeWarningIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder
|
||||||
291
src/plugins/screenrecorder/export.cpp
Normal file
291
src/plugins/screenrecorder/export.cpp
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
// 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 "export.h"
|
||||||
|
|
||||||
|
#include "ffmpegutils.h"
|
||||||
|
#include "screenrecordersettings.h"
|
||||||
|
#include "screenrecordertr.h"
|
||||||
|
|
||||||
|
#include <utils/algorithm.h>
|
||||||
|
#include <utils/fileutils.h>
|
||||||
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/process.h>
|
||||||
|
#include <utils/styledbar.h>
|
||||||
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
|
#include <coreplugin/progressmanager/futureprogress.h>
|
||||||
|
#include <coreplugin/progressmanager/progressmanager.h>
|
||||||
|
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
using namespace Utils;
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
const char screenRecordingExportId[] = "ScreenRecorder::screenRecordingExportTask";
|
||||||
|
|
||||||
|
static const QVector<ExportWidget::Format> &formats()
|
||||||
|
{
|
||||||
|
static const QVector<ExportWidget::Format> result = {
|
||||||
|
{
|
||||||
|
ExportWidget::Format::AnimatedImage,
|
||||||
|
ExportWidget::Format::Lossy,
|
||||||
|
"GIF",
|
||||||
|
".gif",
|
||||||
|
{
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportWidget::Format::AnimatedImage,
|
||||||
|
ExportWidget::Format::Lossless,
|
||||||
|
"WebP",
|
||||||
|
".webp",
|
||||||
|
{
|
||||||
|
"-lossless", "1",
|
||||||
|
"-compression_level", "6",
|
||||||
|
"-qscale", "100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportWidget::Format::AnimatedImage,
|
||||||
|
ExportWidget::Format::Lossy,
|
||||||
|
"WebP",
|
||||||
|
".webp",
|
||||||
|
{
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-compression_level", "6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportWidget::Format::Video,
|
||||||
|
ExportWidget::Format::Lossy,
|
||||||
|
"MP4/H.264",
|
||||||
|
".mp4",
|
||||||
|
{
|
||||||
|
"-pix_fmt", "yuv420p", // 4:2:0 chroma subsampling for Firefox compatibility
|
||||||
|
"-codec", "libx264",
|
||||||
|
"-preset", "veryslow",
|
||||||
|
"-level", "5.2",
|
||||||
|
"-tune", "animation",
|
||||||
|
"-movflags", "+faststart",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportWidget::Format::Video,
|
||||||
|
ExportWidget::Format::Lossy,
|
||||||
|
"WebM/VP9",
|
||||||
|
".webm",
|
||||||
|
{
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-codec", "libvpx-vp9",
|
||||||
|
"-crf", "36", // Creates slightly smaller files than the "MP4/H.264" preset
|
||||||
|
"-deadline", "best",
|
||||||
|
"-row-mt", "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportWidget::Format::AnimatedImage,
|
||||||
|
ExportWidget::Format::Lossless,
|
||||||
|
"avif",
|
||||||
|
".avif",
|
||||||
|
{
|
||||||
|
"-lossless", "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportWidget::Format::Video,
|
||||||
|
ExportWidget::Format::Lossy,
|
||||||
|
"WebM/AV1",
|
||||||
|
".webm",
|
||||||
|
{
|
||||||
|
"-pix_fmt", "yuv422p",
|
||||||
|
"-codec", "libaom-av1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportWidget::Format::Video,
|
||||||
|
ExportWidget::Format::Lossless,
|
||||||
|
"Mov/qtrle",
|
||||||
|
".mov",
|
||||||
|
{
|
||||||
|
"-codec", "qtrle",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString fileDialogFilters()
|
||||||
|
{
|
||||||
|
return transform(formats(), [] (const ExportWidget::Format &fp) {
|
||||||
|
return fp.fileDialogFilter();
|
||||||
|
}).join(";;");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ExportWidget::Format::fileDialogFilter() const
|
||||||
|
{
|
||||||
|
return displayName
|
||||||
|
+ " - " + (kind == Video ? Tr::tr("Video") : Tr::tr("Animated image"))
|
||||||
|
+ " - " + (compression == Lossy ? Tr::tr("Lossy") : Tr::tr("Lossless"))
|
||||||
|
+ " (*" + fileExtension + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportWidget::ExportWidget(QWidget *parent)
|
||||||
|
: StyledBar(parent)
|
||||||
|
, m_trimRange({-1, -1})
|
||||||
|
{
|
||||||
|
m_process = new Process(this);
|
||||||
|
m_process->setUseCtrlCStub(true);
|
||||||
|
m_process->setProcessMode(ProcessMode::Writer);
|
||||||
|
|
||||||
|
auto exportButton = new QToolButton;
|
||||||
|
exportButton->setText(Tr::tr("Export..."));
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Row { st, new StyledSeparator, exportButton, noMargin(), spacing(0) }.attachTo(this);
|
||||||
|
|
||||||
|
connect(exportButton, &QToolButton::clicked, this, [this] {
|
||||||
|
FilePathAspect &lastDir = Internal::settings().exportLastDirectory;
|
||||||
|
QString selectedFilter;
|
||||||
|
FilePath file = FileUtils::getSaveFilePath(nullptr, Tr::tr("Save as"), lastDir(),
|
||||||
|
fileDialogFilters(), &selectedFilter);
|
||||||
|
if (!file.isEmpty()) {
|
||||||
|
m_currentFormat = findOr(formats(), formats().first(),
|
||||||
|
[&selectedFilter] (const Format &fp) {
|
||||||
|
return fp.fileDialogFilter() == selectedFilter;
|
||||||
|
});
|
||||||
|
if (!file.endsWith(m_currentFormat.fileExtension))
|
||||||
|
file = file.stringAppended(m_currentFormat.fileExtension);
|
||||||
|
m_outputClipInfo.file = file;
|
||||||
|
lastDir.setValue(file.parentDir());
|
||||||
|
lastDir.writeToSettingsImmediatly();
|
||||||
|
startExport();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_process, &Process::started, this, [exportButton] {
|
||||||
|
exportButton->setEnabled(false);
|
||||||
|
});
|
||||||
|
connect(m_process, &Process::done, this, [this, exportButton] {
|
||||||
|
exportButton->setEnabled(true);
|
||||||
|
m_futureInterface->reportFinished();
|
||||||
|
if (m_process->exitCode() == 0)
|
||||||
|
emit clipReady(m_outputClipInfo);
|
||||||
|
else
|
||||||
|
FFmpegUtils::reportError(m_process->commandLine(), m_lastOutputChunk);
|
||||||
|
});
|
||||||
|
connect(m_process, &Process::readyReadStandardError, this, [this] {
|
||||||
|
m_lastOutputChunk = m_process->readAllRawStandardError();
|
||||||
|
const int frameProgress = FFmpegUtils::parseFrameProgressFromOutput(m_lastOutputChunk);
|
||||||
|
if (frameProgress >= 0)
|
||||||
|
m_futureInterface->setProgressValue(frameProgress);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportWidget::~ExportWidget()
|
||||||
|
{
|
||||||
|
interruptExport();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExportWidget::startExport()
|
||||||
|
{
|
||||||
|
m_futureInterface.reset(new QFutureInterface<void>);
|
||||||
|
m_futureInterface->setProgressRange(0, m_trimRange.second - m_trimRange.first);
|
||||||
|
Core::ProgressManager::addTask(m_futureInterface->future(),
|
||||||
|
Tr::tr("Exporting Screen Recording"), screenRecordingExportId);
|
||||||
|
m_futureInterface->setProgressValue(0);
|
||||||
|
m_futureInterface->reportStarted();
|
||||||
|
const auto watcher = new QFutureWatcher<void>(this);
|
||||||
|
connect(watcher, &QFutureWatcher<void>::canceled, this, &ExportWidget::interruptExport);
|
||||||
|
connect(watcher, &QFutureWatcher<void>::finished, this, [watcher] {
|
||||||
|
watcher->disconnect();
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
watcher->setFuture(m_futureInterface->future());
|
||||||
|
|
||||||
|
m_process->close();
|
||||||
|
const CommandLine cl(Internal::settings().ffmpegTool(), ffmpegExportParameters());
|
||||||
|
m_process->setCommand(cl);
|
||||||
|
m_process->setWorkingDirectory(Internal::settings().ffmpegTool().parentDir());
|
||||||
|
FFmpegUtils::logFfmpegCall(cl);
|
||||||
|
m_process->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExportWidget::interruptExport()
|
||||||
|
{
|
||||||
|
FFmpegUtils::killFfmpegProcess(m_process);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ExportWidget::ffmpegExportParameters() const
|
||||||
|
{
|
||||||
|
const bool isGif = m_currentFormat.fileExtension == ".gif";
|
||||||
|
const QString trimFilter =
|
||||||
|
!m_inputClipInfo.isCompleteRange(m_trimRange)
|
||||||
|
? QString("[v:0]trim=start=%1:end=%2[trimmed]"
|
||||||
|
";[trimmed]setpts=PTS-STARTPTS[setpts]")
|
||||||
|
.arg(m_inputClipInfo.secondForFrame(m_trimRange.first))
|
||||||
|
.arg(m_inputClipInfo.secondForFrame(m_trimRange.second))
|
||||||
|
: QString("[v:0]null[setpts]");
|
||||||
|
|
||||||
|
const QString cropFilter =
|
||||||
|
!m_inputClipInfo.isCompleteArea(m_cropRect)
|
||||||
|
? QString(";[setpts]crop=w=%3:h=%4:x=%5:y=%6[cropped]")
|
||||||
|
.arg(m_cropRect.width()).arg(m_cropRect.height())
|
||||||
|
.arg(m_cropRect.left()).arg(m_cropRect.top())
|
||||||
|
: QString(";[setpts]null[cropped]");
|
||||||
|
|
||||||
|
const QString extraFilter =
|
||||||
|
isGif
|
||||||
|
? QString(";[cropped]split[cropped1][cropped2]"
|
||||||
|
";[cropped1]palettegen="
|
||||||
|
"reserve_transparent=false"
|
||||||
|
":max_colors=256[pal]"
|
||||||
|
";[cropped2][pal]paletteuse="
|
||||||
|
"diff_mode=rectangle[out]")
|
||||||
|
: QString(";[cropped]null[out]");
|
||||||
|
|
||||||
|
QStringList loop;
|
||||||
|
if (m_currentFormat.kind == Format::AnimatedImage) {
|
||||||
|
const bool doLoop = Internal::settings().animatedImagesAsEndlessLoop();
|
||||||
|
// GIF muxer take different values for "don't loop" than WebP and avif muxer
|
||||||
|
const QLatin1String dontLoopParam(isGif ? "-1" : "1");
|
||||||
|
loop.append({"-loop", doLoop ? QLatin1String("0") : dontLoopParam});
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList args =
|
||||||
|
QStringList {
|
||||||
|
"-y",
|
||||||
|
"-v", "error",
|
||||||
|
"-stats",
|
||||||
|
"-stats_period", "0.25",
|
||||||
|
"-i", m_inputClipInfo.file.toString(),
|
||||||
|
}
|
||||||
|
<< "-filter_complex" << trimFilter + cropFilter + extraFilter << "-map" << "[out]"
|
||||||
|
<< m_currentFormat.encodingParameters
|
||||||
|
<< loop
|
||||||
|
<< m_outputClipInfo.file.toString();
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExportWidget::setClip(const ClipInfo &clip)
|
||||||
|
{
|
||||||
|
if (!qFuzzyCompare(clip.duration, m_inputClipInfo.duration))
|
||||||
|
m_trimRange = {0, clip.framesCount()};
|
||||||
|
if (clip.dimensions != m_inputClipInfo.dimensions)
|
||||||
|
m_cropRect = {QPoint(), clip.dimensions};
|
||||||
|
m_inputClipInfo = clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExportWidget::setCropRect(const QRect &rect)
|
||||||
|
{
|
||||||
|
m_cropRect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExportWidget::setTrimRange(FrameRange range)
|
||||||
|
{
|
||||||
|
m_trimRange = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder
|
||||||
69
src/plugins/screenrecorder/export.h
Normal file
69
src/plugins/screenrecorder/export.h
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ffmpegutils.h"
|
||||||
|
|
||||||
|
#include <utils/styledbar.h>
|
||||||
|
|
||||||
|
#include <QFutureInterface>
|
||||||
|
|
||||||
|
namespace Utils {
|
||||||
|
class Process;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
class CropSizeWarningIcon;
|
||||||
|
|
||||||
|
class ExportWidget : public Utils::StyledBar
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct Format {
|
||||||
|
enum Kind {
|
||||||
|
AnimatedImage,
|
||||||
|
Video,
|
||||||
|
} kind;
|
||||||
|
|
||||||
|
enum Compression {
|
||||||
|
Lossy,
|
||||||
|
Lossless,
|
||||||
|
} compression;
|
||||||
|
|
||||||
|
QString displayName;
|
||||||
|
QString fileExtension;
|
||||||
|
QStringList encodingParameters;
|
||||||
|
|
||||||
|
QString fileDialogFilter() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit ExportWidget(QWidget *parent = nullptr);
|
||||||
|
~ExportWidget();
|
||||||
|
|
||||||
|
void setClip(const ClipInfo &clip);
|
||||||
|
void setCropRect(const QRect &rect);
|
||||||
|
void setTrimRange(FrameRange range);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void clipReady(const ClipInfo &clip);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void startExport();
|
||||||
|
void interruptExport();
|
||||||
|
QStringList ffmpegExportParameters() const;
|
||||||
|
|
||||||
|
ClipInfo m_inputClipInfo;
|
||||||
|
ClipInfo m_outputClipInfo;
|
||||||
|
Format m_currentFormat;
|
||||||
|
Utils::Process *m_process;
|
||||||
|
QByteArray m_lastOutputChunk;
|
||||||
|
std::unique_ptr<QFutureInterface<void>> m_futureInterface;
|
||||||
|
|
||||||
|
QRect m_cropRect;
|
||||||
|
FrameRange m_trimRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder
|
||||||
471
src/plugins/screenrecorder/ffmpegutils.cpp
Normal file
471
src/plugins/screenrecorder/ffmpegutils.cpp
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
// 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 <QTest>
|
||||||
|
#endif // WITH_TESTS
|
||||||
|
|
||||||
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/process.h>
|
||||||
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
|
#include <coreplugin/messagemanager.h>
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVersionNumber>
|
||||||
|
|
||||||
|
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("<b>%1</b> (%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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVersionNumber toolVersion()
|
||||||
|
{
|
||||||
|
Process proc;
|
||||||
|
const CommandLine cl = {
|
||||||
|
Internal::settings().ffprobeTool(),
|
||||||
|
{
|
||||||
|
"-v", "quiet",
|
||||||
|
"-print_format", "json",
|
||||||
|
"-show_versions",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
proc.setCommand(cl);
|
||||||
|
proc.runBlocking();
|
||||||
|
const QByteArray output = proc.allRawOutput();
|
||||||
|
return parseVersionNumber(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
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*(?<frame>\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 {
|
||||||
|
|
||||||
|
void FFmpegOutputParserTest::testVersionParser_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QByteArray>("ffprobeVersionOutput");
|
||||||
|
QTest::addColumn<QVersionNumber>("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<QByteArray>("ffmpegVersionOutput");
|
||||||
|
QTest::addColumn<ClipInfo>("clipInfo");
|
||||||
|
|
||||||
|
// ffprobe -v quiet -print_format json -show_format -show_streams -select_streams V <video file>
|
||||||
|
QTest::newRow("10.623s, 28.33 fps, 640x480, h264, yuv444p")
|
||||||
|
<< QByteArray(
|
||||||
|
R"({
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"codec_name": "h264",
|
||||||
|
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
|
||||||
|
"profile": "High 4:4:4 Predictive",
|
||||||
|
"codec_type": "video",
|
||||||
|
"codec_tag_string": "[0][0][0][0]",
|
||||||
|
"codec_tag": "0x0000",
|
||||||
|
"width": 640,
|
||||||
|
"height": 480,
|
||||||
|
"coded_width": 640,
|
||||||
|
"coded_height": 480,
|
||||||
|
"closed_captions": 0,
|
||||||
|
"film_grain": 0,
|
||||||
|
"has_b_frames": 2,
|
||||||
|
"pix_fmt": "yuv444p",
|
||||||
|
"level": 30,
|
||||||
|
"chroma_location": "left",
|
||||||
|
"field_order": "progressive",
|
||||||
|
"refs": 1,
|
||||||
|
"is_avc": "true",
|
||||||
|
"nal_length_size": "4",
|
||||||
|
"r_frame_rate": "85/3",
|
||||||
|
"avg_frame_rate": "85/3",
|
||||||
|
"time_base": "1/1000",
|
||||||
|
"start_pts": 0,
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"bits_per_raw_sample": "8",
|
||||||
|
"extradata_size": 41,
|
||||||
|
"disposition": {
|
||||||
|
"default": 1,
|
||||||
|
"dub": 0,
|
||||||
|
"original": 0,
|
||||||
|
"comment": 0,
|
||||||
|
"lyrics": 0,
|
||||||
|
"karaoke": 0,
|
||||||
|
"forced": 0,
|
||||||
|
"hearing_impaired": 0,
|
||||||
|
"visual_impaired": 0,
|
||||||
|
"clean_effects": 0,
|
||||||
|
"attached_pic": 0,
|
||||||
|
"timed_thumbnails": 0,
|
||||||
|
"captions": 0,
|
||||||
|
"descriptions": 0,
|
||||||
|
"metadata": 0,
|
||||||
|
"dependent": 0,
|
||||||
|
"still_image": 0
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"ENCODER": "Lavc58.54.100 libx264",
|
||||||
|
"DURATION": "00:00:10.623000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"filename": "out.mkv",
|
||||||
|
"nb_streams": 1,
|
||||||
|
"nb_programs": 0,
|
||||||
|
"format_name": "matroska,webm",
|
||||||
|
"format_long_name": "Matroska / WebM",
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration": "10.623000",
|
||||||
|
"size": "392136",
|
||||||
|
"bit_rate": "295310",
|
||||||
|
"probe_score": 100,
|
||||||
|
"tags": {
|
||||||
|
"ENCODER": "Lavf58.29.100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})")
|
||||||
|
<< ClipInfo{ {}, {640, 480}, "h264", 10.623, 28.33333333333, "yuv444p", 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFmpegOutputParserTest::testClipInfoParser()
|
||||||
|
{
|
||||||
|
QFETCH(QByteArray, ffmpegVersionOutput);
|
||||||
|
QFETCH(ClipInfo, clipInfo);
|
||||||
|
|
||||||
|
const ClipInfo ci = parseClipInfo(ffmpegVersionOutput);
|
||||||
|
|
||||||
|
QCOMPARE(ci.duration, clipInfo.duration);
|
||||||
|
QCOMPARE(ci.rFrameRate, clipInfo.rFrameRate);
|
||||||
|
QCOMPARE(ci.dimensions, clipInfo.dimensions);
|
||||||
|
QCOMPARE(ci.codec, clipInfo.codec);
|
||||||
|
QCOMPARE(ci.pixFmt, clipInfo.pixFmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFmpegOutputParserTest::testFfmpegOutputParser_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QByteArray>("ffmpegRecordingLogLine");
|
||||||
|
QTest::addColumn<int>("frameProgress");
|
||||||
|
|
||||||
|
typedef QByteArray _;
|
||||||
|
|
||||||
|
// ffmpeg -y -video_size vga -f x11grab -i :0.0+100,200 -vcodec qtrle /tmp/QtCreator-VMQjhs/AeOjep.mov
|
||||||
|
QTest::newRow("skip 01")
|
||||||
|
<< _("ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021 the FFmpeg developers\n built with gcc 11 (Ubuntu 11.2.0-19ubuntu1)\n 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\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 02")
|
||||||
|
<< _(" libavutil 56. 70.100 / 56. 70.100\n libavcodec 58.134.100 / 58.134.100\n libavformat 58. 76.100 / 58. 76.100\n libavdevice 58. 13.100 / 58. 13.100\n libavfilter 7.110.100 / 7.110.100\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 03")
|
||||||
|
<< _(" libswscale 5. 9.100 / 5. 9.100\n libswresample 3. 9.100 / 3. 9.100\n libpostproc 55. 9.100 / 55. 9.100\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 04")
|
||||||
|
<< _("Input #0, x11grab, from ':0.0+100,200':\n Duration: N/A, start: 1691512344.764131, bitrate: 294617 kb/s\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 05")
|
||||||
|
<< _(" Stream #0:0: Video: rawvideo (BGR[0] / 0x524742), bgr0, 640x480, 294617 kb/s, ")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 06")
|
||||||
|
<< _("29.97 fps, 30 tbr, 1000k tbn, 1000k tbc\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 07")
|
||||||
|
<< _("Stream mapping:\n Stream #0:0 -> #0:0 (rawvideo (native) -> qtrle (native))\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 08")
|
||||||
|
<< _("Press [q] to stop, [?] for help\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 09")
|
||||||
|
<< _("Output #0, mov, to '/tmp/QtCreator-VMQjhs/AeOjep.mov':\n Metadata:\n encoder : Lavf58.76.100\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 10")
|
||||||
|
<< _(" Stream #0:0: Video: qtrle (rle / 0x20656C72), rgb24(pc, gbr/unknown/unknown, progressive), 640x480, q=2-31, 200 kb/s")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 11")
|
||||||
|
<< _(", 30 fps, ")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 12")
|
||||||
|
<< _("15360 tbn\n Metadata:\n encoder : Lavc58.134.100 qtrle")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("skip 13")
|
||||||
|
<< _("\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("frame 1")
|
||||||
|
<< _("frame= 1 fps=0.0 q=-0.0 size= 0kB time=00:00:00.00 bitrate=4430.8kbits/s speed=N/A \r")
|
||||||
|
<< 1;
|
||||||
|
QTest::newRow("frame 21")
|
||||||
|
<< _("frame= 21 fps=0.0 q=-0.0 size= 256kB time=00:00:00.66 bitrate=3145.9kbits/s speed=1.33x \r")
|
||||||
|
<< 21;
|
||||||
|
QTest::newRow("frame 36")
|
||||||
|
<< _("frame= 36 fps= 36 q=-0.0 size= 256kB time=00:00:01.16 bitrate=1797.7kbits/s speed=1.17x \r")
|
||||||
|
<< 36;
|
||||||
|
QTest::newRow("frame 51")
|
||||||
|
<< _("frame= 51 fps= 34 q=-0.0 size= 512kB time=00:00:01.66 bitrate=2516.7kbits/s speed=1.11x \r")
|
||||||
|
<< 51;
|
||||||
|
QTest::newRow("skip 14")
|
||||||
|
<< _("\r\n\r\n[q] command received. Exiting.\r\n\r\n")
|
||||||
|
<< -1;
|
||||||
|
QTest::newRow("frame 65 - final log line")
|
||||||
|
<< _("frame= 65 fps= 32 q=-0.0 Lsize= 801kB time=00:00:02.13 bitrate=3074.4kbits/s speed=1.07x \nvideo:800kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.125299%\n")
|
||||||
|
<< 65;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFmpegOutputParserTest::testFfmpegOutputParser()
|
||||||
|
{
|
||||||
|
QFETCH(QByteArray, ffmpegRecordingLogLine);
|
||||||
|
QFETCH(int, frameProgress);
|
||||||
|
|
||||||
|
const int parsedFrameProgress = parseFrameProgressFromOutput(ffmpegRecordingLogLine);
|
||||||
|
|
||||||
|
QCOMPARE(parsedFrameProgress, frameProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namescace ScreenRecorder::Internal
|
||||||
|
#endif // WITH_TESTS
|
||||||
94
src/plugins/screenrecorder/ffmpegutils.h
Normal file
94
src/plugins/screenrecorder/ffmpegutils.h
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QVersionNumber>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QTimer;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
namespace Utils {
|
||||||
|
class CommandLine;
|
||||||
|
class FilePath;
|
||||||
|
class Process;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
using FrameRange = std::pair<int, int>;
|
||||||
|
|
||||||
|
struct ClipInfo {
|
||||||
|
Utils::FilePath file;
|
||||||
|
|
||||||
|
// ffmpeg terminology
|
||||||
|
QSize dimensions;
|
||||||
|
QString codec;
|
||||||
|
qreal duration = -1; // seconds
|
||||||
|
qreal rFrameRate = -1; // frames per second
|
||||||
|
QString pixFmt;
|
||||||
|
int streamIdex = -1;
|
||||||
|
|
||||||
|
int framesCount() const;
|
||||||
|
qreal secondForFrame(int frame) const;
|
||||||
|
QString timeStamp(int frame) const;
|
||||||
|
bool isNull() const;
|
||||||
|
bool isCompleteArea(const QRect &rect) const;
|
||||||
|
bool isCompleteRange(FrameRange range) const;
|
||||||
|
bool isLossless() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TimeLabel : public QLabel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit TimeLabel(const ClipInfo &clipInfo, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setFrame(int frame);
|
||||||
|
int frame() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const ClipInfo &m_clipInfo;
|
||||||
|
int m_frame = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CropSizeWarningIcon : public QWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum IconVariant {
|
||||||
|
StandardVariant,
|
||||||
|
ToolBarVariant,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit CropSizeWarningIcon(IconVariant backgroundType, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setCropSize(const QSize &size);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent*) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateVisibility();
|
||||||
|
bool needsWarning() const;
|
||||||
|
|
||||||
|
QSize m_cropSize;
|
||||||
|
const IconVariant m_iconVariant;
|
||||||
|
QTimer *m_updateTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace FFmpegUtils {
|
||||||
|
|
||||||
|
QVersionNumber toolVersion();
|
||||||
|
ClipInfo clipInfo(const Utils::FilePath &path);
|
||||||
|
int parseFrameProgressFromOutput(const QByteArray &output);
|
||||||
|
void sendQuitCommand(Utils::Process *proc);
|
||||||
|
void killFfmpegProcess(Utils::Process *proc);
|
||||||
|
void logFfmpegCall(const Utils::CommandLine &cmdLn);
|
||||||
|
void reportError(const Utils::CommandLine &cmdLn, const QByteArray &error);
|
||||||
|
|
||||||
|
} // namespace FFmpegUtils
|
||||||
|
} // namespace ScreenRecorder
|
||||||
361
src/plugins/screenrecorder/record.cpp
Normal file
361
src/plugins/screenrecorder/record.cpp
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
// 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 "record.h"
|
||||||
|
|
||||||
|
#include "cropandtrim.h"
|
||||||
|
#include "ffmpegutils.h"
|
||||||
|
#include "screenrecordersettings.h"
|
||||||
|
#include "screenrecordertr.h"
|
||||||
|
|
||||||
|
#include <utils/fileutils.h>
|
||||||
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/process.h>
|
||||||
|
#include <utils/qtcsettings.h>
|
||||||
|
#include <utils/styledbar.h>
|
||||||
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
using namespace Utils;
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
using namespace Internal;
|
||||||
|
|
||||||
|
struct RecordPreset {
|
||||||
|
const QString fileExtension;
|
||||||
|
const QStringList encodingParameters;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const RecordPreset &recordPreset()
|
||||||
|
{
|
||||||
|
static const RecordPreset preset = {
|
||||||
|
".mkv",
|
||||||
|
{
|
||||||
|
"-vcodec", "libx264rgb",
|
||||||
|
"-crf", "0",
|
||||||
|
"-preset", "ultrafast",
|
||||||
|
"-tune", "zerolatency",
|
||||||
|
"-reserve_index_space", "1M",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Valid alternatives:
|
||||||
|
// ".mov", { "-vcodec", "qtrle" } // Slower encoding, faster seeking
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecordOptionsDialog : public QDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit RecordOptionsDialog(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QRect screenCropRect() const;
|
||||||
|
void updateCropScene();
|
||||||
|
void updateWidgets();
|
||||||
|
|
||||||
|
static const int m_factor = 4;
|
||||||
|
CropScene *m_cropScene;
|
||||||
|
QImage m_thumbnail;
|
||||||
|
IntegerAspect m_screenId;
|
||||||
|
IntegerAspect m_recordFrameRate;
|
||||||
|
QLabel *m_cropRectLabel;
|
||||||
|
QToolButton *m_resetButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
RecordOptionsDialog::RecordOptionsDialog(QWidget *parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle(Tr::tr("Screen Recording Options"));
|
||||||
|
|
||||||
|
m_screenId.setRange(0, QGuiApplication::screens().count() - 1);
|
||||||
|
|
||||||
|
m_cropScene = new CropScene;
|
||||||
|
|
||||||
|
m_resetButton = new QToolButton;
|
||||||
|
m_resetButton->setIcon(Icons::RESET.icon());
|
||||||
|
|
||||||
|
m_cropRectLabel = new QLabel;
|
||||||
|
|
||||||
|
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Column {
|
||||||
|
Row { m_screenId, st },
|
||||||
|
Group {
|
||||||
|
title(Tr::tr("Recorded screen area")),
|
||||||
|
Column {
|
||||||
|
m_cropScene,
|
||||||
|
Row { st, m_cropRectLabel, m_resetButton },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Row { m_recordFrameRate, st },
|
||||||
|
st,
|
||||||
|
buttonBox,
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
connect(buttonBox, &QDialogButtonBox::accepted, this, [this] {
|
||||||
|
const QRect cropRect = m_cropScene->fullySelected() ? QRect() : screenCropRect();
|
||||||
|
settings().applyRecordSettings({int(m_screenId()), cropRect, int(m_recordFrameRate())});
|
||||||
|
QDialog::accept();
|
||||||
|
});
|
||||||
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
connect(&m_screenId, &IntegerAspect::changed, this, [this] {
|
||||||
|
updateCropScene();
|
||||||
|
m_cropScene->setFullySelected();
|
||||||
|
});
|
||||||
|
connect(m_resetButton, &QToolButton::pressed, this, [this](){
|
||||||
|
m_cropScene->setFullySelected();
|
||||||
|
});
|
||||||
|
connect(m_cropScene, &CropScene::cropRectChanged, this, &RecordOptionsDialog::updateWidgets);
|
||||||
|
|
||||||
|
updateCropScene();
|
||||||
|
|
||||||
|
const ScreenRecorderSettings::RecordSettings rs = settings().recordSettings();
|
||||||
|
m_screenId.setValue(rs.screenId);
|
||||||
|
if (!rs.cropRect.isNull()) {
|
||||||
|
m_cropScene->setCropRect({rs.cropRect.x() / m_factor,
|
||||||
|
rs.cropRect.y() / m_factor,
|
||||||
|
rs.cropRect.width() / m_factor,
|
||||||
|
rs.cropRect.height() / m_factor});
|
||||||
|
} else {
|
||||||
|
m_cropScene->setFullySelected();
|
||||||
|
}
|
||||||
|
m_recordFrameRate.setValue(rs.frameRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect RecordOptionsDialog::screenCropRect() const
|
||||||
|
{
|
||||||
|
const QRect r = m_cropScene->cropRect();
|
||||||
|
return {r.x() * m_factor, r.y() * m_factor, r.width() * m_factor, r.height() * m_factor};
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecordOptionsDialog::updateCropScene()
|
||||||
|
{
|
||||||
|
const ScreenRecorderSettings::RecordSettings rs = ScreenRecorderSettings
|
||||||
|
::sanitizedRecordSettings({int(m_screenId()), screenCropRect(), int(m_recordFrameRate())});
|
||||||
|
const QList<QScreen*> screens = QGuiApplication::screens();
|
||||||
|
m_thumbnail = QGuiApplication::screens().at(rs.screenId)->grabWindow().toImage();
|
||||||
|
const qreal dpr = m_thumbnail.devicePixelRatio();
|
||||||
|
m_thumbnail = m_thumbnail.scaled((m_thumbnail.deviceIndependentSize() / m_factor * dpr)
|
||||||
|
.toSize(),
|
||||||
|
Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
|
m_thumbnail.setDevicePixelRatio(dpr);
|
||||||
|
m_cropScene->setImage(m_thumbnail);
|
||||||
|
const static int lw = CropScene::lineWidth;
|
||||||
|
m_cropScene->setFixedSize(m_thumbnail.deviceIndependentSize().toSize()
|
||||||
|
.grownBy({lw, lw, lw, lw}));
|
||||||
|
QTimer::singleShot(250, this, [this] {
|
||||||
|
updateCropScene();
|
||||||
|
});
|
||||||
|
updateWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecordOptionsDialog::updateWidgets()
|
||||||
|
{
|
||||||
|
const QRect r = screenCropRect();
|
||||||
|
m_cropRectLabel->setText(QString("x:%1 y:%2 w:%3 h:%4")
|
||||||
|
.arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height()));
|
||||||
|
m_resetButton->setEnabled(!m_cropScene->fullySelected());
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordWidget::RecordWidget(const FilePath &recordFile, QWidget *parent)
|
||||||
|
: StyledBar(parent)
|
||||||
|
, m_recordFile(recordFile)
|
||||||
|
{
|
||||||
|
m_process = new Process(this);
|
||||||
|
m_process->setUseCtrlCStub(true);
|
||||||
|
m_process->setProcessMode(ProcessMode::Writer);
|
||||||
|
|
||||||
|
auto settingsButton = new QToolButton;
|
||||||
|
settingsButton->setIcon(Icons::SETTINGS_TOOLBAR.icon());
|
||||||
|
|
||||||
|
auto recordButton = new QToolButton;
|
||||||
|
recordButton->setIcon(Icon({{":/utils/images/filledcircle.png",
|
||||||
|
Theme::IconsStopToolBarColor}}).icon());
|
||||||
|
|
||||||
|
auto stopButton = new QToolButton;
|
||||||
|
stopButton->setIcon(Icon({{":/utils/images/stop_small.png",
|
||||||
|
Theme::IconsBaseColor}}).icon());
|
||||||
|
stopButton->setEnabled(false);
|
||||||
|
|
||||||
|
auto progressLabel = new TimeLabel(m_clipInfo);
|
||||||
|
progressLabel->setEnabled(false);
|
||||||
|
progressLabel->setAlignment(Qt::AlignVCenter | Qt::AlignRight);
|
||||||
|
|
||||||
|
m_openClipAction = new QAction(Tr::tr("Open Mov/qtrle rgb24 file"), this);
|
||||||
|
addAction(m_openClipAction);
|
||||||
|
setContextMenuPolicy(Qt::ActionsContextMenu);
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Row {
|
||||||
|
settingsButton,
|
||||||
|
recordButton,
|
||||||
|
stopButton,
|
||||||
|
st,
|
||||||
|
progressLabel,
|
||||||
|
Space(6),
|
||||||
|
noMargin(), spacing(0),
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
connect(settingsButton, &QToolButton::clicked, this, [this] {
|
||||||
|
m_optionsDialog = new RecordOptionsDialog(this);
|
||||||
|
m_optionsDialog->setWindowModality(Qt::WindowModal);
|
||||||
|
m_optionsDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
m_optionsDialog->show();
|
||||||
|
});
|
||||||
|
connect(recordButton, &QToolButton::clicked, this, [this, progressLabel] {
|
||||||
|
m_clipInfo.duration = 0;
|
||||||
|
progressLabel->setFrame(0);
|
||||||
|
m_clipInfo = {};
|
||||||
|
m_clipInfo.file = m_recordFile;
|
||||||
|
m_clipInfo.rFrameRate = qreal(Internal::settings().recordFrameRate());
|
||||||
|
const CommandLine cl(Internal::settings().ffmpegTool(), ffmpegParameters(m_clipInfo));
|
||||||
|
m_process->setCommand(cl);
|
||||||
|
m_process->setWorkingDirectory(Internal::settings().ffmpegTool().parentDir());
|
||||||
|
FFmpegUtils::logFfmpegCall(cl);
|
||||||
|
m_process->start();
|
||||||
|
});
|
||||||
|
connect(stopButton, &QToolButton::clicked, this, [this] {
|
||||||
|
FFmpegUtils::sendQuitCommand(m_process);
|
||||||
|
});
|
||||||
|
connect(m_process, &Process::started, this, [=] {
|
||||||
|
progressLabel->setEnabled(true);
|
||||||
|
recordButton->setEnabled(false);
|
||||||
|
stopButton->setEnabled(true);
|
||||||
|
settingsButton->setEnabled(false);
|
||||||
|
m_openClipAction->setEnabled(false);
|
||||||
|
emit started();
|
||||||
|
});
|
||||||
|
connect(m_process, &Process::done, this, [=] {
|
||||||
|
recordButton->setEnabled(true);
|
||||||
|
stopButton->setEnabled(false);
|
||||||
|
settingsButton->setEnabled(true);
|
||||||
|
m_openClipAction->setEnabled(true);
|
||||||
|
if (m_process->exitCode() == 0)
|
||||||
|
emit finished(FFmpegUtils::clipInfo(m_clipInfo.file));
|
||||||
|
else
|
||||||
|
FFmpegUtils::reportError(m_process->commandLine(), m_lastOutputChunk);
|
||||||
|
});
|
||||||
|
connect(m_process, &Process::readyReadStandardError, this, [this, progressLabel] {
|
||||||
|
m_lastOutputChunk = m_process->readAllRawStandardError();
|
||||||
|
const int frameProgress = FFmpegUtils::parseFrameProgressFromOutput(m_lastOutputChunk);
|
||||||
|
if (frameProgress > 0) {
|
||||||
|
m_clipInfo.duration = m_clipInfo.secondForFrame(frameProgress);
|
||||||
|
progressLabel->setFrame(m_clipInfo.framesCount());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(m_openClipAction, &QAction::triggered, this, [this, progressLabel] {
|
||||||
|
const FilePath lastDir = Internal::settings().lastOpenDirectory();
|
||||||
|
const FilePath file = FileUtils::getOpenFilePath(Core::ICore::dialogParent(),
|
||||||
|
m_openClipAction->text(), lastDir,
|
||||||
|
"Mov/qtrle rgb24 (*.mov)");
|
||||||
|
if (!file.isEmpty()) {
|
||||||
|
Internal::settings().lastOpenDirectory.setValue(file.parentDir());
|
||||||
|
const ClipInfo clip = FFmpegUtils::clipInfo(file);
|
||||||
|
if (clip.isNull()) {
|
||||||
|
QMessageBox::critical(Core::ICore::dialogParent(),
|
||||||
|
Tr::tr("Cannot Open Clip"),
|
||||||
|
Tr::tr("FFmpeg cannot open %1.").arg(file.toUserOutput()));
|
||||||
|
} else if (!clip.isLossless()) {
|
||||||
|
QMessageBox::critical(Core::ICore::dialogParent(),
|
||||||
|
Tr::tr("Clip Not Supported"),
|
||||||
|
Tr::tr("Please chose a clip with the \"qtrle\" codec and "
|
||||||
|
"pixel format \"rgb24\"."));
|
||||||
|
} else {
|
||||||
|
m_clipInfo.duration = 0;
|
||||||
|
progressLabel->setFrame(0);
|
||||||
|
progressLabel->setEnabled(false);
|
||||||
|
emit finished(clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordWidget::~RecordWidget()
|
||||||
|
{
|
||||||
|
FFmpegUtils::killFfmpegProcess(m_process);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RecordWidget::recordFileExtension()
|
||||||
|
{
|
||||||
|
return recordPreset().fileExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList RecordWidget::ffmpegParameters(const ClipInfo &clipInfo) const
|
||||||
|
{
|
||||||
|
const Internal::ScreenRecorderSettings::RecordSettings rS =
|
||||||
|
Internal::settings().recordSettings();
|
||||||
|
const QString frameRateStr = QString::number(rS.frameRate);
|
||||||
|
const QString screenIdStr = QString::number(rS.screenId);
|
||||||
|
const QString videoSizeStr = QString("%1x%2").arg(rS.cropRect.width())
|
||||||
|
.arg(rS.cropRect.height());
|
||||||
|
QStringList videoGrabParams;
|
||||||
|
// see http://trac.ffmpeg.org/wiki/Capture/Desktop
|
||||||
|
switch (HostOsInfo::hostOs()) {
|
||||||
|
case OsTypeLinux:
|
||||||
|
videoGrabParams.append({"-f", "x11grab"});
|
||||||
|
videoGrabParams.append({"-framerate", frameRateStr});
|
||||||
|
if (!rS.cropRect.isNull()) {
|
||||||
|
videoGrabParams.append({"-video_size", videoSizeStr});
|
||||||
|
videoGrabParams.append({"-i", QString(":%1.0+%2,%3").arg(screenIdStr)
|
||||||
|
.arg(rS.cropRect.x()).arg(rS.cropRect.y())});
|
||||||
|
} else {
|
||||||
|
videoGrabParams.append({"-i", QString(":%1.0").arg(screenIdStr)});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OsTypeWindows: {
|
||||||
|
QString filter = "ddagrab=output_idx=" + screenIdStr;
|
||||||
|
if (!rS.cropRect.isNull()) {
|
||||||
|
filter.append(":video_size=" + videoSizeStr);
|
||||||
|
filter.append(QString(":offset_x=%1:offset_y=%2").arg(rS.cropRect.x())
|
||||||
|
.arg(rS.cropRect.y()));
|
||||||
|
}
|
||||||
|
filter.append(":framerate=" + frameRateStr);
|
||||||
|
filter.append(",hwdownload");
|
||||||
|
filter.append(",format=bgra");
|
||||||
|
videoGrabParams = {
|
||||||
|
"-ss", "00:00.25", // Skip few first frames which are black
|
||||||
|
"-filter_complex", filter,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OsTypeMac:
|
||||||
|
videoGrabParams = {
|
||||||
|
"-f", "avfoundation",
|
||||||
|
"-framerate", frameRateStr,
|
||||||
|
"-video_size", videoSizeStr,
|
||||||
|
"-i", QString("%1:none").arg(screenIdStr),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList args = {
|
||||||
|
"-y",
|
||||||
|
"-v", "error",
|
||||||
|
"-stats",
|
||||||
|
};
|
||||||
|
args.append(videoGrabParams);
|
||||||
|
if (Internal::settings().enableFileSizeLimit())
|
||||||
|
args.append({"-fs", QString::number(Internal::settings().fileSizeLimit()) + "M"});
|
||||||
|
if (Internal::settings().enableRtBuffer())
|
||||||
|
args.append({"-rtbufsize", QString::number(Internal::settings().rtBufferSize()) + "M"});
|
||||||
|
args.append(recordPreset().encodingParameters);
|
||||||
|
args.append(clipInfo.file.toString());
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder
|
||||||
43
src/plugins/screenrecorder/record.h
Normal file
43
src/plugins/screenrecorder/record.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ffmpegutils.h"
|
||||||
|
|
||||||
|
#include <utils/styledbar.h>
|
||||||
|
|
||||||
|
namespace Utils {
|
||||||
|
class Process;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
class RecordOptionsDialog;
|
||||||
|
|
||||||
|
class RecordWidget : public Utils::StyledBar
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit RecordWidget(const Utils::FilePath &recordFile, QWidget *parent = nullptr);
|
||||||
|
~RecordWidget();
|
||||||
|
|
||||||
|
static QString recordFileExtension();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void started();
|
||||||
|
void finished(const ClipInfo &clip);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QStringList ffmpegParameters(const ClipInfo &clipInfo) const;
|
||||||
|
|
||||||
|
const Utils::FilePath m_recordFile;
|
||||||
|
ClipInfo m_clipInfo;
|
||||||
|
Utils::Process *m_process;
|
||||||
|
QByteArray m_lastOutputChunk;
|
||||||
|
RecordOptionsDialog *m_optionsDialog;
|
||||||
|
QAction *m_openClipAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder
|
||||||
33
src/plugins/screenrecorder/screenrecorder.qbs
Normal file
33
src/plugins/screenrecorder/screenrecorder.qbs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import qbs 1.0
|
||||||
|
|
||||||
|
QtcPlugin {
|
||||||
|
name: "ScreenRecorder"
|
||||||
|
|
||||||
|
Depends { name: "Qt.widgets" }
|
||||||
|
Depends { name: "Utils" }
|
||||||
|
|
||||||
|
Depends { name: "Core" }
|
||||||
|
|
||||||
|
files: [
|
||||||
|
"cropandtrim.cpp",
|
||||||
|
"cropandtrim.h",
|
||||||
|
"export.cpp",
|
||||||
|
"export.h",
|
||||||
|
"ffmpegutils.cpp",
|
||||||
|
"ffmpegutils.h",
|
||||||
|
"record.cpp",
|
||||||
|
"record.h",
|
||||||
|
"screenrecorder.qrc",
|
||||||
|
"screenrecorderconstants.h",
|
||||||
|
"screenrecorderplugin.cpp",
|
||||||
|
"screenrecordersettings.cpp",
|
||||||
|
"screenrecordersettings.h",
|
||||||
|
]
|
||||||
|
|
||||||
|
QtcTestFiles {
|
||||||
|
files: [
|
||||||
|
"screenrecorder_test.h",
|
||||||
|
"screenrecorder_test.cpp",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/plugins/screenrecorder/screenrecorder.qrc
Normal file
4
src/plugins/screenrecorder/screenrecorder.qrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/screenrecorder">
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
16
src/plugins/screenrecorder/screenrecorder_test.cpp
Normal file
16
src/plugins/screenrecorder/screenrecorder_test.cpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// 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 "screenrecorder_test.h"
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
namespace ScreenRecorder::Internal {
|
||||||
|
|
||||||
|
FFmpegOutputParserTest::FFmpegOutputParserTest(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
FFmpegOutputParserTest::~FFmpegOutputParserTest() = default;
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder::Internal
|
||||||
29
src/plugins/screenrecorder/screenrecorder_test.h
Normal file
29
src/plugins/screenrecorder/screenrecorder_test.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <coreplugin/dialogs/ioptionspage.h>
|
||||||
|
|
||||||
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
|
namespace ScreenRecorder::Internal {
|
||||||
|
|
||||||
|
class FFmpegOutputParserTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
FFmpegOutputParserTest(QObject *parent = nullptr);
|
||||||
|
~FFmpegOutputParserTest();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void testVersionParser_data();
|
||||||
|
void testVersionParser();
|
||||||
|
void testClipInfoParser_data();
|
||||||
|
void testClipInfoParser();
|
||||||
|
void testFfmpegOutputParser_data();
|
||||||
|
void testFfmpegOutputParser();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder::Internal
|
||||||
15
src/plugins/screenrecorder/screenrecorderconstants.h
Normal file
15
src/plugins/screenrecorder/screenrecorderconstants.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace ScreenRecorder::Constants {
|
||||||
|
|
||||||
|
const char TOOLSSETTINGSPAGE_ID[] = "Z.ScreenRecorder";
|
||||||
|
const char LOGGING_CATEGORY[] = "qtc.screenrecorder";
|
||||||
|
const char ACTION_ID[] = "ScreenRecorder.Action";
|
||||||
|
const char FFMPEG_COMMAND[] = "ffmpeg";
|
||||||
|
const char FFPROBE_COMMAND[] = "ffprobe";
|
||||||
|
const char FFMPEG_DOWNLOAD_URL[] = "https://ffmpeg.org/download.html";
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder::Constants
|
||||||
124
src/plugins/screenrecorder/screenrecorderplugin.cpp
Normal file
124
src/plugins/screenrecorder/screenrecorderplugin.cpp
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// 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 "screenrecorderconstants.h"
|
||||||
|
|
||||||
|
#include "cropandtrim.h"
|
||||||
|
#include "export.h"
|
||||||
|
#include "record.h"
|
||||||
|
#include "screenrecorderconstants.h"
|
||||||
|
#include "screenrecordersettings.h"
|
||||||
|
#include "screenrecordertr.h"
|
||||||
|
|
||||||
|
#ifdef WITH_TESTS
|
||||||
|
#include "screenrecorder_test.h"
|
||||||
|
#endif // WITH_TESTS
|
||||||
|
|
||||||
|
#include <extensionsystem/iplugin.h>
|
||||||
|
|
||||||
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/styledbar.h>
|
||||||
|
#include <utils/stylehelper.h>
|
||||||
|
#include <utils/temporaryfile.h>
|
||||||
|
|
||||||
|
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||||
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
|
#include <coreplugin/actionmanager/command.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
|
using namespace Utils;
|
||||||
|
using namespace Core;
|
||||||
|
|
||||||
|
namespace ScreenRecorder::Internal {
|
||||||
|
|
||||||
|
class ScreenRecorderDialog : public QDialog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScreenRecorderDialog(QWidget *parent = nullptr)
|
||||||
|
: QDialog(parent)
|
||||||
|
, m_recordFile("XXXXXX" + RecordWidget::recordFileExtension())
|
||||||
|
{
|
||||||
|
setWindowTitle(Tr::tr("Record Screen"));
|
||||||
|
setMinimumWidth(320);
|
||||||
|
StyleHelper::setPanelWidget(this);
|
||||||
|
|
||||||
|
m_recordFile.open();
|
||||||
|
m_recordWidget = new RecordWidget(FilePath::fromString(m_recordFile.fileName()));
|
||||||
|
|
||||||
|
m_cropAndTrimStatusWidget = new CropAndTrimWidget;
|
||||||
|
|
||||||
|
m_exportWidget = new ExportWidget;
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Column {
|
||||||
|
m_recordWidget,
|
||||||
|
Row { m_cropAndTrimStatusWidget, m_exportWidget },
|
||||||
|
noMargin(), spacing(0),
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
auto setLowerRowEndabled = [this] (bool enabled) {
|
||||||
|
m_cropAndTrimStatusWidget->setEnabled(enabled);
|
||||||
|
m_exportWidget->setEnabled(enabled);
|
||||||
|
};
|
||||||
|
setLowerRowEndabled(false);
|
||||||
|
connect(m_recordWidget, &RecordWidget::started,
|
||||||
|
this, [setLowerRowEndabled] { setLowerRowEndabled(false); });
|
||||||
|
connect(m_recordWidget, &RecordWidget::finished,
|
||||||
|
this, [this, setLowerRowEndabled] (const ClipInfo &clip) {
|
||||||
|
m_cropAndTrimStatusWidget->setClip(clip);
|
||||||
|
m_exportWidget->setClip(clip);
|
||||||
|
setLowerRowEndabled(true);
|
||||||
|
});
|
||||||
|
connect(m_cropAndTrimStatusWidget, &CropAndTrimWidget::cropRectChanged,
|
||||||
|
m_exportWidget, &ExportWidget::setCropRect);
|
||||||
|
connect(m_cropAndTrimStatusWidget, &CropAndTrimWidget::trimRangeChanged,
|
||||||
|
m_exportWidget, &ExportWidget::setTrimRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
RecordWidget *m_recordWidget;
|
||||||
|
TemporaryFile m_recordFile;
|
||||||
|
CropAndTrimWidget *m_cropAndTrimStatusWidget;
|
||||||
|
ExportWidget *m_exportWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ScreenRecorderPlugin final : public ExtensionSystem::IPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "ScreenRecorder.json")
|
||||||
|
|
||||||
|
public:
|
||||||
|
void initialize() final
|
||||||
|
{
|
||||||
|
auto action = new QAction(Tr::tr("Record Screen..."), this);
|
||||||
|
Command *cmd = ActionManager::registerAction(action, Constants::ACTION_ID,
|
||||||
|
Context(Core::Constants::C_GLOBAL));
|
||||||
|
connect(action, &QAction::triggered, this, &ScreenRecorderPlugin::showDialogOrSettings);
|
||||||
|
ActionContainer *mtools = ActionManager::actionContainer(Core::Constants::M_TOOLS);
|
||||||
|
mtools->addAction(cmd);
|
||||||
|
|
||||||
|
#ifdef WITH_TESTS
|
||||||
|
addTest<FFmpegOutputParserTest>();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void showDialogOrSettings()
|
||||||
|
{
|
||||||
|
if (!Internal::settings().toolsRegistered() &&
|
||||||
|
!Core::ICore::showOptionsDialog(Constants::TOOLSSETTINGSPAGE_ID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dialog = new ScreenRecorderDialog(Core::ICore::dialogParent());
|
||||||
|
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
dialog->show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder::Internal
|
||||||
|
|
||||||
|
#include "screenrecorderplugin.moc"
|
||||||
215
src/plugins/screenrecorder/screenrecordersettings.cpp
Normal file
215
src/plugins/screenrecorder/screenrecordersettings.cpp
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// 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 "screenrecordersettings.h"
|
||||||
|
|
||||||
|
#include "screenrecorderconstants.h"
|
||||||
|
#include "screenrecordertr.h"
|
||||||
|
|
||||||
|
#include <coreplugin/dialogs/ioptionspage.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <help/helpconstants.h>
|
||||||
|
|
||||||
|
#include <utils/fileutils.h>
|
||||||
|
#include <utils/environment.h>
|
||||||
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QScreen>
|
||||||
|
|
||||||
|
using namespace Utils;
|
||||||
|
|
||||||
|
namespace ScreenRecorder::Internal {
|
||||||
|
|
||||||
|
ScreenRecorderSettings &settings()
|
||||||
|
{
|
||||||
|
static ScreenRecorderSettings theSettings;
|
||||||
|
return theSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QRect stringListToRect(const QStringList &stringList)
|
||||||
|
{
|
||||||
|
return stringList.count() == 4 ? QRect(stringList[0].toInt(), stringList[1].toInt(),
|
||||||
|
stringList[2].toInt(), stringList[3].toInt())
|
||||||
|
: QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
static QStringList rectToStringList(const QRect &rect)
|
||||||
|
{
|
||||||
|
return {QString::number(rect.x()), QString::number(rect.y()),
|
||||||
|
QString::number(rect.width()), QString::number(rect.height())};
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenRecorderSettings::ScreenRecorderSettings()
|
||||||
|
{
|
||||||
|
setSettingsGroup("ScreenRecorder");
|
||||||
|
setAutoApply(false);
|
||||||
|
|
||||||
|
const QStringList versionArgs{"-version"};
|
||||||
|
|
||||||
|
ffmpegTool.setSettingsKey("FFmpegTool");
|
||||||
|
ffmpegTool.setExpectedKind(PathChooser::ExistingCommand);
|
||||||
|
ffmpegTool.setCommandVersionArguments(versionArgs);
|
||||||
|
const FilePath ffmpegDefault =
|
||||||
|
Environment::systemEnvironment().searchInPath(Constants::FFMPEG_COMMAND);
|
||||||
|
ffmpegTool.setDefaultValue(ffmpegDefault.toUserOutput());
|
||||||
|
ffmpegTool.setLabelText(Tr::tr("ffmpeg tool:"));
|
||||||
|
|
||||||
|
ffprobeTool.setSettingsKey("FFprobeTool");
|
||||||
|
ffprobeTool.setExpectedKind(PathChooser::ExistingCommand);
|
||||||
|
ffprobeTool.setCommandVersionArguments(versionArgs);
|
||||||
|
const FilePath ffprobeDefault =
|
||||||
|
Environment::systemEnvironment().searchInPath(Constants::FFPROBE_COMMAND);
|
||||||
|
ffprobeTool.setDefaultValue(ffprobeDefault.toUserOutput());
|
||||||
|
ffprobeTool.setLabelText(Tr::tr("ffprobe tool:"));
|
||||||
|
|
||||||
|
enableFileSizeLimit.setSettingsKey("EnableFileSizeLimit");
|
||||||
|
enableFileSizeLimit.setDefaultValue(true);
|
||||||
|
enableFileSizeLimit.setLabel(Tr::tr("Size limit for intermediate output file"));
|
||||||
|
enableFileSizeLimit.setLabelPlacement(BoolAspect::LabelPlacement::AtCheckBox);
|
||||||
|
|
||||||
|
fileSizeLimit.setSettingsKey("FileSizeLimit");
|
||||||
|
fileSizeLimit.setDefaultValue(1024);
|
||||||
|
fileSizeLimit.setRange(100, 1024 * 1024 * 2); // Up to 2GB
|
||||||
|
fileSizeLimit.setSuffix("MB");
|
||||||
|
fileSizeLimit.setEnabler(&enableFileSizeLimit);
|
||||||
|
|
||||||
|
enableRtBuffer.setSettingsKey("EnableRealTimeBuffer");
|
||||||
|
enableRtBuffer.setDefaultValue(true);
|
||||||
|
enableRtBuffer.setLabel(Tr::tr("RAM buffer for real-time frames"));
|
||||||
|
enableRtBuffer.setLabelPlacement(BoolAspect::LabelPlacement::AtCheckBox);
|
||||||
|
|
||||||
|
rtBufferSize.setSettingsKey("RealTimeBufferSize");
|
||||||
|
rtBufferSize.setDefaultValue(1024);
|
||||||
|
rtBufferSize.setRange(100, 1024 * 1024 * 2); // Up to 2GB
|
||||||
|
rtBufferSize.setSuffix("MB");
|
||||||
|
rtBufferSize.setEnabler(&enableRtBuffer);
|
||||||
|
|
||||||
|
logFfmpegCommandline.setSettingsKey("LogFFMpegCommandLine");
|
||||||
|
logFfmpegCommandline.setDefaultValue(false);
|
||||||
|
logFfmpegCommandline.setLabel(Tr::tr("Write command line of FFmpeg calls to General Messages"));
|
||||||
|
logFfmpegCommandline.setLabelPlacement(BoolAspect::LabelPlacement::AtCheckBox);
|
||||||
|
|
||||||
|
animatedImagesAsEndlessLoop.setSettingsKey("AnimatedImagesAsEndlessLoop");
|
||||||
|
animatedImagesAsEndlessLoop.setDefaultValue(true);
|
||||||
|
animatedImagesAsEndlessLoop.setLabel(Tr::tr("Export animated images as infinite loop"));
|
||||||
|
animatedImagesAsEndlessLoop.setLabelPlacement(BoolAspect::LabelPlacement::AtCheckBox);
|
||||||
|
|
||||||
|
lastOpenDirectory.setSettingsKey("LastOpenDir");
|
||||||
|
lastOpenDirectory.setExpectedKind(PathChooser::ExistingDirectory);
|
||||||
|
lastOpenDirectory.setDefaultValue(FileUtils::homePath().toString());
|
||||||
|
|
||||||
|
exportLastDirectory.setSettingsKey("ExportLastDir");
|
||||||
|
exportLastDirectory.setExpectedKind(PathChooser::ExistingDirectory);
|
||||||
|
exportLastDirectory.setDefaultValue(FileUtils::homePath().toString());
|
||||||
|
|
||||||
|
recordFrameRate.setSettingsKey("RecordFrameRate");
|
||||||
|
recordFrameRate.setDefaultValue(24);
|
||||||
|
recordFrameRate.setLabelText(Tr::tr("Recording frame rate:"));
|
||||||
|
recordFrameRate.setRange(1, 60);
|
||||||
|
recordFrameRate.setSuffix(" fps");
|
||||||
|
|
||||||
|
recordScreenId.setSettingsKey("RecordScreenID");
|
||||||
|
recordScreenId.setDefaultValue(0);
|
||||||
|
recordScreenId.setLabelText(Tr::tr("Screen ID:"));
|
||||||
|
|
||||||
|
recordScreenCropRect.setSettingsKey("RecordScreenCropRect");
|
||||||
|
recordScreenCropRect.setDefaultValue(rectToStringList({}));
|
||||||
|
|
||||||
|
setLayouter([this] {
|
||||||
|
using namespace Layouting;
|
||||||
|
auto websiteLabel = new QLabel;
|
||||||
|
websiteLabel->setText(QString("<a href=\"%1\">%1</a>").arg(Constants::FFMPEG_DOWNLOAD_URL));
|
||||||
|
websiteLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||||
|
websiteLabel->setOpenExternalLinks(true);
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
using namespace Layouting;
|
||||||
|
return Column {
|
||||||
|
Group {
|
||||||
|
title(Tr::tr("FFmpeg installation")),
|
||||||
|
Form {
|
||||||
|
ffmpegTool, br,
|
||||||
|
ffprobeTool, br,
|
||||||
|
websiteLabel, br,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
title(Tr::tr("Record settings")),
|
||||||
|
Column {
|
||||||
|
Row { enableFileSizeLimit, fileSizeLimit, st },
|
||||||
|
Row { enableRtBuffer, rtBufferSize, st },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
title(Tr::tr("Export settings")),
|
||||||
|
Column {
|
||||||
|
animatedImagesAsEndlessLoop,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logFfmpegCommandline,
|
||||||
|
st,
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
});
|
||||||
|
|
||||||
|
readSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenRecorderSettings::toolsRegistered() const
|
||||||
|
{
|
||||||
|
return ffmpegTool().isExecutableFile() && ffprobeTool().isExecutableFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenRecorderSettings::RecordSettings ScreenRecorderSettings::sanitizedRecordSettings(const RecordSettings &settings)
|
||||||
|
{
|
||||||
|
const int screenIdFromSettings = settings.screenId;
|
||||||
|
const QList<QScreen*> screens = QGuiApplication::screens();
|
||||||
|
const int effectiveScreenId = qMin(screenIdFromSettings, screens.size() - 1);
|
||||||
|
const QScreen *screen = screens.at(effectiveScreenId);
|
||||||
|
const QSize screenSize = screen->size() * screen->devicePixelRatio();
|
||||||
|
const QRect screenRect(QPoint(), screenSize);
|
||||||
|
const QRect cropRectFromSettings = settings.cropRect;
|
||||||
|
const QRect effectiveCropRect = screenIdFromSettings == effectiveScreenId
|
||||||
|
? screenRect.intersected(cropRectFromSettings) : QRect();
|
||||||
|
return {effectiveScreenId, effectiveCropRect, settings.frameRate};
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenRecorderSettings::RecordSettings ScreenRecorderSettings::recordSettings() const
|
||||||
|
{
|
||||||
|
return sanitizedRecordSettings({int(recordScreenId()), stringListToRect(recordScreenCropRect()),
|
||||||
|
int(recordFrameRate())});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenRecorderSettings::applyRecordSettings(const RecordSettings &settings)
|
||||||
|
{
|
||||||
|
recordScreenId.setValue(settings.screenId);
|
||||||
|
recordScreenId.apply();
|
||||||
|
recordScreenId.writeToSettingsImmediatly();
|
||||||
|
recordScreenCropRect.setValue(rectToStringList(settings.cropRect));
|
||||||
|
recordScreenCropRect.apply();
|
||||||
|
recordScreenCropRect.writeToSettingsImmediatly();
|
||||||
|
recordFrameRate.setValue(settings.frameRate);
|
||||||
|
recordFrameRate.apply();
|
||||||
|
recordFrameRate.writeToSettingsImmediatly();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScreenRecorderSettingsPage : public Core::IOptionsPage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScreenRecorderSettingsPage()
|
||||||
|
{
|
||||||
|
setId(Constants::TOOLSSETTINGSPAGE_ID);
|
||||||
|
setDisplayName(Tr::tr("Screen Recording"));
|
||||||
|
setCategory(Help::Constants::HELP_CATEGORY);
|
||||||
|
setSettingsProvider([] { return &settings(); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static const ScreenRecorderSettingsPage settingsPage;
|
||||||
|
|
||||||
|
} // ImageViewer::Internal
|
||||||
50
src/plugins/screenrecorder/screenrecordersettings.h
Normal file
50
src/plugins/screenrecorder/screenrecordersettings.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QScreen;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
namespace ScreenRecorder::Internal {
|
||||||
|
|
||||||
|
class ScreenRecorderSettings : public Utils::AspectContainer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScreenRecorderSettings();
|
||||||
|
|
||||||
|
bool toolsRegistered() const;
|
||||||
|
|
||||||
|
struct RecordSettings {
|
||||||
|
const int screenId;
|
||||||
|
const QRect cropRect;
|
||||||
|
const int frameRate;
|
||||||
|
};
|
||||||
|
static RecordSettings sanitizedRecordSettings(const RecordSettings &settings);
|
||||||
|
RecordSettings recordSettings() const;
|
||||||
|
void applyRecordSettings(const RecordSettings &settings);
|
||||||
|
|
||||||
|
// Visible in Settings page
|
||||||
|
Utils::FilePathAspect ffmpegTool{this};
|
||||||
|
Utils::FilePathAspect ffprobeTool{this};
|
||||||
|
Utils::BoolAspect enableFileSizeLimit{this};
|
||||||
|
Utils::IntegerAspect fileSizeLimit{this}; // in MB
|
||||||
|
Utils::BoolAspect enableRtBuffer{this};
|
||||||
|
Utils::IntegerAspect rtBufferSize{this}; // in MB
|
||||||
|
Utils::BoolAspect logFfmpegCommandline{this};
|
||||||
|
Utils::BoolAspect animatedImagesAsEndlessLoop{this};
|
||||||
|
|
||||||
|
// Used in other places
|
||||||
|
Utils::FilePathAspect lastOpenDirectory{this};
|
||||||
|
Utils::FilePathAspect exportLastDirectory{this};
|
||||||
|
Utils::IntegerAspect recordFrameRate{this};
|
||||||
|
Utils::IntegerAspect recordScreenId{this};
|
||||||
|
Utils::StringListAspect recordScreenCropRect{this};
|
||||||
|
};
|
||||||
|
|
||||||
|
ScreenRecorderSettings &settings();
|
||||||
|
|
||||||
|
} // ScreenRecorder::Internal
|
||||||
15
src/plugins/screenrecorder/screenrecordertr.h
Normal file
15
src/plugins/screenrecorder/screenrecordertr.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
namespace ScreenRecorder {
|
||||||
|
|
||||||
|
struct Tr
|
||||||
|
{
|
||||||
|
Q_DECLARE_TR_FUNCTIONS(QtC::ScreenRecorder)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ScreenRecorder
|
||||||
Reference in New Issue
Block a user