Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquickvideooutput.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// Copyright (C) 2016 Research In Motion
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
6
7#include <QtCore/qloggingcategory.h>
8#include <QtCore/qrunnable.h>
9#include <QtCore/private/qfactoryloader_p.h>
10
11#include <QtMultimedia/qmediaplayer.h>
12#include <QtMultimedia/qmediacapturesession.h>
13#include <QtMultimedia/private/qmultimediautils_p.h>
14#include <QtMultimedia/private/qvideoframetexturepool_p.h>
15#include <QtMultimedia/private/qvideooutputorientationhandler_p.h>
16
17#include <QtMultimediaQuick/private/qsgvideonode_p.h>
18
19#include <QtQuick/qquickwindow.h>
20#include <QtQuick/private/qquickwindow_p.h>
21
23
24Q_STATIC_LOGGING_CATEGORY(qLcVideo, "qt.multimedia.video")
25
26namespace {
27
28inline bool qIsDefaultAspect(int o)
29{
30 return (o % 180) == 0;
31}
32
33inline bool qIsDefaultAspect(QtVideo::Rotation rotation)
34{
35 return qIsDefaultAspect(qToUnderlying(rotation));
36}
37}
38
39/*!
40 \qmltype VideoOutput
41 \inherits Item
42 \brief Render video or camera viewfinder.
43
44 \ingroup multimedia_qml
45 \ingroup multimedia_video_qml
46 \inqmlmodule QtMultimedia
47
48 \qml
49
50 Rectangle {
51 width: 800
52 height: 600
53 color: "black"
54
55 MediaPlayer {
56 id: player
57 source: "file://video.webm"
58 videoOutput: videoOutput
59 }
60
61 VideoOutput {
62 id: videoOutput
63 anchors.fill: parent
64 }
65 }
66
67 \endqml
68
69 The VideoOutput item supports untransformed, stretched, and uniformly scaled video presentation.
70 For a description of stretched uniformly scaled presentation, see the \l fillMode property
71 description.
72
73 \sa MediaPlayer, Camera
74
75\omit
76 \section1 Screen Saver
77
78 If it is likely that an application will be playing video for an extended
79 period of time without user interaction it may be necessary to disable
80 the platform's screen saver. The \l ScreenSaver (from \l QtSystemInfo)
81 may be used to disable the screensaver in this fashion:
82
83 \qml
84 import QtSystemInfo
85
86 ScreenSaver { screenSaverEnabled: false }
87 \endqml
88\endomit
89*/
90
91// TODO: Restore Qt System Info docs when the module is released
92
93/*!
94 \internal
95 \class QQuickVideoOutput
96 \brief The QQuickVideoOutput class provides a video output item.
97*/
98
99QQuickVideoOutput::QQuickVideoOutput(QQuickItem *parent) :
100 QQuickItem(parent)
101{
102 setFlag(ItemHasContents, true);
103
104 m_sink = new QQuickVideoSink(this);
105 qRegisterMetaType<QVideoFrameFormat>();
106
107 // TODO: investigate if we have any benefit of setting frame in the source thread
108 connect(m_sink, &QVideoSink::videoFrameChanged, this,
109 makeGuardedCall([this](const QVideoFrame &frame) {
110 if (frame.isValid() || m_endOfStreamPolicy == ClearOutput)
111 setFrame(frame);
112 }),
113 Qt::DirectConnection);
114
115 initRhiForSink();
116}
117
118QQuickVideoOutput::~QQuickVideoOutput()
119{
120 {
121 QMutexLocker lock(&m_destructorGuard->m_mutex);
122 m_destructorGuard->m_isAlive = false;
123 }
124
125 delete m_sink;
126 disconnectWindowConnections();
127}
128
129/*!
130 \qmlproperty object QtMultimedia::VideoOutput::videoSink
131
132 This property holds the underlaying C++ QVideoSink object that is used
133 to render the video frames to this VideoOutput element.
134
135 Normal usage of VideoOutput from QML should not require using this property.
136*/
137
138QVideoSink *QQuickVideoOutput::videoSink() const
139{
140 return m_sink;
141}
142
143/*!
144 \qmlproperty enumeration QtMultimedia::VideoOutput::fillMode
145
146 Set this property to define how the video is scaled to fit the target area.
147
148 \list
149 \li Stretch - the video is scaled to fit.
150 \li PreserveAspectFit - the video is scaled uniformly to fit without cropping
151 \li PreserveAspectCrop - the video is scaled uniformly to fill, cropping if necessary
152 \endlist
153
154 The default fill mode is PreserveAspectFit.
155*/
156
157QQuickVideoOutput::FillMode QQuickVideoOutput::fillMode() const
158{
159 return FillMode(m_aspectRatioMode);
160}
161
162void QQuickVideoOutput::setFillMode(FillMode mode)
163{
164 if (mode == fillMode())
165 return;
166
167 m_aspectRatioMode = Qt::AspectRatioMode(mode);
168
169 m_geometryDirty = true;
170 update();
171
172 Q_EMIT fillModeChanged(mode);
173}
174
175void QQuickVideoOutput::_q_newFrame(QSize size)
176{
177 update();
178
179 size = qRotatedFrameSize(size, m_frameDisplayingRotation);
180
181 if (m_nativeSize != size) {
182 m_nativeSize = size;
183
184 m_geometryDirty = true;
185
186 setImplicitWidth(size.width());
187 setImplicitHeight(size.height());
188
189 Q_EMIT sourceRectChanged();
190 }
191}
192
193/* Based on fill mode and our size, figure out the source/dest rects */
194void QQuickVideoOutput::_q_updateGeometry()
195{
196 const QRectF rect(0, 0, width(), height());
197 const QRectF absoluteRect(x(), y(), width(), height());
198
199 if (!m_geometryDirty && m_lastRect == absoluteRect)
200 return;
201
202 QRectF oldContentRect(m_contentRect);
203
204 m_geometryDirty = false;
205 m_lastRect = absoluteRect;
206
207 const auto fill = m_aspectRatioMode;
208 if (m_nativeSize.isEmpty()) {
209 //this is necessary for item to receive the
210 //first paint event and configure video surface.
211 m_contentRect = rect;
212 } else if (fill == Qt::IgnoreAspectRatio) {
213 m_contentRect = rect;
214 } else {
215 QSizeF scaled = m_nativeSize;
216 scaled.scale(rect.size(), fill);
217
218 m_contentRect = QRectF(QPointF(), scaled);
219 m_contentRect.moveCenter(rect.center());
220 }
221
222 updateGeometry();
223
224 if (m_contentRect != oldContentRect)
225 Q_EMIT contentRectChanged();
226}
227
228/*!
229 \qmlproperty int QtMultimedia::VideoOutput::orientation
230
231 This property determines the angle in, degrees, at which the displayed video
232 is rotated clockwise in video coordinates, where the Y-axis points
233 downwards on the display.
234 The orientation transformation is applied before \l mirrored.
235
236 The orientation change affects the mapping of coordinates from the source to the viewport.
237
238 Only multiples of \c 90 degrees are supported, that is 0, 90, -90, 180, 270, etc.,
239 otherwise, the specified value is ignored.
240
241 In some cases, the source video stream requires a certain
242 orientation to be corrected. This includes
243 sources like a camera viewfinder, where the displayed
244 viewfinder should match the reality, no matter what rotation
245 the rest of the user interface has.
246
247 We recommend using this property to compensate a user interface
248 rotation, or align the output view with other application business
249 requirements.
250
251 The default value is \c 0.
252*/
253int QQuickVideoOutput::orientation() const
254{
255 return m_orientation;
256}
257
258void QQuickVideoOutput::setOrientation(int orientation)
259{
260 // Make sure it's a multiple of 90.
261 if (orientation % 90)
262 return;
263
264 // If there's no actual change, return
265 if (m_orientation == orientation)
266 return;
267
268 // If the new orientation is the same effect
269 // as the old one, don't update the video node stuff
270 if (qVideoRotationFromDegrees(orientation - m_orientation) == QtVideo::Rotation::None) {
271 m_orientation = orientation;
272 Q_EMIT orientationChanged();
273 return;
274 }
275
276 m_geometryDirty = true;
277
278 // Otherwise, a new orientation
279 // See if we need to change aspect ratio orientation too
280 bool oldAspect = qIsDefaultAspect(m_orientation);
281 bool newAspect = qIsDefaultAspect(orientation);
282
283 m_orientation = orientation;
284
285 {
286 QMutexLocker lock(&m_frameMutex);
287 m_frameDisplayingRotation = qNormalizedFrameTransformation(m_frame, m_orientation).rotation;
288 }
289
290 if (oldAspect != newAspect) {
291 m_nativeSize.transpose();
292
293 setImplicitWidth(m_nativeSize.width());
294 setImplicitHeight(m_nativeSize.height());
295
296 // Source rectangle does not change for orientation
297 }
298
299 update();
300 Q_EMIT orientationChanged();
301}
302
303/*!
304 \qmlproperty bool QtMultimedia::VideoOutput::mirrored
305 \since 6.9
306
307 Determines whether the displayed video is mirrored around its vertical axis.
308 We recommend using this property if the transformation of the source video stream,
309 such as from a front camera, does not align with the application's business logic.
310 The mirroring is applied after \l orientation.
311
312 The property change affects the mapping of coordinates from the source to the viewport.
313
314 The default value is \c false.
315*/
316bool QQuickVideoOutput::mirrored() const
317{
318 return m_mirrored;
319}
320
321void QQuickVideoOutput::setMirrored(bool mirrored)
322{
323 if (m_mirrored == mirrored)
324 return;
325 m_mirrored = mirrored;
326
327 update();
328 Q_EMIT mirroredChanged();
329}
330
331/*!
332 \qmlproperty rectangle QtMultimedia::VideoOutput::contentRect
333
334 This property holds the item coordinates of the area that
335 would contain video to render. With certain fill modes,
336 this rectangle will be larger than the visible area of the
337 \c VideoOutput.
338
339 This property is useful when other coordinates are specified
340 in terms of the source dimensions - this applied for relative
341 (normalized) frame coordinates in the range of 0 to 1.0.
342
343 Areas outside this will be transparent.
344*/
345QRectF QQuickVideoOutput::contentRect() const
346{
347 return m_contentRect;
348}
349
350/*!
351 \qmlproperty enumeration QtMultimedia::VideoOutput::endOfStreamPolicy
352 \since 6.9
353
354 This property specifies the policy to apply when the video stream ends. This
355 occurs at the end of media playback or upon deactivation of the camera, screen,
356 or window capture.
357
358 The \c endOfStreamPolicy can be one of:
359
360 \value ClearOutput The video output is cleared.
361 \value KeepLastFrame The video output continues displaying the
362 last frame. Use the method \l clearOutput() to clear the output manually.
363
364 The default value is \c ClearOutput.
365*/
366QQuickVideoOutput::EndOfStreamPolicy QQuickVideoOutput::endOfStreamPolicy() const
367{
368 return m_endOfStreamPolicy;
369}
370
371void QQuickVideoOutput::setEndOfStreamPolicy(EndOfStreamPolicy policy)
372{
373 if (m_endOfStreamPolicy == policy)
374 return;
375
376 m_endOfStreamPolicy = policy;
377 Q_EMIT endOfStreamPolicyChanged(policy);
378}
379
380/*!
381 \qmlmethod void QtMultimedia::VideoOutput::clearOutput()
382 \since 6.9
383
384 Clears the video output by removing the currently displayed video frame.
385 This method is recommended when you need to remove the last video frame after
386 detaching the video output from the source or after the source stream ends
387 with the video output's \l endOfStreamPolicy property set to \c KeepLastFrame.
388*/
389void QQuickVideoOutput::clearOutput()
390{
391 setFrame({});
392}
393
394/*!
395 \qmlproperty rectangle QtMultimedia::VideoOutput::sourceRect
396
397 This property holds the area of the source video
398 content that is considered for rendering. The
399 values are in source pixel coordinates, adjusted for
400 the source's pixel aspect ratio.
401
402 Note that typically the top left corner of this rectangle
403 will be \c {0,0} while the width and height will be the
404 width and height of the input content. Only when the video
405 source has a viewport set, these values will differ.
406
407 The orientation setting does not affect this rectangle.
408
409 \sa QVideoFrameFormat::viewport()
410*/
411QRectF QQuickVideoOutput::sourceRect() const
412{
413 // We might have to transpose back
414 QSizeF size = m_nativeSize;
415 if (!size.isValid())
416 return {};
417
418 if (!qIsDefaultAspect(m_frameDisplayingRotation))
419 size.transpose();
420
421
422 // Take the viewport into account for the top left position.
423 // m_nativeSize is already adjusted to the viewport, as it originates
424 // from QVideoFrameFormat::viewport(), which includes pixel aspect ratio
425 const QRectF viewport = adjustedViewport();
426 Q_ASSERT(viewport.size() == size);
427 return QRectF(viewport.topLeft(), size);
428}
429
430void QQuickVideoOutput::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
431{
432 Q_UNUSED(newGeometry);
433 Q_UNUSED(oldGeometry);
434
435 QQuickItem::geometryChange(newGeometry, oldGeometry);
436
437 // Explicitly listen to geometry changes here. This is needed since changing the position does
438 // not trigger a call to updatePaintNode().
439 // We need to react to position changes though, as the window backened's display rect gets
440 // changed in that situation.
441 _q_updateGeometry();
442}
443
444void QQuickVideoOutput::releaseResources()
445{
446 // Called on the gui thread when the window is closed or changed.
447 initRhiForSink();
448 QQuickItem::releaseResources();
449}
450
451void QQuickVideoOutput::initRhiForSink()
452{
453 QRhi *rhi = m_window ? QQuickWindowPrivate::get(m_window)->rhi : nullptr;
454 m_sink->setRhi(rhi);
455}
456
457void QQuickVideoOutput::itemChange(QQuickItem::ItemChange change,
458 const QQuickItem::ItemChangeData &changeData)
459{
460 if (change != QQuickItem::ItemSceneChange)
461 return;
462
463 if (changeData.window == m_window)
464 return;
465
466 disconnectWindowConnections();
467 m_window = changeData.window;
468
469 if (m_window) {
470 auto connectToWindow = [&](auto signal, auto function) {
471 connect(m_window, signal, this, makeGuardedCall(std::move(function)),
472 Qt::DirectConnection);
473 };
474
475 // We want to receive the signals in the render thread
476 connectToWindow(&QQuickWindow::sceneGraphInitialized, [this] {
477 initRhiForSink();
478 });
479
480 connectToWindow(&QQuickWindow::sceneGraphInvalidated, [this] {
481 if (auto texturePool = m_texturePool.lock())
482 texturePool->clearTextures();
483 m_sink->setRhi(nullptr);
484 });
485
486 connectToWindow(&QQuickWindow::afterFrameEnd, [this] {
487 if (auto texturePool = m_texturePool.lock())
488 texturePool->onFrameEndInvoked();
489 });
490 }
491 initRhiForSink();
492}
493
494QSize QQuickVideoOutput::nativeSize() const
495{
496 return m_videoFormat.viewport().size();
497}
498
499void QQuickVideoOutput::updateGeometry()
500{
501 const QRectF viewport = m_videoFormat.viewport();
502 const QSizeF frameSize = m_videoFormat.frameSize();
503 const QRectF normalizedViewport(viewport.x() / frameSize.width(),
504 viewport.y() / frameSize.height(),
505 viewport.width() / frameSize.width(),
506 viewport.height() / frameSize.height());
507 const QRectF rect(0, 0, width(), height());
508 if (nativeSize().isEmpty()) {
509 m_renderedRect = rect;
510 m_sourceTextureRect = normalizedViewport;
511 } else if (m_aspectRatioMode == Qt::IgnoreAspectRatio) {
512 m_renderedRect = rect;
513 m_sourceTextureRect = normalizedViewport;
514 } else if (m_aspectRatioMode == Qt::KeepAspectRatio) {
515 m_sourceTextureRect = normalizedViewport;
516 m_renderedRect = contentRect();
517 } else if (m_aspectRatioMode == Qt::KeepAspectRatioByExpanding) {
518 m_renderedRect = rect;
519 const qreal contentHeight = contentRect().height();
520 const qreal contentWidth = contentRect().width();
521
522 // Calculate the size of the source rectangle without taking the viewport into account
523 const qreal relativeOffsetLeft = -contentRect().left() / contentWidth;
524 const qreal relativeOffsetTop = -contentRect().top() / contentHeight;
525 const qreal relativeWidth = rect.width() / contentWidth;
526 const qreal relativeHeight = rect.height() / contentHeight;
527
528 // Now take the viewport size into account
529 const qreal totalOffsetLeft = normalizedViewport.x() + relativeOffsetLeft * normalizedViewport.width();
530 const qreal totalOffsetTop = normalizedViewport.y() + relativeOffsetTop * normalizedViewport.height();
531 const qreal totalWidth = normalizedViewport.width() * relativeWidth;
532 const qreal totalHeight = normalizedViewport.height() * relativeHeight;
533
534 if (qIsDefaultAspect(m_frameDisplayingRotation)) {
535 m_sourceTextureRect = QRectF(totalOffsetLeft, totalOffsetTop,
536 totalWidth, totalHeight);
537 } else {
538 m_sourceTextureRect = QRectF(totalOffsetTop, totalOffsetLeft,
539 totalHeight, totalWidth);
540 }
541 }
542}
543
544QSGNode *QQuickVideoOutput::updatePaintNode(QSGNode *oldNode,
545 QQuickItem::UpdatePaintNodeData *data)
546{
547 Q_UNUSED(data);
548 _q_updateGeometry();
549
550 QSGVideoNode *videoNode = static_cast<QSGVideoNode *>(oldNode);
551
552 QMutexLocker lock(&m_frameMutex);
553
554 if (m_frameChanged) {
555 if (videoNode && videoNode->pixelFormat() != m_frame.pixelFormat()) {
556 qCDebug(qLcVideo) << "updatePaintNode: deleting old video node because frame format changed";
557 delete videoNode;
558 videoNode = nullptr;
559 }
560
561 if (!m_frame.isValid()) {
562 qCDebug(qLcVideo) << "updatePaintNode: no frames yet";
563 m_frameChanged = false;
564 return nullptr;
565 }
566
567 if (!videoNode) {
568 // Get a node that supports our frame. The surface is irrelevant, our
569 // QSGVideoItemSurface supports (logically) anything.
570 updateGeometry();
571 QRhi *rhi = m_window ? QQuickWindowPrivate::get(m_window)->rhi : nullptr;
572 videoNode = new QSGVideoNode(this, m_videoFormat, rhi);
573 m_texturePool = videoNode->texturePool();
574 qCDebug(qLcVideo) << "updatePaintNode: Video node created.";
575 }
576 }
577
578 if (!videoNode) {
579 m_frameChanged = false;
580 m_frame = QVideoFrame();
581 return nullptr;
582 }
583
584 if (m_frameChanged) {
585 videoNode->setCurrentFrame(m_frame);
586
587 updateHdr(videoNode);
588
589 //don't keep the frame for more than really necessary
590 m_frameChanged = false;
591 m_frame = QVideoFrame();
592 }
593
594 videoNode->setTexturedRectGeometry(
595 m_renderedRect, m_sourceTextureRect,
596 VideoTransformation{ qVideoRotationFromDegrees(orientation()), m_mirrored });
597
598 return videoNode;
599}
600
601void QQuickVideoOutput::updateHdr(QSGVideoNode *videoNode)
602{
603 auto *videoOutputWindow = window();
604 if (!videoOutputWindow)
605 return;
606
607 auto *swapChain = videoOutputWindow->swapChain();
608 if (!swapChain)
609 return;
610
611 const auto requiredSwapChainFormat = qGetRequiredSwapChainFormat(m_frame.surfaceFormat());
612 if (qShouldUpdateSwapChainFormat(swapChain, requiredSwapChainFormat)) {
613 auto *recreateSwapChainJob = QRunnable::create([swapChain, requiredSwapChainFormat]() {
614 swapChain->destroy();
615 swapChain->setFormat(requiredSwapChainFormat);
616 swapChain->createOrResize();
617 });
618
619 // Even though the 'recreate swap chain' job is scheduled for the current frame the
620 // effect will be visible only starting from the next frame since the recreation would
621 // happen after the actual swap.
622 videoOutputWindow->scheduleRenderJob(recreateSwapChainJob, QQuickWindow::AfterSwapStage);
623 }
624
625 videoNode->setSurfaceFormat(swapChain->format());
626 videoNode->setHdrInfo(swapChain->hdrInfo());
627}
628
629void QQuickVideoOutput::disconnectWindowConnections()
630{
631 if (!m_window)
632 return;
633
634 m_window->disconnect(this);
635}
636
637QRectF QQuickVideoOutput::adjustedViewport() const
638{
639 return m_videoFormat.viewport();
640}
641
642void QQuickVideoOutput::setFrame(const QVideoFrame &frame)
643{
644 {
645 QMutexLocker lock(&m_frameMutex);
646
647 m_videoFormat = frame.surfaceFormat();
648 m_frame = frame;
649 m_frameDisplayingRotation = qNormalizedFrameTransformation(frame, m_orientation).rotation;
650 m_frameChanged = true;
651 }
652
653 QMetaObject::invokeMethod(this, &QQuickVideoOutput::_q_newFrame, frame.size());
654}
655
656std::optional<std::chrono::nanoseconds> QQuickVideoOutput::g_signalBackoff;
657void QQuickVideoOutput::setSignalBackoff(std::optional<std::chrono::nanoseconds> ns)
658{
659 g_signalBackoff = ns;
660}
661
662QT_END_NAMESPACE
663
664#include "moc_qquickvideooutput_p.cpp"
Combined button and popup list for selecting options.