Skip to content

Feature/http websocket video streaming - Add HTTP/HTTPS MJPEG and WebSocket Video Streaming Support#13594

Open
alireza787b wants to merge 4 commits intomavlink:masterfrom
alireza787b:feature/http-websocket-video-streaming
Open

Feature/http websocket video streaming - Add HTTP/HTTPS MJPEG and WebSocket Video Streaming Support#13594
alireza787b wants to merge 4 commits intomavlink:masterfrom
alireza787b:feature/http-websocket-video-streaming

Conversation

@alireza787b
Copy link
Copy Markdown

@alireza787b alireza787b commented Oct 25, 2025

This PR implements professional HTTP/HTTPS MJPEG and WebSocket video streaming capabilities for QGroundControl, enabling users to stream video from modern web-based video sources including HTTP servers, WebSocket endpoints, and cloud-based streaming services.

Features

HTTP/HTTPS MJPEG Streaming:

  • GStreamer souphttpsrc-based pipeline for HTTP/HTTPS video sources
  • Configurable network parameters (timeout, retry attempts, buffer size, keep-alive)
  • Custom User-Agent support for server identification
  • Support for both standard HTTP and secure HTTPS connections
  • Compatible with any standard MJPEG-over-HTTP server

WebSocket Video Streaming:

  • Qt6::WebSockets + GStreamer appsrc integration for modern real-time streaming
  • Bidirectional communication protocol supporting quality negotiation and heartbeat
  • Adaptive quality adjustment based on real-time bandwidth estimation (30-frame sliding window)
  • Automatic reconnection with configurable delay and retry logic
  • Thread-safe Qt event loop integration with proper object lifecycle management
  • Low-latency JPEG frame streaming with correct GStreamer timestamping
  • Generic protocol design compatible with various WebSocket video servers

Settings & UI:

  • 14 new video settings with validation and descriptive help text
  • Video display fit options: Fit Width (default), Fit Height, Fill, No Crop
  • Configurable network optimization parameters for diverse network conditions
  • All settings persist automatically via Qt Fact system

Use Cases:

  • Cloud-based drone video streaming services
  • HTTP-based IP cameras and network video sources
  • WebSocket video servers with quality adaptation support
  • Development and testing with local HTTP/WebSocket video sources
  • Remote drone operations with bandwidth-constrained networks

Technical Implementation

Architecture:

  • HTTP Pipeline: souphttpsrc → queue → multipartdemux → jpegdec → [QGC pipeline]
  • WebSocket Pipeline: appsrc (Qt-fed) → jpegdec → [QGC pipeline]

WebSocket Threading Model:

  • QGCWebSocketVideoSource created in GstVideoWorker thread, moved to main thread via moveToThread() for Qt event loop
  • Cross-thread method invocation using QMetaObject::invokeMethod with Qt::QueuedConnection
  • Thread-safe cleanup with deleteLater() to prevent cross-thread deletion crashes

GStreamer Integration:

  • Proper live stream timestamping: GST_CLOCK_TIME_NONE + do-timestamp=TRUE for automatic relative timestamps
  • appsrc configured for live streaming with minimal latency and configurable buffering
  • Correct resource management with reference counting

WebSocket Protocol Design:

  • Generic bidirectional protocol supporting JSON metadata + binary frame data
  • Extensible for future codec support (H.264, H.265, VP8, VP9)
  • Quality negotiation for adaptive streaming
  • Heartbeat/ping mechanism for connection health monitoring
  • Compatible with standard WebSocket video server implementations

Cross-Platform Compatibility:

  • Pure Qt6 and GStreamer APIs (no platform-specific code or headers)
  • CMake platform-specific linking: Windows uses explicit .lib paths, Unix uses library names
  • GStreamer App component added to find_package for Linux/macOS
  • Designed and verified for Windows/Linux/macOS/Android builds

