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 | |
36 | namespace Phonon { |
37 | namespace VLC { |
38 | |
39 | #define DEFAULT_QSIZE QSize(320, 240) |
40 | |
41 | class SurfacePainter : public VideoMemoryStream |
42 | { |
43 | public: |
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 | |
70 | private: |
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 | |
198 | VideoWidget::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 | |
227 | VideoWidget::~VideoWidget() |
228 | { |
229 | if (m_surfacePainter) |
230 | m_surfacePainter->widget = 0; // Lazy delete |
231 | } |
232 | |
233 | void 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 | |
245 | void 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 | |
252 | void 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 | |
271 | Phonon::VideoWidget::AspectRatio VideoWidget::aspectRatio() const |
272 | { |
273 | return m_aspectRatio; |
274 | } |
275 | |
276 | void 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 | |
300 | Phonon::VideoWidget::ScaleMode VideoWidget::scaleMode() const |
301 | { |
302 | return m_scaleMode; |
303 | } |
304 | |
305 | void 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 | |
316 | qreal VideoWidget::brightness() const |
317 | { |
318 | return m_brightness; |
319 | } |
320 | |
321 | void 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 | |
339 | qreal VideoWidget::contrast() const |
340 | { |
341 | return m_contrast; |
342 | } |
343 | |
344 | void 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 | |
361 | qreal VideoWidget::hue() const |
362 | { |
363 | return m_hue; |
364 | } |
365 | |
366 | void 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 | |
398 | qreal VideoWidget::saturation() const |
399 | { |
400 | return m_saturation; |
401 | } |
402 | |
403 | void 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 | |
421 | QWidget *VideoWidget::widget() |
422 | { |
423 | return this; |
424 | } |
425 | |
426 | QSize VideoWidget::sizeHint() const |
427 | { |
428 | return m_videoSize; |
429 | } |
430 | |
431 | void 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 | |
441 | void 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 | |
449 | void 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 | |
463 | void VideoWidget::clearPendingAdjusts() |
464 | { |
465 | m_pendingAdjusts.clear(); |
466 | } |
467 | |
468 | void VideoWidget::paintEvent(QPaintEvent *event) |
469 | { |
470 | Q_UNUSED(event); |
471 | if (m_surfacePainter) |
472 | m_surfacePainter->handlePaint(event); |
473 | } |
474 | |
475 | bool 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 | |
493 | float 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 | |
523 | QImage VideoWidget::snapshot() const |
524 | { |
525 | DEBUG_BLOCK; |
526 | if (m_player) |
527 | return m_player->snapshot(); |
528 | else |
529 | return QImage(); |
530 | } |
531 | |
532 | void 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 | |