/* Copyright (C) 2007-2008 Tanguy Krotoff Copyright (C) 2008 Lukas Durfina Copyright (C) 2009 Fathi Boudra Copyright (C) 2009-2011 vlc-phonon AUTHORS Copyright (C) 2011-2021 Harald Sitter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "videowidget.h" #include #include #include #include #include "utils/debug.h" #include "mediaobject.h" #include "media.h" #include "video/videomemorystream.h" namespace Phonon { namespace VLC { #define DEFAULT_QSIZE QSize(320, 240) class SurfacePainter : public VideoMemoryStream { public: void handlePaint(QPaintEvent *event) { // Mind that locking here is still faster than making this lockfree by // dispatching QEvents. // Plus VLC can actually skip frames as necessary. QMutexLocker lock(&m_mutex); Q_UNUSED(event); if (m_frame.isNull()) { return; } QPainter painter(widget); // When using OpenGL for the QPaintEngine drawing the same QImage twice // does not actually result in a texture change for one reason or another. // So we simply create new images for every event. This is plenty cheap // as the QImage only points to the plane data (it can't even make it // properly shared as it does not know that the data belongs to a QBA). // TODO: investigate if this is still necessary. This was added for gwenview, but with Qt 5.15 the problem // can't be produced. painter.drawImage(drawFrameRect(), QImage(m_frame)); event->accept(); } VideoWidget *widget; private: void *lockCallback(void **planes) override { m_mutex.lock(); planes[0] = (void *) m_frame.bits(); return 0; } void unlockCallback(void *picture,void *const *planes) override { Q_UNUSED(picture); Q_UNUSED(planes); m_mutex.unlock(); } void displayCallback(void *picture) override { Q_UNUSED(picture); if (widget) widget->update(); } unsigned formatCallback(char *chroma, unsigned *width, unsigned *height, unsigned *pitches, unsigned *lines) override { QMutexLocker lock(&m_mutex); // Surface rendering is a fallback system used when no efficient rendering implementation is available. // As such we only support RGB32 for simplicity reasons and this will almost always mean software scaling. // And since scaling is unavoidable anyway we take the canonical frame size and then scale it on our end via // QPainter, again, greater simplicity at likely no real extra cost since this is all super inefficient anyway. // Also, since aspect ratio can be change mid-playback by the user, doing the scaling on our end means we // don't need to restart the entire player to retrigger format calculation. // With all that in mind we simply use the canonical size and feed VLC the QImage's pitch and lines as // effectively the VLC vout is the QImage so its constraints matter. // per https://wiki.videolan.org/Hacker_Guide/Video_Filters/#Pitch.2C_visible_pitch.2C_planes_et_al. // it would seem that we can use either real or visible pitches and lines as VLC generally will iterate the // smallest value when moving data between two entities. i.e. since QImage will at most paint NxM anyway, // we may just go with its values as calculating the real pitch/line of the VLC picture_t for RV32 wouldn't // change the maximum pitch/lines we can paint on the output side. qstrcpy(chroma, "RV32"); m_frame = QImage(*width, *height, QImage::Format_RGB32); Q_ASSERT(!m_frame.isNull()); // ctor may construct null if allocation fails m_frame.fill(0); pitches[0] = m_frame.bytesPerLine(); lines[0] = m_frame.sizeInBytes() / m_frame.bytesPerLine(); return m_frame.sizeInBytes(); } void formatCleanUpCallback() override { // Lazy delete the object to avoid callbacks from VLC after deletion. if (!widget) { // The widget member is set to null by the widget destructor, so when this condition is true the // widget had already been destroyed and we can't possibly receive a paint event anymore, meaning // we need no lock here. If it were any other way we'd have trouble with synchronizing deletion // without deleting a locked mutex. delete this; } } QRect scaleToAspect(QRect srcRect, int w, int h) const { float width = srcRect.width(); float height = srcRect.width() * (float(h) / float(w)); if (height > srcRect.height()) { height = srcRect.height(); width = srcRect.height() * (float(w) / float(h)); } return QRect(0, 0, (int)width, (int)height); } QRect drawFrameRect() const { QRect widgetRect = widget->rect(); QRect drawFrameRect; switch (widget->aspectRatio()) { case Phonon::VideoWidget::AspectRatioWidget: drawFrameRect = widgetRect; // No more calculations needed. return drawFrameRect; case Phonon::VideoWidget::AspectRatio4_3: drawFrameRect = scaleToAspect(widgetRect, 4, 3); break; case Phonon::VideoWidget::AspectRatio16_9: drawFrameRect = scaleToAspect(widgetRect, 16, 9); break; case Phonon::VideoWidget::AspectRatioAuto: drawFrameRect = QRect(0, 0, m_frame.width(), m_frame.height()); break; } // Scale m_drawFrameRect to fill the widget // without breaking aspect: float widgetWidth = widgetRect.width(); float widgetHeight = widgetRect.height(); float frameWidth = widgetWidth; float frameHeight = drawFrameRect.height() * float(widgetWidth) / float(drawFrameRect.width()); switch (widget->scaleMode()) { case Phonon::VideoWidget::ScaleAndCrop: if (frameHeight < widgetHeight) { frameWidth *= float(widgetHeight) / float(frameHeight); frameHeight = widgetHeight; } break; case Phonon::VideoWidget::FitInView: if (frameHeight > widgetHeight) { frameWidth *= float(widgetHeight) / float(frameHeight); frameHeight = widgetHeight; } break; } drawFrameRect.setSize(QSize(int(frameWidth), int(frameHeight))); drawFrameRect.moveTo(int((widgetWidth - frameWidth) / 2.0f), int((widgetHeight - frameHeight) / 2.0f)); return drawFrameRect; } // Could ReadWriteLock two frames so VLC can write while we paint. QImage m_frame; QMutex m_mutex; }; VideoWidget::VideoWidget(QWidget *parent) : BaseWidget(parent), SinkNode(), m_videoSize(DEFAULT_QSIZE), m_aspectRatio(Phonon::VideoWidget::AspectRatioAuto), m_scaleMode(Phonon::VideoWidget::FitInView), m_filterAdjustActivated(false), m_brightness(0.0), m_contrast(0.0), m_hue(0.0), m_saturation(0.0), m_surfacePainter(0) { // We want background painting so Qt autofills with black. setAttribute(Qt::WA_NoSystemBackground, false); // Required for dvdnav #ifdef __GNUC__ #warning dragonplayer munches on our mouse events, so clicking in a DVD menu does not work - vlc 1.2 where are thu? #endif // __GNUC__ setMouseTracking(true); // setBackgroundColor QPalette p = palette(); p.setColor(backgroundRole(), Qt::black); setPalette(p); setAutoFillBackground(true); } VideoWidget::~VideoWidget() { if (m_surfacePainter) m_surfacePainter->widget = 0; // Lazy delete } void VideoWidget::handleConnectToMediaObject(MediaObject *mediaObject) { connect(mediaObject, SIGNAL(hasVideoChanged(bool)), SLOT(updateVideoSize(bool))); connect(mediaObject, SIGNAL(hasVideoChanged(bool)), SLOT(processPendingAdjusts(bool))); connect(mediaObject, SIGNAL(currentSourceChanged(MediaSource)), SLOT(clearPendingAdjusts())); clearPendingAdjusts(); } void VideoWidget::handleDisconnectFromMediaObject(MediaObject *mediaObject) { // Undo all connections or path creation->destruction->creation can cause // duplicated connections or getting signals from two different MediaObjects. disconnect(mediaObject, 0, this, 0); } void VideoWidget::handleAddToMedia(Media *media) { media->addOption(":video"); if (!m_surfacePainter) { #if defined(Q_OS_MAC) m_player->setNsObject(cocoaView()); #elif defined(Q_OS_UNIX) if (QGuiApplication::platformName().contains(QStringLiteral("xcb"), Qt::CaseInsensitive)) { m_player->setXWindow(winId()); } else { enableSurfacePainter(); } #elif defined(Q_OS_WIN) m_player->setHwnd((HWND)winId()); #endif } } Phonon::VideoWidget::AspectRatio VideoWidget::aspectRatio() const { return m_aspectRatio; } void VideoWidget::setAspectRatio(Phonon::VideoWidget::AspectRatio aspect) { DEBUG_BLOCK; if (!m_player) return; m_aspectRatio = aspect; switch (m_aspectRatio) { // FIXME: find a way to implement aspectratiowidget, it is meant to scale // and stretch (i.e. scale to window without retaining aspect ratio). case Phonon::VideoWidget::AspectRatioAuto: m_player->setVideoAspectRatio(QByteArray()); return; case Phonon::VideoWidget::AspectRatio4_3: m_player->setVideoAspectRatio("4:3"); return; case Phonon::VideoWidget::AspectRatio16_9: m_player->setVideoAspectRatio("16:9"); return; } warning() << "The aspect ratio" << aspect << "is not supported by Phonon VLC."; } Phonon::VideoWidget::ScaleMode VideoWidget::scaleMode() const { return m_scaleMode; } void VideoWidget::setScaleMode(Phonon::VideoWidget::ScaleMode scale) { #ifdef __GNUC__ #warning OMG WTF #endif m_scaleMode = scale; switch (m_scaleMode) { } warning() << "The scale mode" << scale << "is not supported by Phonon VLC."; } qreal VideoWidget::brightness() const { return m_brightness; } void VideoWidget::setBrightness(qreal brightness) { DEBUG_BLOCK; if (!m_player) { return; } if (!enableFilterAdjust()) { // Add to pending adjusts m_pendingAdjusts.insert(QByteArray("setBrightness"), brightness); return; } // VLC operates within a 0.0 to 2.0 range for brightness. m_brightness = brightness; m_player->setVideoAdjust(libvlc_adjust_Brightness, phononRangeToVlcRange(m_brightness, 2.0)); } qreal VideoWidget::contrast() const { return m_contrast; } void VideoWidget::setContrast(qreal contrast) { DEBUG_BLOCK; if (!m_player) { return; } if (!enableFilterAdjust()) { // Add to pending adjusts m_pendingAdjusts.insert(QByteArray("setContrast"), contrast); return; } // VLC operates within a 0.0 to 2.0 range for contrast. m_contrast = contrast; m_player->setVideoAdjust(libvlc_adjust_Contrast, phononRangeToVlcRange(m_contrast, 2.0)); } qreal VideoWidget::hue() const { return m_hue; } void VideoWidget::setHue(qreal hue) { DEBUG_BLOCK; if (!m_player) { return; } if (!enableFilterAdjust()) { // Add to pending adjusts m_pendingAdjusts.insert(QByteArray("setHue"), hue); return; } // VLC operates within a 0 to 360 range for hue. // Phonon operates on -1.0 to 1.0, so we need to consider 0 to 180 as // 0 to 1.0 and 180 to 360 as -1 to 0.0. // 360/0 (0) // ___ // / \ // 270 (-.25) | | 90 (.25) // \___/ // 180 (1/-1) // (-.25 is 360 minus 90 (vlcValue of .25). m_hue = hue; const int vlcValue = static_cast(phononRangeToVlcRange(qAbs(hue), 180.0, false)); int value = 0; if (hue >= 0) value = vlcValue; else value = 360.0 - vlcValue; m_player->setVideoAdjust(libvlc_adjust_Hue, value); } qreal VideoWidget::saturation() const { return m_saturation; } void VideoWidget::setSaturation(qreal saturation) { DEBUG_BLOCK; if (!m_player) { return; } if (!enableFilterAdjust()) { // Add to pending adjusts m_pendingAdjusts.insert(QByteArray("setSaturation"), saturation); return; } // VLC operates within a 0.0 to 3.0 range for saturation. m_saturation = saturation; m_player->setVideoAdjust(libvlc_adjust_Saturation, phononRangeToVlcRange(m_saturation, 3.0)); } QWidget *VideoWidget::widget() { return this; } QSize VideoWidget::sizeHint() const { return m_videoSize; } void VideoWidget::updateVideoSize(bool hasVideo) { if (hasVideo) { m_videoSize = m_player->videoSize(); updateGeometry(); update(); } else m_videoSize = DEFAULT_QSIZE; } void VideoWidget::setVisible(bool visible) { if (window() && window()->testAttribute(Qt::WA_DontShowOnScreen) && !m_surfacePainter) { enableSurfacePainter(); } QWidget::setVisible(visible); } void VideoWidget::processPendingAdjusts(bool videoAvailable) { if (!videoAvailable || !m_mediaObject || !m_mediaObject->hasVideo()) { return; } QHashIterator it(m_pendingAdjusts); while (it.hasNext()) { it.next(); QMetaObject::invokeMethod(this, it.key().constData(), Q_ARG(qreal, it.value())); } m_pendingAdjusts.clear(); } void VideoWidget::clearPendingAdjusts() { m_pendingAdjusts.clear(); } void VideoWidget::paintEvent(QPaintEvent *event) { Q_UNUSED(event); if (m_surfacePainter) m_surfacePainter->handlePaint(event); } bool VideoWidget::enableFilterAdjust(bool adjust) { DEBUG_BLOCK; // Need to check for MO here, because we can get called before a VOut is actually // around in which case we just ignore this. if (!m_mediaObject || !m_mediaObject->hasVideo()) { debug() << "no mo or no video!!!"; return false; } if ((!m_filterAdjustActivated && adjust) || (m_filterAdjustActivated && !adjust)) { debug() << "adjust: " << adjust; m_player->setVideoAdjust(libvlc_adjust_Enable, static_cast(adjust)); m_filterAdjustActivated = adjust; } return true; } float VideoWidget::phononRangeToVlcRange(qreal phononValue, float upperBoundary, bool shift) { // VLC operates on different ranges than Phonon. Phonon always uses a range of // -1:1 with 0 as the default value. // It is therefore necessary to convert between the two schemes using sophisticated magic. // First the incoming range is locked between -1..1, then depending on shift // either normalized to 0..2 or 0..1 and finally a new value is calculated // depending on the upperBoundary and the normalized range. float value = static_cast(phononValue); float range = 2.0; // The default normalized range will be 0..2 = 2 // Ensure valid range if (value < -1.0) value = -1.0; else if (value > 1.0) value = 1.0; if (shift) value += 1.0; // Shift into 0..2 range else { // Chop negative value; normalize to 0..1 = range 1 if (value < 0.0) value = 0.0; range = 1.0; } return (value * (upperBoundary/range)); } QImage VideoWidget::snapshot() const { DEBUG_BLOCK; if (m_player) return m_player->snapshot(); else return QImage(); } void VideoWidget::enableSurfacePainter() { if (m_surfacePainter) { return; } debug() << "ENABLING SURFACE PAINTING"; m_surfacePainter = new SurfacePainter; m_surfacePainter->widget = this; m_surfacePainter->setCallbacks(m_player); } } // namespace VLC } // namespace Phonon