Files Modified

  • CMakeLists.txt - Added WebSockets to Qt6 required components
  • src/Settings/Video.SettingsGroup.json - 14 new settings + videoFit default changed to Fit Width
  • src/Settings/VideoSettings.{h,cc} - Setting facts and validation logic
  • src/VideoManager/VideoManager.cc - Stream source routing for HTTP/WebSocket
  • src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt - GStreamer App component, Qt6::WebSockets linking, gstapp-1.0 library
  • src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.{h,cc} - HTTP and WebSocket pipeline implementation
  • src/UI/AppSettings/VideoSettings.qml - UI controls for new settings

Files Added

  • src/VideoManager/VideoReceiver/GStreamer/QGCWebSocketVideoSource.h - WebSocket video source class header (~130 lines)
  • src/VideoManager/VideoReceiver/GStreamer/QGCWebSocketVideoSource.cc - Implementation (~435 lines)

Commits

  1. cae0663 - Add HTTP MJPEG video streaming support
  2. b453273 - Add WebSocket video streaming support

Future Extensibility

This implementation provides a foundation for future video streaming enhancements:

  • WebRTC support for peer-to-peer video streaming
  • Additional codec support (H.264, H.265, VP8, VP9) over WebSocket
  • Multi-stream support (picture-in-picture)
  • Custom authentication mechanisms for secure video sources

Test Steps

Prerequisites

Option 1: PixEagle Drone Simulator (example test server)

Option 2: Any Standard HTTP MJPEG Server

  • IP cameras with HTTP MJPEG output
  • FFmpeg-based HTTP servers
  • Custom HTTP video streaming services

Option 3: Custom WebSocket Video Server

  • Must send JSON metadata followed by binary JPEG frames
  • Should support optional quality negotiation and heartbeat messages

Build Requirements:

  • GStreamer 1.0 with App component
  • Qt6 with WebSockets module

