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