Skip to content

Commit 0e37d62

Browse files
committed
Video: Replace OpenGL qml6glsink with appsink Metal path on macOS
On macOS, the GStreamer qml6glsink element requires an OpenGL context, but Qt 6 defaults to Metal RHI. Rather than forcing OpenGL (deprecated on macOS), this adds a new appsink-based rendering path: GStreamer pipeline -> appsink -> GstAppSinkAdapter -> QVideoSink -> VideoOutput (Metal) Changes: - gstqgcvideosinkbin: Add macOS appsink path (videoconvert -> appsink) alongside existing D3D11 (Windows) and GL (Linux) paths - GstAppSinkAdapter: New class bridging GStreamer appsink to Qt QVideoSink, copies BGRA frames with stride-aware memcpy and buffer bounds validation - FlightDisplayViewMetal.qml: Qt VideoOutput component for Metal rendering - FlightDisplayViewVideo.qml: 3-way component selection (D3D11/Metal/GL) for both main and thermal video - QGCApplication: Let Qt use default Metal RHI on macOS instead of forcing OpenGL - VideoManager: Expose gstreamerAppleSink property, wire appsink adapter to QML VideoOutput's QVideoSink - GStreamer CMakeLists: Add GstApp dependency, Apple-gated sources and Qt Multimedia links - Unit test: _testAppsinkFrameDelivery validates end-to-end frame delivery through the appsink pipeline (macOS-only, QSKIP elsewhere) All changes are gated behind Q_OS_MACOS / if(APPLE) -- no impact on Windows, Linux, Android, or iOS builds.
1 parent c096339 commit 0e37d62

File tree

15 files changed

+500
-17
lines changed

15 files changed

+500
-17
lines changed

src/FlyView/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ qt_add_qml_module(FlyViewModule
1616
FlightDisplayViewDummy.qml
1717
FlightDisplayViewGStreamer.qml
1818
FlightDisplayViewGStreamerD3D11.qml
19+
FlightDisplayViewMetal.qml
1920
FlightDisplayViewQtMultimedia.qml
2021
FlightDisplayViewUVC.qml
2122
FlightDisplayViewVideo.qml
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import QtQuick
2+
import QtMultimedia
3+
4+
import QGroundControl
5+
6+
// macOS Metal rendering path for GStreamer video.
7+
// Frames are pushed from the C++ GstAppSinkAdapter to this VideoOutput's QVideoSink.
8+
VideoOutput {
9+
objectName: "videoContent"
10+
fillMode: VideoOutput.PreserveAspectFit
11+
12+
Connections {
13+
target: QGroundControl.videoManager
14+
function onImageFileChanged(filename) {
15+
grabToImage(function(result) {
16+
if (!result.saveToFile(filename)) {
17+
console.error('Error capturing video frame');
18+
}
19+
});
20+
}
21+
}
22+
}

src/FlyView/FlightDisplayViewVideo.qml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ Item {
137137
}
138138
}
139139
}
140+
Component {
141+
id: videoBackgroundMetalComponent
142+
FlightDisplayViewMetal {
143+
}
144+
}
140145
Loader {
141146
// GStreamer is causing crashes on Lenovo laptop OpenGL Intel drivers. In order to workaround this
142147
// we don't load a QGCVideoBackground object when video is disabled. This prevents any video rendering
@@ -146,7 +151,9 @@ Item {
146151
visible: _showStreamLoader
147152
sourceComponent: QGroundControl.videoManager.gstreamerD3D11Sink
148153
? videoBackgroundD3D11Component
149-
: videoBackgroundGLComponent
154+
: QGroundControl.videoManager.gstreamerAppleSink
155+
? videoBackgroundMetalComponent
156+
: videoBackgroundGLComponent
150157

151158
property bool videoDisabled: QGroundControl.settingsManager.videoSettings.videoSource.rawValue === QGroundControl.settingsManager.videoSettings.disabledVideoSource
152159
}
@@ -233,7 +240,10 @@ Item {
233240
anchors.fill: parent
234241
opacity: _camera ? (_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_BLEND ? _camera.thermalOpacity / 100 : 1.0) : 0
235242
sourceComponent: QGroundControl.videoManager.gstreamerD3D11Sink
236-
? thermalBackgroundD3D11 : thermalBackgroundGL
243+
? thermalBackgroundD3D11
244+
: QGroundControl.videoManager.gstreamerAppleSink
245+
? thermalBackgroundMetal
246+
: thermalBackgroundGL
237247
onLoaded: { if (item) item.objectName = "thermalVideo" }
238248

239249
Component {
@@ -244,6 +254,10 @@ Item {
244254
id: thermalBackgroundD3D11
245255
QGCVideoBackgroundD3D11 {}
246256
}
257+
Component {
258+
id: thermalBackgroundMetal
259+
FlightDisplayViewMetal {}
260+
}
247261
}
248262
}
249263
//-- Zoom

src/QGCApplication.cc

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,15 +255,17 @@ bool QGCApplication::_initVideo()
255255
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
256256
}
257257
qCDebug(QGCApplicationLog) << "D3D11 video sink available, using default graphics API";
258+
#elif defined(Q_OS_MACOS)
259+
// macOS Metal rendering path: appsink → QVideoSink → VideoOutput.
260+
// Do NOT force OpenGL — let Qt use the default Metal RHI backend.
261+
// The appsink path in qgcvideosinkbin avoids the GL-dependent qml6glsink.
262+
if (isOffscreen) {
263+
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
264+
} else {
265+
qCDebug(QGCApplicationLog) << "macOS: using default RHI backend (Metal) for appsink video path";
266+
}
258267
#else
259-
const bool skipGLProbe = isOffscreen
260-
#if defined(Q_OS_MACOS)
261-
// macOS still provides OpenGL (deprecated but functional). The
262-
// QOpenGLContext::create() probe is unreliable without a native
263-
// surface, so skip it and force OpenGL for qml6glsink.
264-
|| true
265-
#endif
266-
;
268+
const bool skipGLProbe = isOffscreen;
267269