Test Case 1: HTTP MJPEG Streaming

  1. Launch QGroundControl
  2. Navigate to Settings → General → Video
  3. Set Video Source to "HTTP Video Stream"
  4. Configure HTTP URL (e.g., http://127.0.0.1:5077/video_feed or your HTTP MJPEG source)
  5. Click Apply and Restart Video
  6. Switch to Fly view
  7. Expected Result: Continuous smooth video playback from HTTP source
  8. Test network parameter changes (timeout, buffer size, retry attempts)
  9. Restart QGC and verify settings persisted

Test Case 2: WebSocket Streaming

  1. Navigate to Settings → General → Video
  2. Set Video Source to "WebSocket Video Stream"
  3. Configure WebSocket URL (e.g., ws://127.0.0.1:5077/ws/video_feed or your WebSocket server)
  4. Click Apply and Restart Video
  5. Switch to Fly view
  6. Expected Result: Continuous smooth video playback with adaptive quality
  7. Monitor QGC logs for "WebSocket connected successfully" message

Test Case 3: Video Display Fit Options

  1. While video is streaming, go to Settings → General → Video
  2. Test each Video Display Fit option:
    • Fit Width (default) - Fills width, maintains aspect ratio
    • Fit Height - Fills height, maintains aspect ratio
    • Fill - Fills entire viewport (may crop)
    • No Crop - Shows entire frame
  3. Expected Result: Each mode displays correctly without visual artifacts

Test Case 4: Adaptive Quality (WebSocket Only)

  1. Stream video via WebSocket from a server supporting quality negotiation
  2. Enable debug logging: qgc.videomanager.websocket category
  3. Simulate low bandwidth (network throttling or limit server quality)
  4. Expected Result: Logs show "Reducing quality due to low bandwidth" with quality decrease
  5. Restore normal bandwidth
  6. Expected Result: Logs show "Increasing quality due to high bandwidth" with quality increase

Test Case 5: Automatic Reconnection (WebSocket)

  1. Start WebSocket video streaming
  2. Stop the WebSocket server while QGC is running
  3. Expected Result: Logs show "WebSocket disconnected" and "Waiting to reconnect"
  4. Restart WebSocket server within 10 seconds
  5. Expected Result: Automatic reconnection within configurable delay, video playback resumes

Test Case 6: Thread Safety & Resource Management

  1. Switch between HTTP and WebSocket sources multiple times rapidly
  2. Start and stop video streaming repeatedly (10+ cycles)
  3. Switch to other video sources (UDP, RTSP, TCP) and back
  4. Close and reopen QGC multiple times
  5. Expected Result: No crashes, no threading errors in logs, clean shutdown every time

Test Case 7: Network Video Sources (Real-World)

  1. Connect to HTTP MJPEG IP camera or cloud video service
  2. Configure URL to point to remote HTTP/HTTPS or WebSocket endpoint
  3. Expected Result: Video streams correctly over network
  4. Test with limited network bandwidth
  5. Expected Result: WebSocket adaptive quality reduces bandwidth usage gracefully; HTTP streaming remains stable

Test Case 8: Cross-Platform CI Verification

  1. Wait for GitHub Actions CI to complete all platform builds
  2. Expected Result: Successful builds on:
    • Linux (Ubuntu with GStreamer and Qt6)
    • macOS (with GStreamer framework)
    • Windows (MSVC with GStreamer)
    • Android (if applicable)

Checklist:

  • Review Contribution Guidelines.
  • Review Code of Conduct.
  • I have tested my changes.
  • Code follows QGC coding standards and style guidelines
  • No platform-specific code (cross-platform compatible)
  • Thread-safe Qt integration with proper resource management
  • Settings use Qt Fact system with validation
  • UI is descriptive and user-friendly
  • Production-level error handling and logging
  • Tested with example HTTP/WebSocket video sources on Windows MSVC
  • CMake properly configured for all platforms
  • No memory leaks or threading issues detected
  • Code is documented with appropriate comments

Related Issue

This PR implements WebSocket and HTTP video streaming capabilities for modern network-based video sources. It addresses the need for flexible video streaming options beyond traditional UDP/RTSP/TCP sources, enabling QGroundControl to work with cloud-based services, IP cameras, and modern web-based video streaming protocols.

Note: This is an enhancement/feature addition implementing new functionality. No specific issue ID as this capability was not previously tracked.


Developer Information:
Alireza Ghaderi (@alireza787b)
Contact: p30planets@gmail.com


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@HTRamsey
Copy link
Copy Markdown
Collaborator

Pretty cool, although I think you're missing some more places where the WebSockets lib needs to be added. One example being the Vagrantfile

@alireza787b
Copy link
Copy Markdown
Author

Pretty cool, although I think you're missing some more places where the WebSockets lib needs to be added. One example being the Vagrantfile

Thanks for catching that! You're absolutely right - I've now added qtwebsockets to all the additional Qt installation locations:

  • Vagrantfile (deploy/vagrant/)
  • install-qt-debian.sh
  • install-qt-windows.ps1
  • install-qt-macos.sh

This ensures consistency across all development environments (CI workflows, Vagrant, and manual setups). The changes have been pushed -
let me know if I've missed anywhere else!

@DonLakeFlyer
Copy link
Copy Markdown
Collaborator

How can this be tested without needing to buy some sort of camera that supports this. With other gstreamer based feed we can simulate streams using gstreamer to validate things work.

@alireza787b
Copy link
Copy Markdown
Author

alireza787b commented Oct 25, 2025

How can this be tested without needing to buy some sort of camera that supports this. With other gstreamer based feed we can simulate streams using gstreamer to validate things work.

@DonLakeFlyer, I've included synthetic test servers that follow the ADSB simulator pattern and don't require a camera or video files to run.

How to Test Without a Camera

  1. Navigate to the Test Directory:

    cd test/VideoStreaming
  2. Install Dependencies:

    pip install -r requirements.txt
  3. Run the Test Server:

    Protocol Command Endpoint Notes
    HTTP MJPEG python http_mjpeg_server.py http://127.0.0.1:5077/video_feed Generates a synthetic test pattern.
    WebSocket python websocket_video_server.py ws://127.0.0.1:5077/ws/video_feed Implements the complete QGC/PixEagle WebSocket protocol (metadata, binary data, ping/pong, quality adjustment).

The test servers will generate synthetic test patterns for validation. The project's README also contains GStreamer CLI alternatives if you prefer command-line tools for simulation.


Real-World Testing

For more comprehensive, real-world testing, you can use PixEagle, which works well with webcams, video files, or the simulator sources mentioned above:

➡️ PixEagle Repository

@HTRamsey
Copy link
Copy Markdown
Collaborator

@alireza787b Interesting feature, we'll get back to it when one of us has more time to test it out

@github-actions github-actions bot added the stale label Dec 16, 2025
Add two new video source types to QGroundControl: 1. HTTP MJPEG: Uses GStreamer souphttpsrc + multipartdemux pipeline for standard HTTP MJPEG streams (IP cameras, PixEagle, etc.) 2. WebSocket: Uses GStreamer appsrc with Qt6 QWebSocket for binary JPEG streaming over WebSocket connections (PixEagle ws/video_feed) Key implementation details: - 14 new configurable settings (timeouts, buffer size, keepalive, reconnect delay, heartbeat, adaptive quality parameters) - Settings captured on main thread before GStreamer worker dispatch to avoid cross-thread SettingsManager access - multipartdemux auto-detects boundary from Content-Type header (no hardcoded boundary string) - WebSocket source uses QAtomicInt guard on appsrc access for thread safety between Qt main thread and GStreamer worker - GStreamer App component is OPTIONAL - builds without it lose WebSocket support but HTTP MJPEG still works - Qt6::WebSockets linked only when GStreamer App is available (not a global dependency) - No copyright headers (per QGC coding standards post-Jan 2026) - Includes Python test servers for validation Rebased cleanly on current master with all audit fixes applied. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 14, 2026

Build Results

Platform Status

Platform Status Details
Linux Passed View
Windows Passed View
MacOS Passed View
Android Passed View

All builds passed.

Pre-commit

Check Status Details
pre-commit Failed (non-blocking) View

Pre-commit hooks: 32 passed, 78 failed, 10 skipped.

Test Results

linux-sanitizers: 56 passed, 0 skipped

linux_gcc_64: 56 passed, 0 skipped

Total: 112 passed, 0 skipped

Code Coverage

Coverage: N/A

No baseline available for comparison

Artifact Sizes

Artifact Size
QGroundControl 328.48 MB
QGroundControl 318.75 MB
QGroundControl-aarch64 196.05 MB
QGroundControl-installer-AMD64 165.07 MB
QGroundControl-installer-AMD64-ARM64 76.93 MB
QGroundControl-installer-ARM64 77.89 MB
QGroundControl-mac 183.52 MB
QGroundControl-windows 183.55 MB
QGroundControl-x86_64 187.65 MB

No baseline available for comparison


Updated: 2026-03-06 14:48:20 UTC • Triggered by: MacOS

Use pkg_check_modules directly with IMPORTED_GLOBAL instead of FindGStreamer.cmake component to avoid directory-scoped IMPORTED target visibility issues with Qt autogen targets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions bot removed the stale label Feb 15, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new network-based video sources to QGroundControl’s video pipeline (HTTP/HTTPS MJPEG and WebSocket-fed JPEG via GStreamer appsrc), along with new Video settings and test servers to validate the streams.

Changes:

  • Introduces HTTP MJPEG (souphttpsrc → multipartdemux → jpegparse) and WebSocket (appsrc → jpegdec) source creation in the GStreamer receiver.
  • Adds new Video settings/Facts and updates the Video settings UI to configure HTTP/WebSocket URLs and some connection parameters.
  • Adds Python HTTP MJPEG and WebSocket test servers plus a small README/requirements set.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
test/VideoStreaming/http_mjpeg_server.py Adds a local Flask-based MJPEG test server for manual validation.
test/VideoStreaming/websocket_video_server.py Adds a local WebSocket JPEG-frame test server for manual validation.
test/VideoStreaming/requirements.txt Adds Python dependencies for the test servers.
test/VideoStreaming/README.md Documents how to run the test servers and expected protocol.
src/VideoManager/VideoReceiver/GStreamer/QGCWebSocketVideoSource.h Declares a Qt WebSocket-to-GStreamer appsrc bridge.
src/VideoManager/VideoReceiver/GStreamer/QGCWebSocketVideoSource.cc Implements WebSocket connection/reconnect/heartbeat and pushing frames to appsrc.
src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h Adds HTTP/WebSocket source helpers and stream settings structs.
src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc Routes http(s)/ws(s) URIs to new source builders and captures settings.
src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt Adds optional gstapp + Qt6 WebSockets build wiring for WebSocket support.
src/VideoManager/VideoManager.cc Wires new video sources into URI selection and timeout behavior.
src/UI/AppSettings/VideoSettings.qml Adds URL inputs and basic HTTP/WebSocket settings groups to the UI.
src/Settings/VideoSettings.h Adds new setting Facts and new source string constants.
src/Settings/VideoSettings.cc Registers new sources and setting Facts; adds streamConfigured logic for new URLs.
src/Settings/Video.SettingsGroup.json Adds new HTTP/WebSocket settings metadata entries.
Comment on lines +32 to +44
signals:
void connected();
void disconnected();
void errorOccurred(const QString &error);

private slots:
void _onConnected();
void _onDisconnected();
void _onBinaryMessageReceived(const QByteArray &message);
void _onTextMessageReceived(const QString &message);
void _onError();
void _onSslErrors(const QList<QSslError> &errors);
void _sendHeartbeat();
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QGCWebSocketVideoSource.h uses QSslError/QString/QByteArray/QList in signals/slots but doesn’t include the required headers (e.g., <QtNetwork/QSslError>, <QtCore/QString>, <QtCore/QByteArray>, <QtCore/QList>). This can break compilation depending on include order; add the proper includes (or a complete-type include for QSslError).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added the required includes (QSslError, QString, QByteArray, QList) and sorted them alphabetically per QGC convention.

Comment on lines +231 to +233
GST_BUFFER_PTS(buffer) = gst_util_uint64_scale(_framesReceived, GST_SECOND, 30);
GST_BUFFER_DTS(buffer) = GST_BUFFER_PTS(buffer);
GST_BUFFER_DURATION(buffer) = gst_util_uint64_scale(1, GST_SECOND, 30);
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appsrc is configured with do-timestamp=TRUE, but _pushFrameToAppsrc sets PTS/DTS/duration using a hard-coded 30 FPS. This will produce incorrect timestamps whenever the server FPS differs and can conflict with appsrc’s timestamping. Prefer leaving timestamps unset (GST_CLOCK_TIME_NONE) and letting appsrc timestamp, or compute timestamps from actual arrival time / negotiated FPS.

Suggested change
GST_BUFFER_PTS(buffer) = gst_util_uint64_scale(_framesReceived, GST_SECOND, 30);
GST_BUFFER_DTS(buffer) = GST_BUFFER_PTS(buffer);
GST_BUFFER_DURATION(buffer) = gst_util_uint64_scale(1, GST_SECOND, 30);
// Let appsrc timestamp the buffer (do-timestamp=TRUE) instead of forcing a hard-coded 30 FPS.
GST_BUFFER_PTS(buffer) = GST_CLOCK_TIME_NONE;
GST_BUFFER_DTS(buffer) = GST_CLOCK_TIME_NONE;
GST_BUFFER_DURATION(buffer) = GST_CLOCK_TIME_NONE;
Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Switched to GST_CLOCK_TIME_NONE for PTS/DTS/DURATION and relying on do-timestamp=TRUE to timestamp from the pipeline clock. This is the correct pattern for a live source with variable frame rate.

Comment on lines +127 to +145
void QGCWebSocketVideoSource::_onBinaryMessageReceived(const QByteArray &message)
{
if (message.isEmpty()) {
return;
}

_pushFrameToAppsrc(message);
_expectingBinaryFrame = false;
}

void QGCWebSocketVideoSource::_onTextMessageReceived(const QString &message)
{
// Text messages are JSON metadata from PixEagle-compatible servers
// Format: {"type":"frame","size":N,"quality":Q}
if (message.contains(QStringLiteral("frame"))) {
_expectingBinaryFrame = true;
}
}

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_onTextMessageReceived sets _expectingBinaryFrame but _onBinaryMessageReceived ignores it and always pushes any binary message into jpegdec. Since protocol metadata (including declared size) is effectively ignored (_expectedFrameSize is never used), non-frame binary messages or size mismatches can be fed to the decoder. Consider enforcing the expected state/size before pushing to appsrc.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the dead protocol state (_expectingBinaryFrame, _expectedFrameSize). Replaced with a lightweight JPEG SOI marker check (0xFFD8) on incoming binary messages — protocol-agnostic and prevents non-JPEG data from reaching jpegdec.

Comment on lines +59 to +82
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_check_modules(GSTREAMER_APP QUIET IMPORTED_TARGET gstreamer-app-1.0)
endif()
if(TARGET PkgConfig::GSTREAMER_APP)
set_target_properties(PkgConfig::GSTREAMER_APP PROPERTIES IMPORTED_GLOBAL TRUE)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE PkgConfig::GSTREAMER_APP)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE QGC_GST_APP_AVAILABLE)

