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