268270
if (skipGLProbe) {
269271
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
@@ -276,7 +278,7 @@ bool QGCApplication::_initVideo()
276278
<< "Using default graphics API (Metal/Vulkan).";
277279
}
278280
}
279-
#endif // QGC_GST_D3D11_SINK
281+
#endif // QGC_GST_D3D11_SINK / Q_OS_MACOS
280282
#endif
281283

282284
QGCCorePlugin::instance(); // CorePlugin must be initialized before VideoManager for Video Cleanup

src/VideoManager/VideoManager.cc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
#include "UVCReceiver.h"
2323
#ifdef QGC_GST_STREAMING
2424
#include "GStreamerHelpers.h"
25+
#include "GStreamer.h"
26+
#endif
27+
#if defined(QGC_GST_STREAMING) && defined(Q_OS_MACOS)
28+
#include <QtMultimedia/QVideoSink>
29+
#include <QtMultimediaQuick/private/qquickvideooutput_p.h>
2530
#endif
2631

2732
#include <QtConcurrent/QtConcurrent>
@@ -501,6 +506,15 @@ bool VideoManager::gstreamerD3D11Sink()
501506
#endif
502507
}
503508

509+
bool VideoManager::gstreamerAppleSink()
510+
{
511+
#if defined(QGC_GST_STREAMING) && defined(Q_OS_MACOS) && !defined(QGC_GST_D3D11_SINK)
512+
return true;
513+
#else
514+
return false;
515+
#endif
516+
}
517+
504518
bool VideoManager::uvcEnabled()
505519
{
506520
return UVCReceiver::enabled();
@@ -888,6 +902,22 @@ void VideoManager::_initVideoReceiver(VideoReceiver *receiver, QQuickWindow *win
888902
}
889903
receiver->setSink(sink);
890904

905+
#if defined(QGC_GST_STREAMING) && defined(Q_OS_MACOS) && !defined(QGC_GST_D3D11_SINK)
906+
// macOS Metal path: connect appsink inside the sinkbin to the QVideoSink
907+
// belonging to the QML VideoOutput widget.
908+
if (sink && widget) {
909+
auto *videoOutput = qobject_cast<QQuickVideoOutput *>(widget);
910+
if (videoOutput) {
911+
QVideoSink *videoSink = videoOutput->videoSink();
912+
if (!GStreamer::setupAppleSinkAdapter(sink, videoSink, receiver)) {
913+
qCWarning(VideoManagerLog) << "setupAppleSinkAdapter failed" << receiver->name();
914+
}
915+
} else {
916+
qCWarning(VideoManagerLog) << "Widget is not a VideoOutput, cannot connect appsink" << receiver->name();
917+
}
918+
}
919+
#endif
920+
891921
(void) connect(receiver, &VideoReceiver::onStartComplete, this, [this, receiver](VideoReceiver::STATUS status) {
892922
qCDebug(VideoManagerLog) << "Video" << receiver->name() << "Start complete, status:" << status;
893923
switch (status) {

src/VideoManager/VideoManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class VideoManager : public QObject
2727

2828
Q_PROPERTY(bool gstreamerEnabled READ gstreamerEnabled CONSTANT)
2929
Q_PROPERTY(bool gstreamerD3D11Sink READ gstreamerD3D11Sink CONSTANT)
30+
Q_PROPERTY(bool gstreamerAppleSink READ gstreamerAppleSink CONSTANT)
3031
Q_PROPERTY(bool qtmultimediaEnabled READ qtmultimediaEnabled CONSTANT)
3132
Q_PROPERTY(bool uvcEnabled READ uvcEnabled CONSTANT)
3233
Q_PROPERTY(bool autoStreamConfigured READ autoStreamConfigured NOTIFY autoStreamConfiguredChanged)
@@ -83,6 +84,7 @@ class VideoManager : public QObject
8384
void setfullScreen(bool on);
8485
static bool gstreamerEnabled();
8586
static bool gstreamerD3D11Sink();
87+
static bool gstreamerAppleSink();
8688
static bool qtmultimediaEnabled();
8789
static bool uvcEnabled();
8890

src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE VideoItemStub.h)
44
if(QGC_ENABLE_GST_VIDEOSTREAMING)
55
find_package(QGCGStreamer
66
REQUIRED
7-
COMPONENTS Core Base Video Gl GlPrototypes Rtsp
7+
COMPONENTS Core Base Video Gl GlPrototypes Rtsp App
88
OPTIONAL_COMPONENTS GlEgl GlWayland GlX11
99
)
1010

@@ -20,6 +20,19 @@ if(QGC_ENABLE_GST_VIDEOSTREAMING)
2020
GstVideoReceiver.h
2121
)
2222

23+
if(APPLE)
24+
target_sources(${CMAKE_PROJECT_NAME}
25+
PRIVATE
26+
GstAppSinkAdapter.cc
27+
GstAppSinkAdapter.h
28+
)
29+
target_link_libraries(${CMAKE_PROJECT_NAME}
30+
PRIVATE
31+
Qt6::Multimedia
32+
Qt6::MultimediaQuickPrivate
33+
)
34+
endif()
35+
2336
add_subdirectory(gstqgc)
2437
endif()
2538

src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
#include "AppSettings.h"
55
#include "GstVideoReceiver.h"
66

7+
#ifdef Q_OS_MACOS
8+
#include "GstAppSinkAdapter.h"
9+
#endif
10+
711
#include <QtCore/QCoreApplication>
812
#include <QtCore/QDir>
913
#include <QtCore/QFileInfo>
@@ -870,4 +874,24 @@ VideoReceiver *createVideoReceiver(QObject *parent)
870874
return new GstVideoReceiver(parent);
871875
}
872876

877+
bool setupAppleSinkAdapter(void *sinkBin, QVideoSink *videoSink, QObject *adapterParent)
878+
{
879+
#ifdef Q_OS_MACOS
880+
if (!sinkBin || !videoSink) {
881+
return false;
882+
}
883+
884+
auto *adapter = new GstAppSinkAdapter(adapterParent);
885+
if (!adapter->setup(GST_ELEMENT(sinkBin), videoSink)) {
886+
qCCritical(GStreamerLog) << "GstAppSinkAdapter::setup() failed";
887+
adapter->deleteLater();
888+
return false;
889+
}
890+
return true;
891+
#else
892+
Q_UNUSED(sinkBin); Q_UNUSED(videoSink); Q_UNUSED(adapterParent);
893+
return false;
894+
#endif
895+
}
896+
873897
} // namespace GStreamer

src/VideoManager/VideoReceiver/GStreamer/GStreamer.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Q_DECLARE_LOGGING_CATEGORY(GStreamerAPILog)
77
Q_DECLARE_LOGGING_CATEGORY(GStreamerDecoderRanksLog)
88

99
class QQuickItem;
10+
class QVideoSink;
1011
class VideoReceiver;
1112

1213
namespace GStreamer
@@ -31,4 +32,8 @@ void *createVideoSink(QQuickItem *widget, QObject *parent = nullptr);
3132
void releaseVideoSink(void *sink);
3233
VideoReceiver *createVideoReceiver(QObject *parent = nullptr);
3334

35+
/// On macOS: connect the appsink inside the sinkbin to a QVideoSink.
36+
/// Returns true on success. No-op (returns false) on other platforms.
37+
bool setupAppleSinkAdapter(void *sinkBin, QVideoSink *videoSink, QObject *adapterParent);
38+
3439
}

0 commit comments

Comments
 (0)