target_sources(${CMAKE_PROJECT_NAME}
PRIVATE
QGCWebSocketVideoSource.cc
QGCWebSocketVideoSource.h
)

find_package(Qt6 ${QGC_QT_MINIMUM_VERSION}...${QGC_QT_MAXIMUM_VERSION}
COMPONENTS WebSockets
)
if(TARGET Qt6::WebSockets)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt6::WebSockets)
else()
message(WARNING "Qt6::WebSockets not found - WebSocket video streaming will not be available")
endif()
endif()
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CMake adds QGCWebSocketVideoSource.* and defines QGC_GST_APP_AVAILABLE based only on gstreamer-app being found, but the source unconditionally includes QtWebSockets headers. If gstapp is present but Qt6::WebSockets is not, this will fail to compile/link. Gate adding these sources/defines on BOTH dependencies (gstapp + Qt6::WebSockets), or otherwise disable WebSocket support cleanly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. WebSocket sources and QGC_GST_APP_AVAILABLE are now only added when both gstreamer-app-1.0 and Qt6::WebSockets are found. If either is missing, a status message explains which dependency is unavailable.

Comment on lines +89 to 135
SettingsGroupLayout {
Layout.fillWidth: true
heading: qsTr("HTTP Stream Settings")
visible: _isHTTP

LabelledFactTextField {
Layout.fillWidth: true
label: qsTr("Connection Timeout")
fact: _videoSettings.httpTimeout
}

LabelledFactTextField {
Layout.fillWidth: true
label: qsTr("Retry Attempts")
fact: _videoSettings.httpRetryAttempts
}

FactCheckBoxSlider {
Layout.fillWidth: true
text: qsTr("Keep-Alive")
fact: _videoSettings.httpKeepAlive
}
}

