1/*
2 Copyright (C) 2007-2008 Tanguy Krotoff <tkrotoff@gmail.com>
3 Copyright (C) 2008 Lukas Durfina <lukas.durfina@gmail.com>
4 Copyright (C) 2009 Fathi Boudra <fabo@kde.org>
5 Copyright (C) 2009-2011 vlc-phonon AUTHORS <kde-multimedia@kde.org>
6 Copyright (C) 2011-2021 Harald Sitter <sitter@kde.org>
7
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Lesser General Public
10 License as published by the Free Software Foundation; either
11 version 2.1 of the License, or (at your option) any later version.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public
19 License along with this library. If not, see <http://www.gnu.org/licenses/>.
20*/
21
22#include "videowidget.h"
23
24#include <QGuiApplication>
25#include <QPainter>
26#include <QPaintEvent>
27
28#include <vlc/vlc.h>
29
30#include "utils/debug.h"
31#include "mediaobject.h"
32#include "media.h"
33
34#include "video/videomemorystream.h"
35
36namespace Phonon {
37namespace VLC {
38
39#define DEFAULT_QSIZE QSize(320, 240)
40
41class SurfacePainter : public VideoMemoryStream
42{
43public:
44 void handlePaint(QPaintEvent *event)
45 {
46 // Mind that locking here is still faster than making this lockfree by
47 // dispatching QEvents.
48 // Plus VLC can actually skip frames as necessary.
49 QMutexLocker lock(&m_mutex);
50 Q_UNUSED(event);
51
52 if (m_frame.isNull()) {
53 return;
54 }
55
56 QPainter painter(widget);
57 // When using OpenGL for the QPaintEngine drawing the same QImage twice
58 // does not actually result in a texture change for one reason or another.
59 // So we simply create new images for every event. This is plenty cheap
60 // as the QImage only points to the plane data (it can't even make it
61 // properly shared as it does not know that the data belongs to a QBA).
62 // TODO: investigate if this is still necessary. This was added for gwenview, but with Qt 5.15 the problem
63 // can't be produced.
64 painter.drawImage(r: drawFrameRect(), image: QImage(m_frame));
65 event->accept();
66 }
67
68 VideoWidget *widget;
69
70private:
71 void *lockCallback(void **planes) override
72 {
73 m_mutex.lock();
74 planes[0] = (void *) m_frame.bits();
75 return 0;
76 }
77
78 void unlockCallback(void *picture,void *const *planes) override
79 {
80 Q_UNUSED(picture);
81 Q_UNUSED(planes);
82 m_mutex.unlock();
83 }
84
85 void displayCallback(void *picture) override
86 {
87 Q_UNUSED(picture);
88 if (widget)
89 widget->update();
90 }
91
92 unsigned formatCallback(char *chroma,
93 unsigned *width, unsigned *height,
94 unsigned *pitches,
95 unsigned *lines) override
96 {
97 QMutexLocker lock(&m_mutex);
98 // Surface rendering is a fallback system used when no efficient rendering implementation is available.
99 // As such we only support RGB32 for simplicity reasons and this will almost always mean software scaling.
100 // And since scaling is unavoidable anyway we take the canonical frame size and then scale it on our end via
101 // QPainter, again, greater simplicity at likely no real extra cost since this is all super inefficient anyway.
102 // Also, since aspect ratio can be change mid-playback by the user, doing the scaling on our end means we
103 // don't need to restart the entire player to retrigger format calculation.
104 // With all that in mind we simply use the canonical size and feed VLC the QImage's pitch and lines as
105 // effectively the VLC vout is the QImage so its constraints matter.
106
107 // per https://wiki.videolan.org/Hacker_Guide/Video_Filters/#Pitch.2C_visible_pitch.2C_planes_et_al.
108 // it would seem that we can use either real or visible pitches and lines as VLC generally will iterate the
109 // smallest value when moving data between two entities. i.e. since QImage will at most paint NxM anyway,
110 // we may just go with its values as calculating the real pitch/line of the VLC picture_t for RV32 wouldn't
111 // change the maximum pitch/lines we can paint on the output side.
112
113 qstrcpy(dst: chroma, src: "RV32");
114 m_frame = QImage(*width, *height, QImage::Format_RGB32);
115 Q_ASSERT(!m_frame.isNull()); // ctor may construct null if allocation fails
116 m_frame.fill(pixel: 0);
117 pitches[0] = m_frame.bytesPerLine();
118 lines[0] = m_frame.sizeInBytes() / m_frame.bytesPerLine();
119
120 return m_frame.sizeInBytes();
121 }
122
123 void formatCleanUpCallback() override
124 {
125 // Lazy delete the object to avoid callbacks from VLC after deletion.
126 if (!widget) {
127 // The widget member is set to null by the widget destructor, so when this condition is true the
128 // widget had already been destroyed and we can't possibly receive a paint event anymore, meaning
129 // we need no lock here. If it were any other way we'd have trouble with synchronizing deletion
130 // without deleting a locked mutex.
131 delete this;
132 }
133 }
134
135 QRect scaleToAspect(QRect srcRect, int w, int h) const
136 {
137 float width = srcRect.width();
138 float height = srcRect.width() * (float(h) / float(w));
139 if (height > srcRect.height()) {
140 height = srcRect.height();
141 width = srcRect.height() * (float(w) / float(h));
142 }
143 return QRect(0, 0, (int)width, (int)height);
144 }
145
146 QRect drawFrameRect() const
147 {
148 QRect widgetRect = widget->rect();
149 QRect drawFrameRect;
150 switch (widget->aspectRatio()) {
151 case Phonon::VideoWidget::AspectRatioWidget:
152 drawFrameRect = widgetRect;
153 // No more calculations needed.
154 return drawFrameRect;
155 case Phonon::VideoWidget::AspectRatio4_3:
156 drawFrameRect = scaleToAspect(srcRect: widgetRect, w: 4, h: 3);
157 break;
158 case Phonon::VideoWidget::AspectRatio16_9:
159 drawFrameRect = scaleToAspect(srcRect: widgetRect, w: 16, h: 9);
160 break;
161 case Phonon::VideoWidget::AspectRatioAuto:
162 drawFrameRect = QRect(0, 0, m_frame.width(), m_frame.height());
163 break;
164 }
165
166 // Scale m_drawFrameRect to fill the widget
167 // without breaking aspect:
168 float widgetWidth = widgetRect.width();
169 float widgetHeight = widgetRect.height();
170 float frameWidth = widgetWidth;
171 float frameHeight = drawFrameRect.height() * float(widgetWidth) / float(drawFrameRect.width());
172
173 switch (widget->scaleMode()) {
174 case Phonon::VideoWidget::ScaleAndCrop:
175 if (frameHeight < widgetHeight) {
176 frameWidth *= float(widgetHeight) / float(frameHeight);
177 frameHeight = widgetHeight;
178 }
179 break;
180 case Phonon::VideoWidget::FitInView:
181 if (frameHeight > widgetHeight) {
182 frameWidth *= float(widgetHeight) / float(frameHeight);
183 frameHeight = widgetHeight;
184 }
185 break;
186 }
187 drawFrameRect.setSize(QSize(int(frameWidth), int(frameHeight)));
188 drawFrameRect.moveTo(ax: int((widgetWidth - frameWidth) / 2.0f),
189 ay: int((widgetHeight - frameHeight) / 2.0f));
190 return drawFrameRect;
191 }
192
193 // Could ReadWriteLock two frames so VLC can write while we paint.
194 QImage m_frame;
195 QMutex m_mutex;
196};
197
198VideoWidget::VideoWidget(QWidget *parent) :
199 BaseWidget(parent),
200 SinkNode(),
201 m_videoSize(DEFAULT_QSIZE),
202 m_aspectRatio(Phonon::VideoWidget::AspectRatioAuto),
203 m_scaleMode(Phonon::VideoWidget::FitInView),
204 m_filterAdjustActivated(false),
205 m_brightness(0.0),
206 m_contrast(0.0),
207 m_hue(0.0),
208 m_saturation(0.0),
209 m_surfacePainter(0)
210{
211 // We want background painting so Qt autofills with black.
212 setAttribute(Qt::WA_NoSystemBackground, on: false);
213
214 // Required for dvdnav
215#ifdef __GNUC__
216#warning dragonplayer munches on our mouse events, so clicking in a DVD menu does not work - vlc 1.2 where are thu?
217#endif // __GNUC__
218 setMouseTracking(true);
219
220 // setBackgroundColor
221 QPalette p = palette();
222 p.setColor(acr: backgroundRole(), acolor: Qt::black);
223 setPalette(p);
224 setAutoFillBackground(true);
225}
226
227VideoWidget::~VideoWidget()
228{
229 if (m_surfacePainter)
230 m_surfacePainter->widget = 0; // Lazy delete
231}
232
233void VideoWidget::handleConnectToMediaObject(MediaObject *mediaObject)
234{
235 connect(asender: mediaObject, SIGNAL(hasVideoChanged(bool)),
236 SLOT(updateVideoSize(bool)));
237 connect(asender: mediaObject, SIGNAL(hasVideoChanged(bool)),
238 SLOT(processPendingAdjusts(bool)));
239 connect(asender: mediaObject, SIGNAL(currentSourceChanged(MediaSource)),
240 SLOT(clearPendingAdjusts()));
241
242 clearPendingAdjusts();
243}
244
245void VideoWidget::handleDisconnectFromMediaObject(MediaObject *mediaObject)
246{
247 // Undo all connections or path creation->destruction->creation can cause
248 // duplicated connections or getting signals from two different MediaObjects.
249 disconnect(sender: mediaObject, signal: 0, receiver: this, member: 0);
250}
251
252void VideoWidget::handleAddToMedia(Media *media)
253{
254 media->addOption(option: ":video");
255
256 if (!m_surfacePainter) {
257#if defined(Q_OS_MAC)
258 m_player->setNsObject(cocoaView());
259#elif defined(Q_OS_UNIX)
260 if (QGuiApplication::platformName().contains(QStringLiteral("xcb"), cs: Qt::CaseInsensitive)) {
261 m_player->setXWindow(winId());
262 } else {
263 enableSurfacePainter();
264 }
265#elif defined(Q_OS_WIN)
266 m_player->setHwnd((HWND)winId());
267#endif
268 }
269}
270
271Phonon::VideoWidget::AspectRatio VideoWidget::aspectRatio() const
272{
273 return m_aspectRatio;
274}
275
276void VideoWidget::setAspectRatio(Phonon::VideoWidget::AspectRatio aspect)
277{
278 DEBUG_BLOCK;
279 if (!m_player)
280 return;
281
282 m_aspectRatio = aspect;
283
284 switch (m_aspectRatio) {
285 // FIXME: find a way to implement aspectratiowidget, it is meant to scale
286 // and stretch (i.e. scale to window without retaining aspect ratio).
287 case Phonon::VideoWidget::AspectRatioAuto:
288 m_player->setVideoAspectRatio(QByteArray());
289 return;
290 case Phonon::VideoWidget::AspectRatio4_3:
291 m_player->setVideoAspectRatio("4:3");
292 return;
293 case Phonon::VideoWidget::AspectRatio16_9:
294 m_player->setVideoAspectRatio("16:9");
295 return;
296 }
297 warning() << "The aspect ratio" << aspect << "is not supported by Phonon VLC.";
298}
299
300Phonon::VideoWidget::ScaleMode VideoWidget::scaleMode() const
301{
302 return m_scaleMode;
303}
304
305void VideoWidget::setScaleMode(Phonon::VideoWidget::ScaleMode scale)
306{
307#ifdef __GNUC__
308#warning OMG WTF
309#endif
310 m_scaleMode = scale;
311 switch (m_scaleMode) {
312 }
313 warning() << "The scale mode" << scale << "is not supported by Phonon VLC.";
314}
315
316qreal VideoWidget::brightness() const
317{
318 return m_brightness;
319}
320
321void VideoWidget::setBrightness(qreal brightness)
322{
323 DEBUG_BLOCK;
324 if (!m_player) {
325 return;
326 }
327 if (!enableFilterAdjust()) {
328 // Add to pending adjusts
329 m_pendingAdjusts.insert(key: QByteArray("setBrightness"), value: brightness);
330 return;
331 }
332
333 // VLC operates within a 0.0 to 2.0 range for brightness.
334 m_brightness = brightness;
335 m_player->setVideoAdjust(adjust: libvlc_adjust_Brightness,
336 value: phononRangeToVlcRange(phononValue: m_brightness, upperBoundary: 2.0));
337}
338
339qreal VideoWidget::contrast() const
340{
341 return m_contrast;
342}
343
344void VideoWidget::setContrast(qreal contrast)
345{
346 DEBUG_BLOCK;
347 if (!m_player) {
348 return;
349 }
350 if (!enableFilterAdjust()) {
351 // Add to pending adjusts
352 m_pendingAdjusts.insert(key: QByteArray("setContrast"), value: contrast);
353 return;
354 }
355
356 // VLC operates within a 0.0 to 2.0 range for contrast.
357 m_contrast = contrast;
358 m_player->setVideoAdjust(adjust: libvlc_adjust_Contrast, value: phononRangeToVlcRange(phononValue: m_contrast, upperBoundary: 2.0));
359}
360
361qreal VideoWidget::hue() const
362{
363 return m_hue;
364}
365
366void VideoWidget::setHue(qreal hue)
367{
368 DEBUG_BLOCK;
369 if (!m_player) {
370 return;
371 }
372 if (!enableFilterAdjust()) {
373 // Add to pending adjusts
374 m_pendingAdjusts.insert(key: QByteArray("setHue"), value: hue);
375 return;
376 }
377
378 // VLC operates within a 0 to 360 range for hue.
379 // Phonon operates on -1.0 to 1.0, so we need to consider 0 to 180 as
380 // 0 to 1.0 and 180 to 360 as -1 to 0.0.
381 // 360/0 (0)
382 // ___
383 // / \
384 // 270 (-.25) | | 90 (.25)
385 // \___/
386 // 180 (1/-1)
387 // (-.25 is 360 minus 90 (vlcValue of .25).
388 m_hue = hue;
389 const int vlcValue = static_cast<int>(phononRangeToVlcRange(phononValue: qAbs(t: hue), upperBoundary: 180.0, shift: false));
390 int value = 0;
391 if (hue >= 0)
392 value = vlcValue;
393 else
394 value = 360.0 - vlcValue;
395 m_player->setVideoAdjust(adjust: libvlc_adjust_Hue, value);
396}
397
398qreal VideoWidget::saturation() const
399{
400 return m_saturation;
401}
402
403void VideoWidget::setSaturation(qreal saturation)
404{
405 DEBUG_BLOCK;
406 if (!m_player) {
407 return;
408 }
409 if (!enableFilterAdjust()) {
410 // Add to pending adjusts
411 m_pendingAdjusts.insert(key: QByteArray("setSaturation"), value: saturation);
412 return;
413 }
414
415 // VLC operates within a 0.0 to 3.0 range for saturation.
416 m_saturation = saturation;
417 m_player->setVideoAdjust(adjust: libvlc_adjust_Saturation,
418 value: phononRangeToVlcRange(phononValue: m_saturation, upperBoundary: 3.0));
419}
420
421QWidget *VideoWidget::widget()
422{
423 return this;
424}
425
426QSize VideoWidget::sizeHint() const
427{
428 return m_videoSize;
429}
430
431void VideoWidget::updateVideoSize(bool hasVideo)
432{
433 if (hasVideo) {
434 m_videoSize = m_player->videoSize();
435 updateGeometry();
436 update();
437 } else
438 m_videoSize = DEFAULT_QSIZE;
439}
440
441void VideoWidget::setVisible(bool visible)
442{
443 if (window() && window()->testAttribute(attribute: Qt::WA_DontShowOnScreen) && !m_surfacePainter) {
444 enableSurfacePainter();
445 }
446 QWidget::setVisible(visible);
447}
448
449void VideoWidget::processPendingAdjusts(bool videoAvailable)
450{
451 if (!videoAvailable || !m_mediaObject || !m_mediaObject->hasVideo()) {
452 return;
453 }
454
455 QHashIterator<QByteArray, qreal> it(m_pendingAdjusts);
456 while (it.hasNext()) {
457 it.next();
458 QMetaObject::invokeMethod(obj: this, member: it.key().constData(), Q_ARG(qreal, it.value()));
459 }
460 m_pendingAdjusts.clear();
461}
462
463void VideoWidget::clearPendingAdjusts()
464{
465 m_pendingAdjusts.clear();
466}
467
468void VideoWidget::paintEvent(QPaintEvent *event)
469{
470 Q_UNUSED(event);
471 if (m_surfacePainter)
472 m_surfacePainter->handlePaint(event);
473}
474
475bool VideoWidget::enableFilterAdjust(bool adjust)
476{
477 DEBUG_BLOCK;
478 // Need to check for MO here, because we can get called before a VOut is actually
479 // around in which case we just ignore this.
480 if (!m_mediaObject || !m_mediaObject->hasVideo()) {
481 debug() << "no mo or no video!!!";
482 return false;
483 }
484 if ((!m_filterAdjustActivated && adjust) ||
485 (m_filterAdjustActivated && !adjust)) {
486 debug() << "adjust: " << adjust;
487 m_player->setVideoAdjust(adjust: libvlc_adjust_Enable, value: static_cast<int>(adjust));
488 m_filterAdjustActivated = adjust;
489 }
490 return true;
491}
492
493float VideoWidget::phononRangeToVlcRange(qreal phononValue, float upperBoundary,
494 bool shift)
495{
496 // VLC operates on different ranges than Phonon. Phonon always uses a range of
497 // -1:1 with 0 as the default value.
498 // It is therefore necessary to convert between the two schemes using sophisticated magic.
499 // First the incoming range is locked between -1..1, then depending on shift
500 // either normalized to 0..2 or 0..1 and finally a new value is calculated
501 // depending on the upperBoundary and the normalized range.
502 float value = static_cast<float>(phononValue);
503 float range = 2.0; // The default normalized range will be 0..2 = 2
504
505 // Ensure valid range
506 if (value < -1.0)
507 value = -1.0;
508 else if (value > 1.0)
509 value = 1.0;
510
511 if (shift)
512 value += 1.0; // Shift into 0..2 range
513 else {
514 // Chop negative value; normalize to 0..1 = range 1
515 if (value < 0.0)
516 value = 0.0;
517 range = 1.0;
518 }
519
520 return (value * (upperBoundary/range));
521}
522
523QImage VideoWidget::snapshot() const
524{
525 DEBUG_BLOCK;
526 if (m_player)
527 return m_player->snapshot();
528 else
529 return QImage();
530}
531
532void VideoWidget::enableSurfacePainter()
533{
534 if (m_surfacePainter) {
535 return;
536 }
537
538 debug() << "ENABLING SURFACE PAINTING";
539 m_surfacePainter = new SurfacePainter;
540 m_surfacePainter->widget = this;
541 m_surfacePainter->setCallbacks(m_player);
542}
543
544} // namespace VLC
545} // namespace Phonon
546

source code of phonon-vlc/src/video/videowidget.cpp