File indexing completed on 2024-05-12 04:44:38

0001 /*
0002     Copyright (C) 2007-2008 Tanguy Krotoff <tkrotoff@gmail.com>
0003     Copyright (C) 2008 Lukas Durfina <lukas.durfina@gmail.com>
0004     Copyright (C) 2009 Fathi Boudra <fabo@kde.org>
0005     Copyright (C) 2009-2011 vlc-phonon AUTHORS <kde-multimedia@kde.org>
0006     Copyright (C) 2011-2021 Harald Sitter <sitter@kde.org>
0007 
0008     This library is free software; you can redistribute it and/or
0009     modify it under the terms of the GNU Lesser General Public
0010     License as published by the Free Software Foundation; either
0011     version 2.1 of the License, or (at your option) any later version.
0012 
0013     This library is distributed in the hope that it will be useful,
0014     but WITHOUT ANY WARRANTY; without even the implied warranty of
0015     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0016     Lesser General Public License for more details.
0017 
0018     You should have received a copy of the GNU Lesser General Public
0019     License along with this library.  If not, see <http://www.gnu.org/licenses/>.
0020 */
0021 
0022 #include "videowidget.h"
0023 
0024 #include <QGuiApplication>
0025 #include <QPainter>
0026 #include <QPaintEvent>
0027 
0028 #include <vlc/vlc.h>
0029 
0030 #include "utils/debug.h"
0031 #include "mediaobject.h"
0032 #include "media.h"
0033 
0034 #include "video/videomemorystream.h"
0035 
0036 namespace Phonon {
0037 namespace VLC {
0038 
0039 #define DEFAULT_QSIZE QSize(320, 240)
0040 
0041 class SurfacePainter : public VideoMemoryStream
0042 {
0043 public:
0044     void handlePaint(QPaintEvent *event)
0045     {
0046         // Mind that locking here is still faster than making this lockfree by
0047         // dispatching QEvents.
0048         // Plus VLC can actually skip frames as necessary.
0049         QMutexLocker lock(&m_mutex);
0050         Q_UNUSED(event);
0051 
0052         if (m_frame.isNull()) {
0053             return;
0054         }
0055 
0056         QPainter painter(widget);
0057         // When using OpenGL for the QPaintEngine drawing the same QImage twice
0058         // does not actually result in a texture change for one reason or another.
0059         // So we simply create new images for every event. This is plenty cheap
0060         // as the QImage only points to the plane data (it can't even make it
0061         // properly shared as it does not know that the data belongs to a QBA).
0062         // TODO: investigate if this is still necessary. This was added for gwenview, but with Qt 5.15 the problem
0063         //   can't be produced.
0064         painter.drawImage(drawFrameRect(), QImage(m_frame));
0065         event->accept();
0066     }
0067 
0068     VideoWidget *widget;
0069 
0070 private:
0071     void *lockCallback(void **planes) override
0072     {
0073         m_mutex.lock();
0074         planes[0] = (void *) m_frame.bits();
0075         return 0;
0076     }
0077 
0078     void unlockCallback(void *picture,void *const *planes) override
0079     {
0080         Q_UNUSED(picture);
0081         Q_UNUSED(planes);
0082         m_mutex.unlock();
0083     }
0084 
0085     void displayCallback(void *picture) override
0086     {
0087         Q_UNUSED(picture);
0088         if (widget)
0089             widget->update();
0090     }
0091 
0092     unsigned formatCallback(char *chroma,
0093                                     unsigned *width, unsigned *height,
0094                                     unsigned *pitches,
0095                                     unsigned *lines) override
0096     {
0097         QMutexLocker lock(&m_mutex);
0098         // Surface rendering is a fallback system used when no efficient rendering implementation is available.
0099         // As such we only support RGB32 for simplicity reasons and this will almost always mean software scaling.
0100         // And since scaling is unavoidable anyway we take the canonical frame size and then scale it on our end via
0101         // QPainter, again, greater simplicity at likely no real extra cost since this is all super inefficient anyway.
0102         // Also, since aspect ratio can be change mid-playback by the user, doing the scaling on our end means we
0103         // don't need to restart the entire player to retrigger format calculation.
0104         // With all that in mind we simply use the canonical size and feed VLC the QImage's pitch and lines as
0105         // effectively the VLC vout is the QImage so its constraints matter.
0106 
0107         // per https://wiki.videolan.org/Hacker_Guide/Video_Filters/#Pitch.2C_visible_pitch.2C_planes_et_al.
0108         // it would seem that we can use either real or visible pitches and lines as VLC generally will iterate the
0109         // smallest value when moving data between two entities. i.e. since QImage will at most paint NxM anyway,
0110         // we may just go with its values as calculating the real pitch/line of the VLC picture_t for RV32 wouldn't
0111         // change the maximum pitch/lines we can paint on the output side.
0112 
0113         qstrcpy(chroma, "RV32");
0114         m_frame = QImage(*width, *height, QImage::Format_RGB32);
0115         Q_ASSERT(!m_frame.isNull()); // ctor may construct null if allocation fails
0116         m_frame.fill(0);
0117         pitches[0] = m_frame.bytesPerLine();
0118         lines[0] = m_frame.sizeInBytes() / m_frame.bytesPerLine();
0119 
0120         return  m_frame.sizeInBytes();
0121     }
0122 
0123     void formatCleanUpCallback() override
0124     {
0125         // Lazy delete the object to avoid callbacks from VLC after deletion.
0126         if (!widget) {
0127             // The widget member is set to null by the widget destructor, so when this condition is true the
0128             // widget had already been destroyed and we can't possibly receive a paint event anymore, meaning
0129             // we need no lock here. If it were any other way we'd have trouble with synchronizing deletion
0130             // without deleting a locked mutex.
0131             delete this;
0132         }
0133     }
0134 
0135     QRect scaleToAspect(QRect srcRect, int w, int h) const
0136     {
0137         float width = srcRect.width();
0138         float height = srcRect.width() * (float(h) / float(w));
0139         if (height > srcRect.height()) {
0140             height = srcRect.height();
0141             width = srcRect.height() * (float(w) / float(h));
0142         }
0143         return QRect(0, 0, (int)width, (int)height);
0144     }
0145 
0146     QRect drawFrameRect() const
0147     {
0148         QRect widgetRect = widget->rect();
0149         QRect drawFrameRect;
0150         switch (widget->aspectRatio()) {
0151         case Phonon::VideoWidget::AspectRatioWidget:
0152             drawFrameRect = widgetRect;
0153             // No more calculations needed.
0154             return drawFrameRect;
0155         case Phonon::VideoWidget::AspectRatio4_3:
0156             drawFrameRect = scaleToAspect(widgetRect, 4, 3);
0157             break;
0158         case Phonon::VideoWidget::AspectRatio16_9:
0159             drawFrameRect = scaleToAspect(widgetRect, 16, 9);
0160             break;
0161         case Phonon::VideoWidget::AspectRatioAuto:
0162             drawFrameRect = QRect(0, 0, m_frame.width(), m_frame.height());
0163             break;
0164         }
0165 
0166         // Scale m_drawFrameRect to fill the widget
0167         // without breaking aspect:
0168         float widgetWidth = widgetRect.width();
0169         float widgetHeight = widgetRect.height();
0170         float frameWidth = widgetWidth;
0171         float frameHeight = drawFrameRect.height() * float(widgetWidth) / float(drawFrameRect.width());
0172 
0173         switch (widget->scaleMode()) {
0174         case Phonon::VideoWidget::ScaleAndCrop:
0175             if (frameHeight < widgetHeight) {
0176                 frameWidth *= float(widgetHeight) / float(frameHeight);
0177                 frameHeight = widgetHeight;
0178             }
0179             break;
0180         case Phonon::VideoWidget::FitInView:
0181             if (frameHeight > widgetHeight) {
0182                 frameWidth *= float(widgetHeight) / float(frameHeight);
0183                 frameHeight = widgetHeight;
0184             }
0185             break;
0186         }
0187         drawFrameRect.setSize(QSize(int(frameWidth), int(frameHeight)));
0188         drawFrameRect.moveTo(int((widgetWidth - frameWidth) / 2.0f),
0189                                int((widgetHeight - frameHeight) / 2.0f));
0190         return drawFrameRect;
0191     }
0192 
0193     // Could ReadWriteLock two frames so VLC can write while we paint.
0194     QImage m_frame;
0195     QMutex m_mutex;
0196 };
0197 
0198 VideoWidget::VideoWidget(QWidget *parent) :
0199     BaseWidget(parent),
0200     SinkNode(),
0201     m_videoSize(DEFAULT_QSIZE),
0202     m_aspectRatio(Phonon::VideoWidget::AspectRatioAuto),
0203     m_scaleMode(Phonon::VideoWidget::FitInView),
0204     m_filterAdjustActivated(false),
0205     m_brightness(0.0),
0206     m_contrast(0.0),
0207     m_hue(0.0),
0208     m_saturation(0.0),
0209     m_surfacePainter(0)
0210 {
0211     // We want background painting so Qt autofills with black.
0212     setAttribute(Qt::WA_NoSystemBackground, false);
0213 
0214     // Required for dvdnav
0215 #ifdef __GNUC__
0216 #warning dragonplayer munches on our mouse events, so clicking in a DVD menu does not work - vlc 1.2 where are thu?
0217 #endif // __GNUC__
0218     setMouseTracking(true);
0219 
0220     // setBackgroundColor
0221     QPalette p = palette();
0222     p.setColor(backgroundRole(), Qt::black);
0223     setPalette(p);
0224     setAutoFillBackground(true);
0225 }
0226 
0227 VideoWidget::~VideoWidget()
0228 {
0229     if (m_surfacePainter)
0230         m_surfacePainter->widget = 0; // Lazy delete
0231 }
0232 
0233 void VideoWidget::handleConnectToMediaObject(MediaObject *mediaObject)
0234 {
0235     connect(mediaObject, SIGNAL(hasVideoChanged(bool)),
0236             SLOT(updateVideoSize(bool)));
0237     connect(mediaObject, SIGNAL(hasVideoChanged(bool)),
0238             SLOT(processPendingAdjusts(bool)));
0239     connect(mediaObject, SIGNAL(currentSourceChanged(MediaSource)),
0240             SLOT(clearPendingAdjusts()));
0241 
0242     clearPendingAdjusts();
0243 }
0244 
0245 void VideoWidget::handleDisconnectFromMediaObject(MediaObject *mediaObject)
0246 {
0247     // Undo all connections or path creation->destruction->creation can cause
0248     // duplicated connections or getting signals from two different MediaObjects.
0249     disconnect(mediaObject, 0, this, 0);
0250 }
0251 
0252 void VideoWidget::handleAddToMedia(Media *media)
0253 {
0254     media->addOption(":video");
0255 
0256     if (!m_surfacePainter) {
0257 #if defined(Q_OS_MAC)
0258         m_player->setNsObject(cocoaView());
0259 #elif defined(Q_OS_UNIX)
0260         if (QGuiApplication::platformName().contains(QStringLiteral("xcb"), Qt::CaseInsensitive)) {
0261             m_player->setXWindow(winId());
0262         } else {
0263             enableSurfacePainter();
0264         }
0265 #elif defined(Q_OS_WIN)
0266         m_player->setHwnd((HWND)winId());
0267 #endif
0268     }
0269 }
0270 
0271 Phonon::VideoWidget::AspectRatio VideoWidget::aspectRatio() const
0272 {
0273     return m_aspectRatio;
0274 }
0275 
0276 void VideoWidget::setAspectRatio(Phonon::VideoWidget::AspectRatio aspect)
0277 {
0278     DEBUG_BLOCK;
0279     if (!m_player)
0280         return;
0281 
0282     m_aspectRatio = aspect;
0283 
0284     switch (m_aspectRatio) {
0285     // FIXME: find a way to implement aspectratiowidget, it is meant to scale
0286     // and stretch (i.e. scale to window without retaining aspect ratio).
0287     case Phonon::VideoWidget::AspectRatioAuto:
0288         m_player->setVideoAspectRatio(QByteArray());
0289         return;
0290     case Phonon::VideoWidget::AspectRatio4_3:
0291         m_player->setVideoAspectRatio("4:3");
0292         return;
0293     case Phonon::VideoWidget::AspectRatio16_9:
0294         m_player->setVideoAspectRatio("16:9");
0295         return;
0296     }
0297     warning() << "The aspect ratio" << aspect << "is not supported by Phonon VLC.";
0298 }
0299 
0300 Phonon::VideoWidget::ScaleMode VideoWidget::scaleMode() const
0301 {
0302     return m_scaleMode;
0303 }
0304 
0305 void VideoWidget::setScaleMode(Phonon::VideoWidget::ScaleMode scale)
0306 {
0307 #ifdef __GNUC__
0308 #warning OMG WTF
0309 #endif
0310     m_scaleMode = scale;
0311     switch (m_scaleMode) {
0312     }
0313     warning() << "The scale mode" << scale << "is not supported by Phonon VLC.";
0314 }
0315 
0316 qreal VideoWidget::brightness() const
0317 {
0318     return m_brightness;
0319 }
0320 
0321 void VideoWidget::setBrightness(qreal brightness)
0322 {
0323     DEBUG_BLOCK;
0324     if (!m_player) {
0325         return;
0326     }
0327     if (!enableFilterAdjust()) {
0328         // Add to pending adjusts
0329         m_pendingAdjusts.insert(QByteArray("setBrightness"), brightness);
0330         return;
0331     }
0332 
0333     // VLC operates within a 0.0 to 2.0 range for brightness.
0334     m_brightness = brightness;
0335     m_player->setVideoAdjust(libvlc_adjust_Brightness,
0336                              phononRangeToVlcRange(m_brightness, 2.0));
0337 }
0338 
0339 qreal VideoWidget::contrast() const
0340 {
0341     return m_contrast;
0342 }
0343 
0344 void VideoWidget::setContrast(qreal contrast)
0345 {
0346     DEBUG_BLOCK;
0347     if (!m_player) {
0348         return;
0349     }
0350     if (!enableFilterAdjust()) {
0351         // Add to pending adjusts
0352         m_pendingAdjusts.insert(QByteArray("setContrast"), contrast);
0353         return;
0354     }
0355 
0356     // VLC operates within a 0.0 to 2.0 range for contrast.
0357     m_contrast = contrast;
0358     m_player->setVideoAdjust(libvlc_adjust_Contrast, phononRangeToVlcRange(m_contrast, 2.0));
0359 }
0360 
0361 qreal VideoWidget::hue() const
0362 {
0363     return m_hue;
0364 }
0365 
0366 void VideoWidget::setHue(qreal hue)
0367 {
0368     DEBUG_BLOCK;
0369     if (!m_player) {
0370         return;
0371     }
0372     if (!enableFilterAdjust()) {
0373         // Add to pending adjusts
0374         m_pendingAdjusts.insert(QByteArray("setHue"), hue);
0375         return;
0376     }
0377 
0378     // VLC operates within a 0 to 360 range for hue.
0379     // Phonon operates on -1.0 to 1.0, so we need to consider 0 to 180 as
0380     // 0 to 1.0 and 180 to 360 as -1 to 0.0.
0381     //              360/0 (0)
0382     //                 ___
0383     //                /   \
0384     //    270 (-.25)  |   |  90 (.25)
0385     //                \___/
0386     //             180 (1/-1)
0387     // (-.25 is 360 minus 90 (vlcValue of .25).
0388     m_hue = hue;
0389     const int vlcValue = static_cast<int>(phononRangeToVlcRange(qAbs(hue), 180.0, false));
0390     int value = 0;
0391     if (hue >= 0)
0392         value = vlcValue;
0393     else
0394         value = 360.0 - vlcValue;
0395     m_player->setVideoAdjust(libvlc_adjust_Hue, value);
0396 }
0397 
0398 qreal VideoWidget::saturation() const
0399 {
0400     return m_saturation;
0401 }
0402 
0403 void VideoWidget::setSaturation(qreal saturation)
0404 {
0405     DEBUG_BLOCK;
0406     if (!m_player) {
0407         return;
0408     }
0409     if (!enableFilterAdjust()) {
0410         // Add to pending adjusts
0411         m_pendingAdjusts.insert(QByteArray("setSaturation"), saturation);
0412         return;
0413     }
0414 
0415     // VLC operates within a 0.0 to 3.0 range for saturation.
0416     m_saturation = saturation;
0417     m_player->setVideoAdjust(libvlc_adjust_Saturation,
0418                               phononRangeToVlcRange(m_saturation, 3.0));
0419 }
0420 
0421 QWidget *VideoWidget::widget()
0422 {
0423     return this;
0424 }
0425 
0426 QSize VideoWidget::sizeHint() const
0427 {
0428     return m_videoSize;
0429 }
0430 
0431 void VideoWidget::updateVideoSize(bool hasVideo)
0432 {
0433     if (hasVideo) {
0434         m_videoSize = m_player->videoSize();
0435         updateGeometry();
0436         update();
0437     } else
0438         m_videoSize = DEFAULT_QSIZE;
0439 }
0440 
0441 void VideoWidget::setVisible(bool visible)
0442 {
0443     if (window() && window()->testAttribute(Qt::WA_DontShowOnScreen) && !m_surfacePainter) {
0444         enableSurfacePainter();
0445     }
0446     QWidget::setVisible(visible);
0447 }
0448 
0449 void VideoWidget::processPendingAdjusts(bool videoAvailable)
0450 {
0451     if (!videoAvailable || !m_mediaObject || !m_mediaObject->hasVideo()) {
0452         return;
0453     }
0454 
0455     QHashIterator<QByteArray, qreal> it(m_pendingAdjusts);
0456     while (it.hasNext()) {
0457         it.next();
0458         QMetaObject::invokeMethod(this, it.key().constData(), Q_ARG(qreal, it.value()));
0459     }
0460     m_pendingAdjusts.clear();
0461 }
0462 
0463 void VideoWidget::clearPendingAdjusts()
0464 {
0465     m_pendingAdjusts.clear();
0466 }
0467 
0468 void VideoWidget::paintEvent(QPaintEvent *event)
0469 {
0470     Q_UNUSED(event);
0471     if (m_surfacePainter)
0472         m_surfacePainter->handlePaint(event);
0473 }
0474 
0475 bool VideoWidget::enableFilterAdjust(bool adjust)
0476 {
0477     DEBUG_BLOCK;
0478     // Need to check for MO here, because we can get called before a VOut is actually
0479     // around in which case we just ignore this.
0480     if (!m_mediaObject || !m_mediaObject->hasVideo()) {
0481         debug() << "no mo or no video!!!";
0482         return false;
0483     }
0484     if ((!m_filterAdjustActivated && adjust) ||
0485             (m_filterAdjustActivated && !adjust)) {
0486         debug() << "adjust: " << adjust;
0487         m_player->setVideoAdjust(libvlc_adjust_Enable, static_cast<int>(adjust));
0488         m_filterAdjustActivated = adjust;
0489     }
0490     return true;
0491 }
0492 
0493 float VideoWidget::phononRangeToVlcRange(qreal phononValue, float upperBoundary,
0494                                          bool shift)
0495 {
0496     // VLC operates on different ranges than Phonon. Phonon always uses a range of
0497     // -1:1 with 0 as the default value.
0498     // It is therefore necessary to convert between the two schemes using sophisticated magic.
0499     // First the incoming range is locked between -1..1, then depending on shift
0500     // either normalized to 0..2 or 0..1 and finally a new value is calculated
0501     // depending on the upperBoundary and the normalized range.
0502     float value = static_cast<float>(phononValue);
0503     float range = 2.0; // The default normalized range will be 0..2 = 2
0504 
0505     // Ensure valid range
0506     if (value < -1.0)
0507         value = -1.0;
0508     else if (value > 1.0)
0509         value = 1.0;
0510 
0511     if (shift)
0512         value += 1.0; // Shift into 0..2 range
0513     else {
0514         // Chop negative value; normalize to 0..1 = range 1
0515         if (value < 0.0)
0516             value = 0.0;
0517         range = 1.0;
0518     }
0519 
0520     return (value * (upperBoundary/range));
0521 }
0522 
0523 QImage VideoWidget::snapshot() const
0524 {
0525     DEBUG_BLOCK;
0526     if (m_player)
0527         return m_player->snapshot();
0528     else
0529         return QImage();
0530 }
0531 
0532 void VideoWidget::enableSurfacePainter()
0533 {
0534     if (m_surfacePainter) {
0535         return;
0536     }
0537 
0538     debug() << "ENABLING SURFACE PAINTING";
0539     m_surfacePainter = new SurfacePainter;
0540     m_surfacePainter->widget = this;
0541     m_surfacePainter->setCallbacks(m_player);
0542 }
0543 
0544 } // namespace VLC
0545 } // namespace Phonon