SettingsGroupLayout {
Layout.fillWidth: true
heading: qsTr("WebSocket Stream Settings")
visible: _isWebSocket

LabelledFactTextField {
Layout.fillWidth: true
label: qsTr("Connection Timeout")
fact: _videoSettings.websocketTimeout
}

LabelledFactTextField {
Layout.fillWidth: true
label: qsTr("Reconnect Delay")
fact: _videoSettings.websocketReconnectDelay
}

LabelledFactTextField {
Layout.fillWidth: true
label: qsTr("Heartbeat Interval")
fact: _videoSettings.websocketHeartbeat
}
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VideoSettings.qml exposes only a subset of the newly-added HTTP/WebSocket tuning Facts (e.g., httpBufferSize/httpUserAgent/adaptiveQuality/minQuality/maxQuality/websocketBufferFrames are defined in settings but have no UI controls here). Either add the missing controls or remove/keep them internal to avoid confusing “hidden” settings.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the unused settings (httpBufferSize, httpUserAgent, adaptiveQuality, minQuality, maxQuality, websocketBufferFrames). PixEagle's adaptive quality runs server-side (bandwidth EWMA + encoding time + CPU monitoring via AdaptiveQualityEngine), so no client-side quality controls are needed. Buffer size and user-agent remain as hardcoded struct defaults in the GStreamer receiver. Will re-add as user-facing settings if/when client-initiated quality negotiation is implemented.

Comment on lines +55 to +107
async def video_handler(websocket):
"""Handle a single WebSocket video client."""
print(f"Client connected: {websocket.remote_address}")
frame_number = 0
fps = 30
quality = 85
frame_interval = 1.0 / fps

try:
while True:
start = time.monotonic()

frame = generate_test_frame(640, 480, frame_number, fps)
_, jpeg = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, quality])
jpeg_bytes = jpeg.tobytes()

