Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions .github/build-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,20 @@
"app",
"coreelements",
"isomp4",
"jpegformat",
"libav",
"matroska",
"mpegtsdemux",
"multifile",
"multipart",
"opengl",
"openh264",
"playback",
"rtp",
"rtpmanager",
"rtsp",
"sdpelem",
"soup",
"tcp",
"typefindfunctions",
"udp",
Expand All @@ -58,10 +61,27 @@
"videoconvert",
"videoscale"
],
"android": ["androidmedia", "dav1d"],
"apple": ["applemedia", "dav1d"],
"windows": ["d3d", "d3d11", "d3d12", "dav1d", "nvcodec"],
"linux": ["nvcodec", "qsv", "va", "vulkan"]
"android": [
"androidmedia",
"dav1d"
],
"apple": [
"applemedia",
"dav1d"
],
"windows": [
"d3d",
"d3d11",
"d3d12",
"dav1d",
"nvcodec"
],
"linux": [
"nvcodec",
"qsv",
"va",
"vulkan"
]
},
"checksums": {
"1.28.4": {
Expand Down
8 changes: 5 additions & 3 deletions cmake/GStreamer/Components.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,14 @@ function(gstreamer_build_apis_and_deps APIS_OUT DEPS_OUT)
endforeach()

# Platform extra deps — kept here as the single registration point.
# GstSourceFactory uses GTlsFileDatabase for custom-CA HTTPS sources on
# desktop. Mobile SDKs provide GIO through their bundled GStreamer target.
if(NOT ANDROID AND NOT IOS)
list(APPEND _deps gio-2.0)
endif()
if(WIN32)
list(APPEND _deps graphene-1.0)
endif()
if(ANDROID OR IOS)
list(APPEND _deps gio-2.0)
endif()
if(ANDROID)
list(APPEND _deps gmodule-2.0 zlib)
endif()
Expand Down
3 changes: 2 additions & 1 deletion cmake/GStreamer/PluginPolicy.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ set(GSTREAMER_PLUGIN_ALTERNATES
# multifile (splitmuxsink), isomp4 (qtmux/mp4mux), and matroska (matroskamux)
# are load-bearing for video recording (GstVideoReceiver _kFileMux).
set(GSTREAMER_RUNTIME_REQUIRED_PLUGINS
coreelements isomp4 matroska multifile opengl playback rtsp rtp rtpmanager tcp udp videoconvertscale
app coreelements isomp4 jpegformat matroska multifile multipart opengl playback rtsp rtp
rtpmanager soup tcp udp videoconvertscale
)

# iOS xcframework: plugins whose dependent static libs aren't bundled in the
Expand Down
3 changes: 2 additions & 1 deletion cmake/GStreamer/tests/test_components.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ qgc_test_assert_in_list("seed: api_gl_prototypes" api_gl_prototypes _apis)
qgc_test_assert_in_list("seed: api_rtsp" api_rtsp _apis)
qgc_test_assert_in_list("seed: api_video" api_video _apis)
qgc_test_assert_in_list("seed deps: gstreamer-base-1.0" gstreamer-base-1.0 _deps)
qgc_test_assert_in_list("desktop TLS dependency: gio-2.0" gio-2.0 _deps)
qgc_test_pass("build_apis_and_deps Core seed")

gstreamer_build_apis_and_deps(_apis2 _deps2 Core App)
Expand All @@ -94,7 +95,7 @@ qgc_test_pass("WIN32 platform extras")

set(ANDROID ON)
gstreamer_build_apis_and_deps(_apis_a _deps_a Core)
qgc_test_assert_in_list("ANDROID -> gio-2.0" gio-2.0 _deps_a)
qgc_test_assert_not_in_list("ANDROID uses bundled gio-2.0" gio-2.0 _deps_a)
qgc_test_assert_in_list("ANDROID -> gmodule-2.0" gmodule-2.0 _deps_a)
qgc_test_assert_in_list("ANDROID -> zlib" zlib _deps_a)
set(ANDROID OFF)
Expand Down
6 changes: 5 additions & 1 deletion cmake/GStreamer/tests/test_plugin_policy.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ qgc_test_assert_in_list("apple has vtdec" vtdec _plugins_apple)
qgc_test_pass("plugins_for apple addenda")

file(READ "${CMAKE_CURRENT_LIST_DIR}/../../../.github/build-config.json" QGC_BUILD_CONFIG_CONTENT)
gstreamer_plugins_for(PLATFORM "" OUT_VAR _plugins_common_real)
gstreamer_plugins_for(PLATFORM windows OUT_VAR _plugins_windows_real)
qgc_test_assert_in_list("windows has d3d11" d3d11 _plugins_windows_real)
qgc_test_assert_in_list("windows has d3d12" d3d12 _plugins_windows_real)
Expand Down Expand Up @@ -72,8 +73,11 @@ qgc_test_assert_in_list("partial: x264enc retained" x264enc
qgc_test_pass("filter_alternates partial-pair unsatisfied")

gstreamer_runtime_required_plugins(_required)
foreach(_p IN ITEMS coreelements isomp4 matroska multifile opengl playback rtsp rtp rtpmanager tcp udp videoconvertscale)
foreach(_p IN ITEMS
app coreelements isomp4 jpegformat matroska multifile multipart opengl playback
rtsp rtp rtpmanager soup tcp udp videoconvertscale)
qgc_test_assert_in_list("runtime required: ${_p}" "${_p}" _required)
qgc_test_assert_in_list("common packaged runtime requirement: ${_p}" "${_p}" _plugins_common_real)
endforeach()
qgc_test_assert_not_in_list("runtime required: openh264 is optional codec implementation" openh264 _required)
qgc_test_pass("runtime_required_plugins")
Expand Down
15 changes: 15 additions & 0 deletions cmake/find-modules/FindGStreamer.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# components without editing this file.
# 4. Hash parsing moved out — qgc_parse_expected_hash lives in
# cmake/modules/Download.cmake; this module no longer parses hashes.
# 5. Desktop GIO is resolved through the same repaired pkg-config path as
# GStreamer components for custom-CA HTTPS source support.
# When syncing from upstream, re-apply each listed patch and update this
# block. Do NOT remove this block during sync.

Expand Down Expand Up @@ -57,6 +59,9 @@ Configuration Variables
# new GStreamer::NewOne target instead of silently inheriting the cached _FOUND.
if (GStreamer_FOUND)
set(_gst_all_present TRUE)
if("gio-2.0" IN_LIST GSTREAMER_EXTRA_DEPS AND NOT TARGET GStreamer::gio)
set(_gst_all_present FALSE)
endif()
foreach(_gst_c IN LISTS GStreamer_FIND_COMPONENTS)
if (NOT TARGET GStreamer::${_gst_c} AND NOT _gst_c IN_LIST GStreamer_ABSENT_COMPONENTS)
set(_gst_all_present FALSE)
Expand Down Expand Up @@ -294,6 +299,16 @@ foreach(_gst_PLUGIN IN LISTS GSTREAMER_APIS)
_gst_create_component_target(${_gst_PLUGIN} "gstreamer-${_gst_PLUGIN_PC}-1.0")
endforeach()

# GIO is not a GStreamer API component, but GstSourceFactory calls it directly
# for a custom TLS trust database. Reuse the component target path so Windows
# pkg-config paths with spaces receive QGC's normal recovery and static/shared
# linkage remains consistent with the selected GStreamer SDK.
if("gio-2.0" IN_LIST GSTREAMER_EXTRA_DEPS)
set(GStreamer_FIND_REQUIRED_gio TRUE)
_gst_create_component_target(gio "gio-2.0")
unset(GStreamer_FIND_REQUIRED_gio)
endif()

# Link API component targets into the umbrella so consumers get the full set
# of includes and libraries (rtsp, video, gl, etc.) transitively.
if(TARGET GStreamer::GStreamer)
Expand Down
43 changes: 42 additions & 1 deletion docs/en/qgc-user-guide/settings_view/video.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Configure video streaming and recording settings.

## Video Source

- **Source** — Video Stream Disabled / RTSP Video Stream / UDP h.264 / UDP h.265 / TCP-MPEG2 / MPEG-TS / Integrated Camera
- **Source** — Video Stream Disabled / RTSP Video Stream / UDP h.264 / UDP h.265 / TCP-MPEG2 / MPEG-TS / HTTP MJPEG / WebSocket JPEG / Integrated Camera

## Connection

Expand All @@ -13,6 +13,47 @@ Connection settings vary by source type:
- **RTSP URL** — full RTSP stream address
- **TCP URL** — TCP stream address
- **UDP URL** — UDP stream address and port (default: `0.0.0.0:5600`)
- **HTTP MJPEG URL** — full `http://` or `https://` URL for a multipart MJPEG stream
- **WebSocket JPEG URL** — full `ws://` or `wss://` URL for a source that sends each complete JPEG frame as one binary WebSocket message

## Network Video Security

HTTP MJPEG and WebSocket JPEG sources can be anonymous for local labs and trusted test networks. If you select Basic or Bearer authentication, use `https://` or `wss://`; QGC rejects credentials on plaintext HTTP or WS.

- **Authentication** — None, Basic, or Bearer token
- **Username** — Basic authentication username
- **Session credential** — password or token retained only in memory until QGC exits or you clear it
- **Credential file** — optional owner-only file containing one password or token for unattended Unix-like desktop use; other platforms use the in-memory session credential
- **Origin** — optional HTTP/WebSocket Origin header for servers that require one
- **CA certificate file** — optional PEM trust file for HTTPS or WSS validation.
For HTTP MJPEG, this file replaces the system trust store for that source, so
include every issuing CA needed by the endpoint.

Do not put passwords, bearer tokens, or API keys in the video URL. QGC rejects URL user-info and common token query parameters and removes user-info, query, and fragment data from video URL logs.
HTTP MJPEG redirects are disabled whenever authentication, Origin, or a custom CA is configured so credentials and trust policy cannot be silently forwarded to another endpoint. WebSocket handshake redirects are not followed.

HTTP MJPEG and WebSocket JPEG streams can be recorded as MKV or MOV. MP4 does
not accept the parsed JPEG stream, so QGC rejects that combination before
recording instead of creating an unusable file.

For resource safety, each JPEG message is limited to 16 MiB, each dimension to
8192 pixels, and the decoded image to 7680 x 4320 pixels. Malformed or oversized
frames are rejected before decoding.

## Test Sources

Synthetic HTTP MJPEG and WebSocket JPEG servers are available in `test/VideoStreaming/`.
They generate moving JPEG frames and do not require a camera.

```bash
cd test/VideoStreaming
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
python http_mjpeg_server.py --host 127.0.0.1 --port 5077
```

Use `http://127.0.0.1:5077/video_feed` with the HTTP MJPEG source. For WebSocket JPEG, run `python websocket_jpeg_server.py --host 127.0.0.1 --port 5078` and use `ws://127.0.0.1:5078/ws/video_feed`.

## Settings

Expand Down
1 change: 1 addition & 0 deletions src/AppSettings/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ qt_add_qml_module(AppSettingsModule
SettingsPage.qml
TcpSettings.qml
UdpSettings.qml
VideoNetworkSecuritySettings.qml
NO_PLUGIN
)

Expand Down
121 changes: 121 additions & 0 deletions src/AppSettings/VideoNetworkSecuritySettings.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

import QGroundControl
import QGroundControl.Controls
import QGroundControl.FactControls

SettingsGroupLayout {
id: root

Layout.fillWidth: true
heading: qsTr("Network Video Security")

property var _settings: QGroundControl.settingsManager.videoSettings
property bool _usesAuthentication: _settings.networkVideoAuthType.rawValue !== VideoSettings.NetworkVideoAuthNone
property bool _usesBasicAuthentication: (
_settings.networkVideoAuthType.rawValue === VideoSettings.NetworkVideoAuthBasic
)
property string _credentialMessage: ""
property string _configurationError: _settings.networkVideoConfigurationError

QGCPalette {
id: qgcPal
colorGroupEnabled: root.enabled
}

LabelledFactComboBox {
Layout.fillWidth: true
fact: root._settings.networkVideoAuthType
indexModel: false
}

LabelledFactTextField {
Layout.fillWidth: true
textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 40
fact: root._settings.networkVideoUsername
visible: root._usesBasicAuthentication
}

ColumnLayout {
Layout.fillWidth: true
visible: root._usesAuthentication
spacing: ScreenTools.defaultFontPixelHeight / 2

QGCLabel {
Layout.fillWidth: true
text: root._usesBasicAuthentication ? qsTr("Session password") : qsTr("Session bearer token")
}

QGCTextField {
id: sessionSecret
Layout.fillWidth: true
echoMode: TextInput.Password
maximumLength: 4096
placeholderText: root._settings.networkVideoSessionSecretConfigured
? qsTr("Credential configured")
: qsTr("Enter credential")
}

RowLayout {
Layout.fillWidth: true
spacing: ScreenTools.defaultFontPixelWidth

QGCButton {
text: qsTr("Use")
enabled: sessionSecret.text.length > 0
onClicked: {
root._credentialMessage = root._settings.setNetworkVideoSecret(sessionSecret.text)
if (root._credentialMessage.length === 0) {
sessionSecret.clear()
}
}
}

QGCButton {
text: qsTr("Clear")
enabled: root._settings.networkVideoSessionSecretConfigured
onClicked: {
root._settings.clearNetworkVideoSecret()
sessionSecret.clear()
root._credentialMessage = ""
}
}

Item {
Layout.fillWidth: true
}
}
}

LabelledFactBrowse {
Layout.fillWidth: true
fact: root._settings.networkVideoSecretFile
selectFolder: false
showClearButton: true
visible: root._usesAuthentication && root._settings.networkVideoCredentialFileSupported
}

LabelledFactTextField {
Layout.fillWidth: true
textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 40
fact: root._settings.networkVideoOrigin
}

LabelledFactBrowse {
Layout.fillWidth: true
fact: root._settings.networkVideoCaCertificateFile
selectFolder: false
showClearButton: true
visible: !ScreenTools.isMobile
}

QGCLabel {
Layout.fillWidth: true
color: qgcPal.warningText
text: root._credentialMessage.length > 0 ? root._credentialMessage : root._configurationError
visible: text.length > 0
wrapMode: Text.WordWrap
}
}
19 changes: 18 additions & 1 deletion src/AppSettings/pages/Video.SettingsUI.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"autoStreamConfig": "QGroundControl.videoManager.autoStreamConfigured",
"sourceDisabled": "videoSource === QGroundControl.settingsManager.videoSettings.disabledVideoSource",
"isStreamSource": "QGroundControl.videoManager.isStreamSource",
"rtpLatencyVisible": "!QGroundControl.settingsManager.videoSettings.lowLatencyMode.rawValue"
"rtpLatencyVisible": "!QGroundControl.settingsManager.videoSettings.lowLatencyMode.rawValue",
"isHttpMjpeg": "videoSource === QGroundControl.settingsManager.videoSettings.httpMjpegVideoSource",
"isWebsocketJpeg": "videoSource === QGroundControl.settingsManager.videoSettings.websocketJpegVideoSource",
"isNetworkJpeg": "isHttpMjpeg || isWebsocketJpeg"
},
"groups": [
{
Expand Down Expand Up @@ -37,9 +40,23 @@
{
"setting": "videoSettings.udpUrl",
"showWhen": "videoSource === QGroundControl.settingsManager.videoSettings.udp264VideoSource || videoSource === QGroundControl.settingsManager.videoSettings.udp265VideoSource || videoSource === QGroundControl.settingsManager.videoSettings.mpegtsVideoSource"
},
{
"setting": "videoSettings.httpMjpegUrl",
"showWhen": "isHttpMjpeg"
},
{
"setting": "videoSettings.websocketJpegUrl",
"showWhen": "isWebsocketJpeg"
}
]
},
{
"component": "VideoNetworkSecuritySettings",
"sectionName": "Network Video Security",
"keywords": ["authentication", "basic", "bearer", "token", "password", "origin", "tls", "certificate"],
"showWhen": "isNetworkJpeg && !autoStreamConfig"
},
{
"heading": "Settings",
"keywords": ["aspect ratio", "low latency", "decoder", "hardware decode", "disable when disarmed", "gpu", "zero-copy"],
Expand Down
Loading
Loading