/*****************************************************************************
* Copyright (C) 2019 VLC authors and VideoLAN
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#include "videosurface.hpp"
#include <QSGRectangleNode>
#include <QThreadPool>
#include <vlc_window.h>
#include <QQuickRenderControl>
#ifdef QT_DECLARATIVE_PRIVATE
# include <QtGui/qpa/qplatformwindow.h>
#endif
#include "maininterface/mainctx.hpp"
VideoSurfaceProvider::VideoSurfaceProvider(bool threadedSurfaceUpdates, QObject* parent)
: QObject(parent)
, m_threadedSurfaceUpdates(threadedSurfaceUpdates)
{
}
bool VideoSurfaceProvider::isEnabled()
{
return m_voutWindow != nullptr;
}
bool VideoSurfaceProvider::hasVideoEmbed() const
{
return m_videoEmbed;
}
void VideoSurfaceProvider::enable(vlc_window_t* voutWindow)
{
assert(voutWindow);
m_voutWindow = voutWindow;
emit videoEnabledChanged(true);
}
void VideoSurfaceProvider::disable()
{
setVideoEmbed(false);
m_voutWindow = nullptr;
emit videoEnabledChanged(false);
}
void VideoSurfaceProvider::setVideoEmbed(bool embed)
{
m_videoEmbed = embed;
emit hasVideoEmbedChanged(embed);
}
void VideoSurfaceProvider::onWindowClosed()
{
if (m_voutWindow)
vlc_window_ReportClose(m_voutWindow);
}
void VideoSurfaceProvider::onMousePressed(int vlcButton)
{
if (m_voutWindow)
vlc_window_ReportMousePressed(m_voutWindow, vlcButton);
}
void VideoSurfaceProvider::onMouseReleased(int vlcButton)
{
if (m_voutWindow)
vlc_window_ReportMouseReleased(m_voutWindow, vlcButton);
}
void VideoSurfaceProvider::onMouseDoubleClick(int vlcButton)
{
if (m_voutWindow)
vlc_window_ReportMouseDoubleClick(m_voutWindow, vlcButton);
}
void VideoSurfaceProvider::onMouseMoved(float x, float y)
{
if (m_voutWindow)
vlc_window_ReportMouseMoved(m_voutWindow, x, y);
}
void VideoSurfaceProvider::onMouseWheeled(int vlcButton)
{
if (m_voutWindow)
vlc_window_ReportKeyPress(m_voutWindow, vlcButton);
}
void VideoSurfaceProvider::onKeyPressed(int key, Qt::KeyboardModifiers modifiers)
{
QKeyEvent event(QEvent::KeyPress, key, modifiers);
int vlckey = qtEventToVLCKey(&event);
if (m_voutWindow)
vlc_window_ReportKeyPress(m_voutWindow, vlckey);
}
void VideoSurfaceProvider::onSurfacePropertiesChanged(const std::optional<QSizeF>& size,
const std::optional<QPointF>& position,
const std::optional<qreal>& scale)
{
emit surfacePropertiesChanged(size, position, scale);
if (m_voutWindow && size)
vlc_window_ReportSize(m_voutWindow, std::ceil(size->width()), std::ceil(size->height()));
}
VideoSurface::VideoSurface(QQuickItem* parent)
: ViewBlockingRectangle(parent)
{
setAcceptHoverEvents(true);
setAcceptedMouseButtons(Qt::AllButtons);
setFlag(ItemAcceptsInputMethod, true);
{
// This is for initialization, they are adjusted in `::updatePaintNode()`:
setUpdateRenderPosition(true);
setFlag(ItemHasContents, false); // `setUpdateRenderPosition(true)` sets `ItemHasContents`
}
}
int VideoSurface::qtMouseButton2VLC( Qt::MouseButton qtButton )
{
switch( qtButton )
{
case Qt::LeftButton:
return 0;
case Qt::RightButton:
return 2;
case Qt::MiddleButton:
return 1;
default:
return -1;
}
}
void VideoSurface::mousePressEvent(QMouseEvent* event)
{
int vlc_button = qtMouseButton2VLC( event->button() );
if( vlc_button >= 0 )
{
emit mousePressed(vlc_button);
event->accept();
}
else
event->ignore();
}
void VideoSurface::mouseReleaseEvent(QMouseEvent* event)
{
int vlc_button = qtMouseButton2VLC( event->button() );
if( vlc_button >= 0 )
{
emit mouseReleased(vlc_button);
event->accept();
}
else
event->ignore();
}
void VideoSurface::mouseMoveEvent(QMouseEvent* event)
{
QPointF current_pos = event->position();
QQuickWindow* window = this->window();
if (!window)
return;
qreal dpr = window->effectiveDevicePixelRatio();
emit mouseMoved(current_pos.x() * dpr, current_pos.y() * dpr);
event->accept();
}
void VideoSurface::hoverMoveEvent(QHoverEvent* event)
{
QPointF current_pos = event->position();
if (current_pos != m_oldHoverPos)
{
QQuickWindow* window = this->window();
if (!window)
return;
qreal dpr = window->effectiveDevicePixelRatio();
emit mouseMoved(current_pos.x() * dpr, current_pos.y() * dpr);
m_oldHoverPos = current_pos;
}
event->accept();
}
void VideoSurface::mouseDoubleClickEvent(QMouseEvent* event)
{
int vlc_button = qtMouseButton2VLC( event->button() );
if( vlc_button >= 0 )
{
emit mouseDblClicked(vlc_button);
event->accept();
}
else
event->ignore();
}
void VideoSurface::keyPressEvent(QKeyEvent* event)
{
emit keyPressed(event->key(), event->modifiers());
event->ignore();
}
#if QT_CONFIG(wheelevent)
void VideoSurface::wheelEvent(QWheelEvent *event)
{
m_wheelEventConverter.wheelEvent(event);
event->accept();
}
#endif
Qt::CursorShape VideoSurface::getCursorShape() const
{
return cursor().shape();
}
void VideoSurface::setCursorShape(Qt::CursorShape shape)
{
setCursor(shape);
}
void VideoSurface::synchronize()
{
// This may be called from the rendering thread, not necessarily
// during synchronization (GUI thread is not blocked). Try to
// be very careful.
QSizeF size;
QPointF position;
static const bool isObjectThread = QThread::currentThread() == thread();
if (isObjectThread)
{
assert(QThread::currentThread() == thread());
// Item's thread (GUI thread):
size = this->size();
position = this->mapToScene(QPointF(0,0));
}
else
{
assert(QThread::currentThread() != thread());
// Render thread:
size = renderSize();
position = renderPosition();
}
std::optional<QSizeF> newSize;
std::optional<QPointF> newPosition;
std::optional<qreal> newScale;
if (m_allDirty || m_dprDirty || m_oldRenderSize != size)
{
if (!size.isEmpty())
{
newSize = size * m_dpr;
m_oldRenderSize = size;
}
}
if (m_allDirty || m_dprDirty || m_oldRenderPosition != position)
{
if (position.x() >= 0.0 && position.y() >= 0.0)
{
newPosition = position * m_dpr; // render position is relative to scene/viewport
m_oldRenderPosition = position;
}
}
if (m_allDirty || m_dprDirty)
{
newScale = m_dpr;
m_dprDirty = false;
}
if (newPosition || newSize || newScale)
emit surfacePropertiesChanged(newSize, newPosition, newScale);
m_allDirty = false;
}
void VideoSurface::itemChange(ItemChange change, const ItemChangeData &value)
{
switch (change)
{
case ItemSceneChange:
{
// It is intentional that window connection is made in `::updatePaintNode()`, and not here, because we don't
// want to explicitly connect whenever `ItemHasContents`, `isVisible()`, `window()` are all satisfied, which
// is implicitly the case with `::updatePaintNode()` (it is only called when all these are satisfied). This
// is strictly for maintenance reasons.
disconnect(m_synchConnection);
// if window changed but is valid, we can signal dpr change just to be sure, It is not clear if Qt signals
// ItemDevicePixelRatioHasChanged when item's window/scene changes to a new window that has different DPR:
if (value.window)
[[fallthrough]];
else
break;
}
case ItemDevicePixelRatioHasChanged:
{
m_dprChanged = true;
// Request update, so that `updatePaintNode()` gets called which updates the DPR for `::synchronize()`:
if (flags().testFlag(ItemHasContents)) // "Only items which specify QQuickItem::ItemHasContents are allowed to call QQuickItem::update()."
update();
break;
}
case ItemVisibleHasChanged:
{
if (!value.boolValue)
{
// Connection is made in `::updatePaintNode()` (which is called when both the item is visible and `ItemHasContents` is set).
disconnect(m_synchConnection);
}
break;
}
default: break;
}
QQuickItem::itemChange(change, value);
}
void VideoSurface::setVideoSurfaceProvider(VideoSurfaceProvider *newVideoSurfaceProvider)
{
if (m_provider == newVideoSurfaceProvider)
return;
if (m_provider)
{
disconnect(this, nullptr, m_provider, nullptr);
disconnect(&m_wheelEventConverter, nullptr, m_provider, nullptr);
disconnect(m_provider, nullptr, this, nullptr);
assert(m_provider->videoSurface() == this);
m_provider->setVideoSurface({});
}
m_provider = newVideoSurfaceProvider;
if (m_provider)
{
if (const auto current = m_provider->videoSurface())
current->setVideoSurfaceProvider(nullptr); // it is probably not a good idea to break the QML binding here
connect(this, &VideoSurface::mouseMoved, m_provider, &VideoSurfaceProvider::onMouseMoved);
connect(this, &VideoSurface::mousePressed, m_provider, &VideoSurfaceProvider::onMousePressed);
connect(this, &VideoSurface::mouseDblClicked, m_provider, &VideoSurfaceProvider::onMouseDoubleClick);
connect(this, &VideoSurface::mouseReleased, m_provider, &VideoSurfaceProvider::onMouseReleased);
connect(this, &VideoSurface::keyPressed, m_provider, &VideoSurfaceProvider::onKeyPressed);
connect(this, &VideoSurface::surfacePropertiesChanged, m_provider, &VideoSurfaceProvider::onSurfacePropertiesChanged, Qt::DirectConnection);
// With auto connection, this should be queued if the signal was emitted from vout thread,
// so that the slot is executed in item's thread:
connect(m_provider, &VideoSurfaceProvider::videoEnabledChanged, this, [this](bool enabled) {
if (enabled)
{
m_videoEnabledChanged = true;
if (flags().testFlag(ItemHasContents)) // "Only items which specify QQuickItem::ItemHasContents are allowed to call QQuickItem::update()."
update();
}
});
connect(&m_wheelEventConverter, &WheelToVLCConverter::vlcWheelKey, m_provider, &VideoSurfaceProvider::onMouseWheeled);
assert(!m_provider->videoSurface());
m_provider->setVideoSurface(this);
setFlag(ItemHasContents, true);
update(); // this should not be necessary right after setting `ItemHasContents`, but just in case
}
else
{
setFlag(ItemHasContents, false);
// Connection is made in `::updatePaintNode()` (which is called when both the item is visible and `ItemHasContents` is set).
disconnect(m_synchConnection);
}
emit videoSurfaceProviderChanged();
}
QSGNode *VideoSurface::updatePaintNode(QSGNode *node, UpdatePaintNodeData *data)
{
// This is called from the render thread, but during synchronization.
// So the GUI thread is blocked here. This makes it safer to access the window
// to get the effective DPR, rather than doing it outside the synchronization
// stage.
const auto w = window();
assert (w);
if (Q_UNLIKELY(!m_provider))
return node;
// WARNING: For some reason `::updatePaintNode()` is called initially when the item is invisible.
if (!m_synchConnection && isVisible())
{
// Disconnection is made in `::itemChange()`'s `ItemSceneChange` handler.
// This is constant:
if (m_provider->supportsThreadedSurfaceUpdates())
{
// Synchronize just before swapping the frame for better synchronization:
m_synchConnection = connect(w, &QQuickWindow::afterRendering, this, &VideoSurface::synchronize, Qt::DirectConnection);
}
else
{
m_synchConnection = connect(w, &QQuickWindow::afterAnimating, this, &VideoSurface::synchronize);
}
}
if (m_dprChanged)
{
m_dpr = w->effectiveDevicePixelRatio();
m_dprDirty = true;
m_dprChanged = false;
}
if (m_videoEnabledChanged)
{
m_allDirty = true;
m_videoEnabledChanged = false;
}
if (QThread::currentThread() == thread())
{
// As per `::synchronize()` we do not need `ViewBlockingRectangle` to provide the render position in this case:
setUpdateRenderPosition(false);
// Override `ViewBlockingRectangle`'s `ItemHasContents`, if it is unset (when both `renderEnabled` and
// `updateRenderPosition()` are false):
setFlag(ItemHasContents, true);
}
else
{
setUpdateRenderPosition(true);
}
return ViewBlockingRectangle::updatePaintNode(node, data);
}
¤ Dauer der Verarbeitung: 0.4 Sekunden
¤
*© Formatika GbR, Deutschland
|
|