# Send JSON metadata first
metadata = json.dumps({
"type": "frame",
"size": len(jpeg_bytes),
"quality": quality,
"frame": frame_number,
"timestamp": time.time()
})
await websocket.send(metadata)

# Send binary JPEG frame
await websocket.send(jpeg_bytes)

frame_number += 1

elapsed = time.monotonic() - start
remaining = frame_interval - elapsed
if remaining > 0:
await asyncio.sleep(remaining)

except websockets.exceptions.ConnectionClosed:
print(f"Client disconnected: {websocket.remote_address}")


async def main(port):
print(f"Starting WebSocket video server on ws://0.0.0.0:{port}/ws/video_feed")
async with websockets.serve(video_handler, "0.0.0.0", port):
await asyncio.Future()


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='WebSocket Video Test Server')
parser.add_argument('--port', type=int, default=5078, help='Server port (default: 5078)')
parser.add_argument('--fps', type=int, default=30, help='Frames per second (default: 30)')
args = parser.parse_args()

asyncio.run(main(args.port))
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--fps is accepted on the command line but ignored: video_handler hard-codes fps=30 and main() doesn’t pass args.fps through. This makes the test server behavior not match the documented usage. Thread the selected FPS into the handler (e.g., via a closure/global) and use it for frame_interval and frame generation.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The --fps argument is now threaded through to the handler via functools.partial.

