übernahme Code Shortcut

This commit is contained in:
georg0480
2026-01-31 15:28:10 +01:00
parent 6f4d6b9301
commit ef46c21291
1787 changed files with 1126465 additions and 0 deletions

683
src/videowidget.cpp Normal file
View File

@@ -0,0 +1,683 @@
/*
* Copyright (c) 2011-2025 Meltytech, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "videowidget.h"
#include "Logger.h"
#include "dialogs/durationdialog.h"
#include "mainwindow.h"
#include "qmltypes/qmlfilter.h"
#include "qmltypes/qmlutilities.h"
#include "settings.h"
#include <Mlt.h>
#include <QOffscreenSurface>
#include <QOpenGLContext>
#include <QQuickItem>
#include <QUrl>
#include <QtQml>
#include <QtWidgets>
using namespace Mlt;
VideoWidget::VideoWidget(QObject *parent)
: QQuickWidget(QmlUtilities::sharedEngine(), (QWidget *) parent)
, Controller()
, m_grid(0)
, m_initSem(0)
, m_isInitialized(false)
, m_frameRenderer(nullptr)
, m_zoom(0.0f)
, m_offset(QPoint(0, 0))
, m_snapToGrid(true)
, m_scrubAudio(false)
, m_maxTextureSize(4096)
, m_hideVui(false)
{
LOG_DEBUG() << "begin";
setAttribute(Qt::WA_AcceptTouchEvents);
setResizeMode(QQuickWidget::SizeRootObjectToView);
setClearColor(palette().window().color());
QDir importPath = QmlUtilities::qmlDir();
importPath.cd("modules");
engine()->addImportPath(importPath.path());
QmlUtilities::setCommonProperties(rootContext());
rootContext()->setContextProperty("video", this);
m_refreshTimer.setInterval(10);
m_refreshTimer.setSingleShot(true);
if (Settings.playerGPU())
m_glslManager.reset(new Filter(profile(), "glsl.manager"));
if ((m_glslManager && !m_glslManager->is_valid())) {
m_glslManager.reset();
}
connect(quickWindow(),
&QQuickWindow::visibilityChanged,
this,
&VideoWidget::setBlankScene,
Qt::QueuedConnection);
connect(&m_refreshTimer, &QTimer::timeout, this, &VideoWidget::onRefreshTimeout);
connect(this, &VideoWidget::rectChanged, this, &VideoWidget::zoomChanged);
LOG_DEBUG() << "end";
}
VideoWidget::~VideoWidget()
{
LOG_DEBUG() << "begin";
stop();
if (m_frameRenderer && m_frameRenderer->isRunning()) {
m_frameRenderer->quit();
m_frameRenderer->wait();
m_frameRenderer->deleteLater();
}
LOG_DEBUG() << "end";
}
void VideoWidget::initialize()
{
LOG_DEBUG() << "begin";
m_frameRenderer = new FrameRenderer();
connect(m_frameRenderer,
&FrameRenderer::frameDisplayed,
this,
&VideoWidget::onFrameDisplayed,
Qt::QueuedConnection);
connect(m_frameRenderer,
&FrameRenderer::frameDisplayed,
this,
&VideoWidget::frameDisplayed,
Qt::QueuedConnection);
connect(m_frameRenderer, SIGNAL(imageReady()), SIGNAL(imageReady()));
m_initSem.release();
m_isInitialized = true;
LOG_DEBUG() << "end";
}
void VideoWidget::renderVideo() {}
void VideoWidget::setBlankScene()
{
quickWindow()->setColor(palette().window().color());
setSource(QmlUtilities::blankVui());
m_savedQmlSource.clear();
}
void VideoWidget::resizeVideo(int width, int height)
{
double x, y, w, h;
double this_aspect = (double) width / height;
double video_aspect = profile().dar();
// Special case optimisation to negate odd effect of sample aspect ratio
// not corresponding exactly with image resolution.
if ((int) (this_aspect * 1000) == (int) (video_aspect * 1000)) {
w = width;
h = height;
}
// Use OpenGL to normalise sample aspect ratio
else if (height * video_aspect > width) {
w = width;
h = width / video_aspect;
} else {
w = height * video_aspect;
h = height;
}
x = (width - w) / 2.0;
y = (height - h) / 2.0;
m_rect.setRect(x, y, w, h);
emit rectChanged();
}
void VideoWidget::resizeEvent(QResizeEvent *event)
{
QQuickWidget::resizeEvent(event);
resizeVideo(event->size().width(), event->size().height());
}
void VideoWidget::onRefreshTimeout()
{
Controller::refreshConsumer(m_scrubAudio);
m_scrubAudio = false;
}
void VideoWidget::mousePressEvent(QMouseEvent *event)
{
QQuickWidget::mousePressEvent(event);
if (event->isAccepted())
return;
if (event->button() == Qt::LeftButton)
m_dragStart = event->pos();
else if (event->button() == Qt::MiddleButton)
m_mousePosition = event->pos();
if (MLT.isClip())
emit dragStarted();
}
void VideoWidget::mouseMoveEvent(QMouseEvent *event)
{
QQuickWidget::mouseMoveEvent(event);
if (event->isAccepted())
return;
if (event->buttons() & Qt::MiddleButton) {
emit offsetChanged(m_offset + m_mousePosition - event->pos());
m_mousePosition = event->pos();
return;
}
if (event->modifiers() == (Qt::ShiftModifier | Qt::AltModifier) && m_producer) {
emit seekTo(m_producer->get_length() * event->position().x() / width());
return;
}
if (!(event->buttons() & Qt::LeftButton))
return;
if (m_dragStart.isNull())
return;
if ((event->pos() - m_dragStart).manhattanLength() < QApplication::startDragDistance())
return;
// Reset the drag point to prevent repeating drag actions.
m_dragStart.setX(0);
m_dragStart.setY(0);
if (!MLT.producer())
return;
if (MLT.isMultitrack() || MLT.isPlaylist()) {
MAIN.showStatusMessage(tr("You cannot drag from Project."));
return;
} else if (!MLT.isSeekableClip()) {
MAIN.showStatusMessage(tr("You cannot drag a non-seekable source"));
return;
}
// Cannot show a DurationDialog during mouse drag
// This is usually MLT.isLiveProducer(), but that checks for > 1 week,
// which is too long for the timeline.
if (m_producer->get_playtime() > qRound(profile().fps() * 24 * 3600)) {
m_producer->set_in_and_out(0, profile().fps() * 60 - 1);
}
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
mimeData->setData(Mlt::XmlMimeType, MLT.XML().toUtf8());
drag->setMimeData(mimeData);
mimeData->setText(QString::number(MLT.producer()->get_playtime()));
if (m_frameRenderer && m_frameRenderer->getDisplayFrame().is_valid()) {
Mlt::Frame displayFrame(m_frameRenderer->getDisplayFrame().clone(false, true));
QImage displayImage
= MLT.image(&displayFrame, 45 * MLT.profile().dar(), 45).scaledToHeight(45);
drag->setPixmap(QPixmap::fromImage(displayImage));
}
drag->setHotSpot(QPoint(0, 0));
drag->exec(Qt::CopyAction);
}
void VideoWidget::keyPressEvent(QKeyEvent *event)
{
QQuickWidget::keyPressEvent(event);
if (event->isAccepted())
return;
MAIN.keyPressEvent(event);
}
bool VideoWidget::event(QEvent *event)
{
bool result = QQuickWidget::event(event);
if (event->type() == QEvent::PaletteChange && m_sharedFrame.is_valid())
onFrameDisplayed(m_sharedFrame);
return result;
}
int VideoWidget::setProducer(Mlt::Producer *producer, bool isMulti)
{
int error = Controller::setProducer(producer, isMulti);
if (!error) {
error = reconfigure(isMulti);
if (!error) {
// The profile display aspect ratio may have changed.
resizeVideo(width(), height());
}
}
return error;
}
void VideoWidget::createThread(RenderThread **thread, thread_function_t function, void *data)
{
#ifdef Q_OS_WIN
// On Windows, MLT event consumer-thread-create is fired from the Qt main thread.
while (!m_isInitialized)
QCoreApplication::processEvents();
#else
if (!m_isInitialized) {
m_initSem.acquire();
}
#endif
if (!m_renderThread) {
m_renderThread.reset(new RenderThread(function, data));
(*thread) = m_renderThread.get();
(*thread)->start();
} else {
m_renderThread->start();
}
}
static void onThreadCreate(mlt_properties owner, VideoWidget *self, mlt_event_data data)
{
Q_UNUSED(owner)
auto threadData = (mlt_event_data_thread *) Mlt::EventData(data).to_object();
if (threadData) {
self->createThread((RenderThread **) threadData->thread,
threadData->function,
threadData->data);
}
}
static void onThreadJoin(mlt_properties owner, VideoWidget *self, mlt_event_data data)
{
Q_UNUSED(owner)
Q_UNUSED(self)
auto threadData = (mlt_event_data_thread *) Mlt::EventData(data).to_object();
if (threadData && threadData->thread) {
auto renderThread = (RenderThread *) *threadData->thread;
if (renderThread) {
renderThread->quit();
renderThread->wait();
}
}
}
void VideoWidget::startGlsl()
{
if (m_glslManager) {
m_glslManager->fire_event("init glsl");
if (!m_glslManager->get_int("glsl_supported")) {
m_glslManager.reset();
// Need to destroy MLT global reference to prevent filters from trying to use GPU.
mlt_properties_clear(mlt_global_properties(), "glslManager");
emit gpuNotSupported();
} else {
emit started();
}
}
}
static void onThreadStarted(mlt_properties owner, VideoWidget *self)
{
Q_UNUSED(owner)
self->startGlsl();
}
void VideoWidget::stopGlsl()
{
//TODO This is commented out for now because it is causing crashes.
//Technically, this should be the correct thing to do, but it appears
//some changes in the 15.01 and 15.03 releases have created regression
//with respect to restarting the consumer in GPU mode.
// m_glslManager->fire_event("close glsl");
}
static void onThreadStopped(mlt_properties owner, VideoWidget *self)
{
Q_UNUSED(owner)
self->stopGlsl();
}
int VideoWidget::reconfigure(bool isMulti)
{
int error = 0;
// use SDL for audio, OpenGL for video
QString serviceName = property("mlt_service").toString();
if (!m_consumer || !m_consumer->is_valid()) {
if (serviceName.isEmpty()) {
m_consumer.reset(new Mlt::FilteredConsumer(previewProfile(), "sdl2_audio"));
if (m_consumer->is_valid())
serviceName = "sdl2_audio";
else
serviceName = "rtaudio";
m_consumer.reset();
}
if (isMulti)
m_consumer.reset(new Mlt::FilteredConsumer(previewProfile(), "multi"));
else
m_consumer.reset(
new Mlt::FilteredConsumer(previewProfile(), serviceName.toLatin1().constData()));
m_threadStartEvent.reset();
m_threadStopEvent.reset();
m_threadCreateEvent.reset();
m_threadJoinEvent.reset();
}
if (m_consumer->is_valid()) {
// Connect the producer to the consumer - tell it to "run" later
if (m_producer && m_producer->is_valid())
m_consumer->connect(*m_producer);
// Make an event handler for when a frame's image should be displayed
m_consumer->listen("consumer-frame-show", this, (mlt_listener) on_frame_show);
m_consumer->set("real_time", MLT.realTime());
m_consumer->set("scale", double(Settings.playerPreviewScale()) / MLT.profile().height());
const int processingMode = property("processing_mode").toInt();
const bool isDeckLinkHLG = serviceName.startsWith("decklink")
&& property("decklinkGamma").toInt() == 1;
switch (processingMode) {
case ShotcutSettings::Native10Cpu:
case ShotcutSettings::Linear10Cpu:
m_consumer->set("mlt_image_format", "rgba64");
break;
case ShotcutSettings::Linear10GpuCpu:
m_consumer->set("mlt_image_format", isDeckLinkHLG ? "yuv444p10" : "rgba64");
break;
default: // Native8Cpu
m_consumer->set("mlt_image_format",
serviceName.startsWith("decklink") ? "yuv422" : "yuv420p");
break;
}
m_consumer->set("channels", property("audio_channels").toInt());
if (property("audio_channels").toInt() == 4) {
m_consumer->set("channel_layout", "quad");
} else {
m_consumer->set("channel_layout", "auto");
}
switch (MLT.profile().colorspace()) {
case 601:
case 170:
m_consumer->set("color_trc", "smpte170m");
break;
case 240:
m_consumer->set("color_trc", "smpte240m");
break;
case 470:
m_consumer->set("color_trc", "bt470bg");
break;
case 2020:
if (isDeckLinkHLG) {
m_consumer->set("color_trc", "arib-std-b67");
} else {
m_consumer->clear("color_trc");
}
break;
default:
m_consumer->set("color_trc", "bt709");
break;
}
if (processingMode == ShotcutSettings::Linear10Cpu
|| (processingMode == ShotcutSettings::Linear10GpuCpu
&& property("decklinkGamma").toInt() != 1)) {
m_consumer->set("mlt_color_trc", "linear");
} else {
m_consumer->clear("mlt_color_trc");
}
if (isMulti) {
m_consumer->set("terminate_on_pause", 0);
m_consumer->set("0", serviceName.toLatin1().constData());
if (!profile().progressive())
m_consumer->set("0.progressive", property("progressive").toBool());
m_consumer->set("0.rescale", property("rescale").toString().toLatin1().constData());
m_consumer->set("0.deinterlacer",
property("deinterlacer").toString().toLatin1().constData());
m_consumer->set("0.buffer", qMax(25, qRound(profile().fps())));
m_consumer->set("0.prefill", 8);
m_consumer->set("0.drop_max", qRound(profile().fps() / 4.0));
if (property("keyer").isValid())
m_consumer->set("0.keyer", property("keyer").toInt());
m_consumer->set("0.video_delay", Settings.playerVideoDelayMs());
} else {
if (!profile().progressive())
m_consumer->set("progressive", property("progressive").toBool());
m_consumer->set("rescale", property("rescale").toString().toLatin1().constData());
m_consumer->set("deinterlacer",
property("deinterlacer").toString().toLatin1().constData());
m_consumer->set("buffer", qMax(25, qRound(profile().fps())));
m_consumer->set("prefill", 8);
m_consumer->set("drop_max", qRound(profile().fps() / 4.0));
if (property("keyer").isValid())
m_consumer->set("keyer", property("keyer").toInt());
m_consumer->set("video_delay", Settings.playerVideoDelayMs());
}
if (m_glslManager) {
if (!m_threadCreateEvent)
m_threadCreateEvent.reset(m_consumer->listen("consumer-thread-create",
this,
(mlt_listener) onThreadCreate));
if (!m_threadJoinEvent)
m_threadJoinEvent.reset(
m_consumer->listen("consumer-thread-join", this, (mlt_listener) onThreadJoin));
if (!m_threadStartEvent)
m_threadStartEvent.reset(m_consumer->listen("consumer-thread-started",
this,
(mlt_listener) onThreadStarted));
if (!m_threadStopEvent)
m_threadStopEvent.reset(m_consumer->listen("consumer-thread-stopped",
this,
(mlt_listener) onThreadStopped));
} else {
emit started();
}
} else {
// Cleanup on error
error = 2;
Controller::closeConsumer();
Controller::close();
}
return error;
}
void VideoWidget::refreshConsumer(bool scrubAudio)
{
scrubAudio |= isPaused() ? scrubAudio : Settings.playerScrubAudio();
m_scrubAudio |= scrubAudio;
m_refreshTimer.start();
}
QPoint VideoWidget::offset() const
{
if (m_zoom == 0.0) {
return QPoint(0, 0);
} else {
return QPoint(m_offset.x() - (MLT.profile().width() * m_zoom - width()) / 2,
m_offset.y() - (MLT.profile().height() * m_zoom - height()) / 2);
}
}
QImage VideoWidget::image() const
{
SharedFrame frame = m_frameRenderer->getDisplayFrame();
if (frame.is_valid()) {
const uint8_t *image = frame.get_image(mlt_image_rgba);
if (image) {
int width = frame.get_image_width();
int height = frame.get_image_height();
QImage temp(image, width, height, QImage::Format_RGBA8888);
return temp.copy();
}
}
return QImage();
}
bool VideoWidget::imageIsProxy() const
{
bool isProxy = false;
SharedFrame frame = m_frameRenderer->getDisplayFrame();
if (frame.is_valid()) {
Mlt::Producer *frameProducer = frame.get_original_producer();
if (frameProducer && frameProducer->is_valid() && frameProducer->get_int(kIsProxyProperty)) {
isProxy = true;
}
delete frameProducer;
}
return isProxy;
}
void VideoWidget::requestImage() const
{
m_frameRenderer->requestImage();
}
void VideoWidget::toggleVuiDisplay()
{
m_hideVui = !m_hideVui;
refreshConsumer();
}
void VideoWidget::onFrameDisplayed(const SharedFrame &frame)
{
m_mutex.lock();
m_sharedFrame = frame;
m_mutex.unlock();
bool isVui = frame.get_int(kShotcutVuiMetaProperty) && !m_hideVui;
if (!isVui && source() != QmlUtilities::blankVui()) {
m_savedQmlSource = source();
setSource(QmlUtilities::blankVui());
} else if (isVui && !m_savedQmlSource.isEmpty() && source() != m_savedQmlSource) {
setSource(m_savedQmlSource);
}
quickWindow()->update();
}
void VideoWidget::setGrid(int grid)
{
m_grid = grid;
emit gridChanged();
quickWindow()->update();
}
void VideoWidget::setZoom(float zoom)
{
m_zoom = zoom;
emit zoomChanged();
// Reset the VUI control
setSource(source());
quickWindow()->update();
}
void VideoWidget::setOffsetX(int x)
{
m_offset.setX(x);
emit offsetChanged();
quickWindow()->update();
}
void VideoWidget::setOffsetY(int y)
{
m_offset.setY(y);
emit offsetChanged();
quickWindow()->update();
}
void VideoWidget::setCurrentFilter(QmlFilter *filter, QmlMetadata *meta)
{
m_hideVui = false;
if (meta && meta->type() == QmlMetadata::Filter
&& QFile::exists(meta->vuiFilePath().toLocalFile())) {
filter->producer().set(kShotcutVuiMetaProperty, 1);
rootContext()->setContextProperty("filter", filter);
setSource(meta->vuiFilePath());
refreshConsumer();
} else {
setBlankScene();
}
}
void VideoWidget::setSnapToGrid(bool snap)
{
m_snapToGrid = snap;
emit snapToGridChanged();
}
// MLT consumer-frame-show event handler
void VideoWidget::on_frame_show(mlt_consumer, VideoWidget *widget, mlt_event_data data)
{
auto frame = Mlt::EventData(data).to_frame();
if (frame.is_valid() && frame.get_int("rendered")) {
int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000;
if (widget->m_frameRenderer
&& widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) {
QMetaObject::invokeMethod(widget->m_frameRenderer,
"showFrame",
Qt::QueuedConnection,
Q_ARG(Mlt::Frame, frame));
} else if (!Settings.playerRealtime()) {
LOG_WARNING() << "VideoWidget dropped frame" << frame.get_position();
}
}
}
RenderThread::RenderThread(thread_function_t function, void *data)
: QThread{nullptr}
, m_function{function}
, m_data{data}
, m_context{new QOpenGLContext}
, m_surface{new QOffscreenSurface}
{
QSurfaceFormat format;
format.setProfile(QSurfaceFormat::CoreProfile);
format.setMajorVersion(3);
format.setMinorVersion(2);
format.setDepthBufferSize(0);
format.setStencilBufferSize(0);
m_context->setFormat(format);
m_context->create();
m_context->moveToThread(this);
m_surface->setFormat(format);
m_surface->create();
}
RenderThread::~RenderThread()
{
m_surface->destroy();
}
void RenderThread::run()
{
Q_ASSERT(m_context->isValid());
m_context->makeCurrent(m_surface.get());
m_function(m_data);
m_context->doneCurrent();
}
FrameRenderer::FrameRenderer()
: QThread(nullptr)
, m_semaphore(3)
, m_imageRequested(false)
{
setObjectName("FrameRenderer");
moveToThread(this);
start();
}
FrameRenderer::~FrameRenderer() {}
void FrameRenderer::showFrame(Mlt::Frame frame)
{
m_displayFrame = SharedFrame(frame);
emit frameDisplayed(m_displayFrame);
if (m_imageRequested) {
m_imageRequested = false;
emit imageReady();
}
m_semaphore.release();
}
void FrameRenderer::requestImage()
{
m_imageRequested = true;
}
SharedFrame FrameRenderer::getDisplayFrame()
{
return m_displayFrame;
}