Comment on lines +30 to +33
DEFINE_SETTINGFACT(adaptiveQuality)
DEFINE_SETTINGFACT(minQuality)
DEFINE_SETTINGFACT(maxQuality)
DEFINE_SETTINGFACT(websocketBufferFrames)
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These WebSocket adaptive-quality related Facts are introduced but aren’t referenced anywhere outside settings (no implementation or UI). If adaptive quality isn’t implemented yet, consider removing these Facts until they’re wired up, or add the missing logic/UI so they have an effect.

Suggested change
DEFINE_SETTINGFACT(adaptiveQuality)
DEFINE_SETTINGFACT(minQuality)
DEFINE_SETTINGFACT(maxQuality)
DEFINE_SETTINGFACT(websocketBufferFrames)
Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. Adaptive quality is handled entirely server-side by PixEagle's AdaptiveQualityEngine — no client-side settings needed.

Comment on lines 138 to 144
g_object_set(_pipeline,
"message-forward", TRUE,
nullptr);

_captureStreamSettings();

_source = _makeSource(_uri);
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_captureStreamSettings() reads SettingsManager/Fact values from the GStreamer worker thread (start() runs on the worker when _needDispatch() is true). Settings/Facts are QObjects owned by the main thread, so this is unsafe cross-thread access. Capture these settings on the caller (main) thread and pass plain values into the worker, or fetch them via a blocking queued invoke to the main thread.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. _captureStreamSettings() now executes before _needDispatch() dispatches to the worker thread, ensuring Fact values are read on the main thread. The worker then uses the captured plain-value structs (_httpSettings, _wsSettings) safely.

Comment on lines +34 to +40
{
"name": "httpUrl",
"shortDesc": "HTTP Video URL",
"longDesc": "HTTP/HTTPS URL for MJPEG video stream. Format: http://host:port/path (e.g., http://192.168.1.100:5077/video_feed for PixEagle, or http://camera-ip/mjpeg for IP cameras).",
"type": "string",
"default": ""
},
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says the default “Video Display Fit” is changed to “Fit Width”, but the metadata here still indicates enumValues 0=Fit Width and default is 1 (Fit Height). If the default is intended to be Fit Width, update the default accordingly (and ensure any related code/UI matches).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON default for videoFit was never changed in this PR — it remains 1 (Fit Height) as before. Corrected the PR description to remove the incorrect claim. No code change needed.

@alireza787b
Copy link
Copy Markdown
Author

Addressing all 9 review comments plus additional improvements:

Thread safety:

  • Capture stream settings on main thread before dispatching to GStreamer worker

Build correctness:

  • CMake: gate WebSocket sources on both gstreamer-app-1.0 AND Qt6::WebSockets

A/V correctness:

  • Use GST_CLOCK_TIME_NONE with do-timestamp=TRUE instead of hardcoded 30 FPS timestamps

Code quality:

  • Add missing Qt headers for types used in signals/slots
  • Replace dead protocol state with JPEG SOI marker validation
  • Extract _createAndConnectWebSocket() to eliminate code duplication
  • Fix _wsSource stop/deleteLater race and null pointer after ownership transfer
  • Fix test server --fps passthrough

YAGNI removals:

  • Remove adaptiveQuality, minQuality, maxQuality, websocketBufferFrames, httpBufferSize, httpUserAgent settings — adaptive quality is handled server-side by PixEagle's AdaptiveQualityEngine (bandwidth EWMA + encoding time + CPU monitoring). Buffer size and user-agent kept as hardcoded defaults.

Note: videoFit default was not changed — PR description corrected. WebRTC support intentionally excluded (separate PR — requires ICE/STUN/TURN/SDP signaling, fundamentally different architecture).

- Fix cross-thread safety: capture stream settings on main thread before dispatching to GStreamer worker (_captureStreamSettings reads Fact QObjects owned by main thread) - Fix CMake: gate WebSocket sources on both gstreamer-app-1.0 AND Qt6::WebSockets to prevent build failure when only one is available - Fix timestamps: use GST_CLOCK_TIME_NONE with do-timestamp=TRUE instead of hardcoded 30 FPS (correct pattern for live source with variable FPS) - Add missing Qt headers in QGCWebSocketVideoSource.h (QSslError, QString, QByteArray, QList) - Replace dead protocol state (_expectingBinaryFrame/_expectedFrameSize) with JPEG SOI marker validation (0xFFD8) - Extract _createAndConnectWebSocket() to eliminate duplication between start() and _reconnect() - Fix _wsSource lifecycle: use single queued lambda for stop+deleteLater to prevent race, null pointer after ownership transfer to GStreamer bin - Remove unused settings: adaptiveQuality, minQuality, maxQuality, websocketBufferFrames, httpBufferSize, httpUserAgent (adaptive quality is handled server-side by PixEagle's AdaptiveQualityEngine) - Fix test server --fps passthrough via functools.partial
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment