QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GstVideoReceiver.cc
Go to the documentation of this file.
1//-----------------------------------------------------------------------------
2// Our pipeline look like this:
3//
4// +-->queue-->_decoderValve[-->_decoder-->_videoSink]
5// |
6// _source-->_tee
7// |
8// +-->queue-->_recorderValve[-->_fileSink]
9//-----------------------------------------------------------------------------
10
11#include "GstVideoReceiver.h"
12
14
15#include "GStreamerHelpers.h"
16#include "GstSourceFactory.h"
17#include "QGCLoggingCategory.h"
18#include "QGCNetworkHelper.h"
20
21#include <QtCore/QDateTime>
22#include <QtCore/QMutexLocker>
23#include <QtCore/QUrl>
24#include <QtQuick/QQuickItem>
25
26#include <algorithm>
27
28#include <gst/gst.h>
29#include <gst/video/video.h>
30
31QGC_LOGGING_CATEGORY(GstVideoReceiverLog, "Video.GStreamer.GstVideoReceiver")
32
33namespace {
34// kEosTimeoutNs: bus wait budget for EOS/ERROR during stop(); 3 s covers slow hw decoders.
35constexpr GstClockTime kEosTimeoutNs = 3 * GST_SECOND;
36
37// Refs the element's first src pad into *userData and stops iterating. Resync is handled
38// internally by gst_element_foreach_src_pad (unlike a bare gst_iterator_next loop).
39gboolean grabFirstSrcPad(GstElement * /*element*/, GstPad *pad, gpointer userData)
40{
41 *static_cast<GstPad **>(userData) = GST_PAD(gst_object_ref(pad));
42 return FALSE;
43}
44
45bool isRecoverableH265PaciError(GstMessage *msg, const GError *error, const gchar *debug)
46{
47 if (!msg || !error || (error->domain != GST_STREAM_ERROR) || (error->code != GST_STREAM_ERROR_FORMAT) ||
48 !debug || !g_strrstr(debug, "NAL unit type 50 not supported yet")) {
49 return false;
50 }
51
52 GstObject *src = GST_MESSAGE_SRC(msg);
53 if (!src || !GST_IS_ELEMENT(src)) {
54 return false;
55 }
56
57 GstElementFactory *factory = gst_element_get_factory(GST_ELEMENT(src));
58 return factory && (g_strcmp0(GST_OBJECT_NAME(factory), "rtph265depay") == 0);
59}
60
61const char* recordingParserFactory(const GstCaps* caps)
62{
63 if (!caps || gst_caps_is_any(caps) || gst_caps_is_empty(caps)) {
64 return nullptr;
65 }
66
67 for (guint i = 0; i < gst_caps_get_size(caps); ++i) {
68 const GstStructure* structure = gst_caps_get_structure(caps, i);
69 if (gst_structure_has_name(structure, "video/x-h264")) {
70 return "h264parse";
71 }
72 if (gst_structure_has_name(structure, "video/x-h265")) {
73 return "h265parse";
74 }
75 }
76
77 return nullptr;
78}
79
80} // namespace
81
83 : VideoReceiver(parent)
84 , _worker(new GstVideoWorker(this))
85{
86 qCDebug(GstVideoReceiverLog) << this;
87
88 _worker->start();
89 (void) connect(&_watchdogTimer, &QTimer::timeout, this, &GstVideoReceiver::_watchdog);
90}
91
93{
94 stop();
95 _worker->shutdown();
96
97 qCDebug(GstVideoReceiverLog) << this;
98}
99
100QString GstVideoReceiver::_redactedUri() const
101{
103}
104
105void GstVideoReceiver::start(uint32_t timeout)
106{
107 if (_needDispatch()) {
108 _worker->dispatch([this, timeout]() { start(timeout); });
109 return;
110 }
111
112 if (_pipeline) {
113 qCDebug(GstVideoReceiverLog) << "Already running!" << _redactedUri();
115 return;
116 }
117
118 if (_uri.isEmpty()) {
119 qCDebug(GstVideoReceiverLog) << "Failed because URI is not specified";
121 return;
122 }
123
125 _buffer = lowLatency() ? -1 : 0;
126
127 qCDebug(GstVideoReceiverLog) << "Starting" << _redactedUri() << ", lowLatency" << lowLatency()
128 << ", timeout" << _timeout;
129
130 // GST_DEBUG_BIN_TO_DOT_FILE is a no-op unless GST_DEBUG_DUMP_DOT_DIR is set; surface that
131 // once per process so field debugging doesn't require re-reading the source.
132 [[maybe_unused]] static const bool dotDirHinted = []() {
133 if (qgetenv("GST_DEBUG_DUMP_DOT_DIR").isEmpty()) {
134 qCInfo(GstVideoReceiverLog).noquote()
135 << "Pipeline dot-graph dumps are disabled. Set GST_DEBUG_DUMP_DOT_DIR=/path/to/dir to enable.";
136 }
137 return true;
138 }();
139
140 _endOfStream = false;
141
142 bool running = false;
143 bool pipelineUp = false;
144
145 GstElement *decoderQueue = nullptr;
146 GstElement *recorderQueue = nullptr;
147
148 do {
149 _tee = gst_element_factory_make("tee", nullptr);
150 if (!_tee) {
151 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('tee') failed";
152 break;
153 }
154
155 GstPad *pad = gst_element_get_static_pad(_tee, "sink");
156 if (!pad) {
157 qCCritical(GstVideoReceiverLog) << "gst_element_get_static_pad() failed";
158 break;
159 }
160
162
163 _teeProbeId = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, _teeProbe, this, nullptr);
164 gst_clear_object(&pad);
165 if (_teeProbeId == 0) {
166 // _teeProbe updates _lastSourceFrameTime; without it the watchdog timer fires spuriously instead of reporting a real failure.
167 qCCritical(GstVideoReceiverLog) << "gst_pad_add_probe(_teeProbe) failed";
168 break;
169 }
170
171 decoderQueue = gst_element_factory_make("queue", nullptr);
172 if (!decoderQueue) {
173 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('queue') failed";
174 break;
175 }
176
177 // leaky=downstream (2) + tiny depth: the live-display branch must drop the oldest
178 // buffer on backpressure, not stall the streaming thread. Recording branch (below)
179 // keeps default non-leaky semantics so every frame reaches the muxer.
180 g_object_set(decoderQueue,
181 "leaky", 2,
182 "max-size-buffers", 2,
183 "max-size-bytes", 0,
184 "max-size-time", G_GUINT64_CONSTANT(0),
185 nullptr);
186
187 _decoderValve = gst_element_factory_make("valve", nullptr);
188 if (!_decoderValve) {
189 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('valve') failed";
190 break;
191 }
192
193 g_object_set(_decoderValve,
194 "drop", TRUE,
195 nullptr);
196
197 recorderQueue = gst_element_factory_make("queue", nullptr);
198 if (!recorderQueue) {
199 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('queue') failed";
200 break;
201 }
202
203 _recorderValve = gst_element_factory_make("valve", nullptr);
204 if (!_recorderValve) {
205 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('valve') failed";
206 break;
207 }
208
209 g_object_set(_recorderValve,
210 "drop", TRUE,
211 nullptr);
212 // Preserve stream-start/caps/segment events while buffers are dropped. When recording
213 // starts later, the newly linked parser receives the negotiated codec configuration.
214 gst_util_set_object_arg(G_OBJECT(_recorderValve), "drop-mode", "forward-sticky-events");
215
216 _pipeline = gst_pipeline_new("receiver");
217 if (!_pipeline) {
218 qCCritical(GstVideoReceiverLog) << "gst_pipeline_new() failed";
219 break;
220 }
221
222 g_object_set(_pipeline,
223 "message-forward", TRUE,
224 nullptr);
225
227 sourceConfig.jitterBuffer = (_buffer < 0)
229 : (_buffer == 0
232 sourceConfig.latencyMs = _rtpJitterLatencyMs;
233 // do-retransmission needs ≥40 ms latency headroom over the default 20 ms rtx-delay;
234 // forcibly disable for sub-frame latency configurations to avoid retransmit storms.
236 _source = GStreamer::SourceFactory::create(_uri, sourceConfig);
237 if (!_source) {
238 qCCritical(GstVideoReceiverLog) << "SourceFactory::create() failed";
239 break;
240 }
241
242 gst_bin_add_many(GST_BIN(_pipeline), _source, _tee, decoderQueue, _decoderValve, recorderQueue, _recorderValve, nullptr);
243
244 pipelineUp = true;
245
246 GstPad *srcPad = nullptr;
247 (void) gst_element_foreach_src_pad(_source, grabFirstSrcPad, &srcPad);
248
249 if (srcPad) {
250 _onNewSourcePad(srcPad);
251 gst_clear_object(&srcPad);
252 } else {
253 (void) g_signal_connect(_source, "pad-added", G_CALLBACK(_onNewPad), this);
254 }
255
256 if (!gst_element_link_many(_tee, decoderQueue, _decoderValve, nullptr)) {
257 qCCritical(GstVideoReceiverLog) << "Unable to link decoder queue";
258 break;
259 }
260
261 if (!gst_element_link_many(_tee, recorderQueue, _recorderValve, nullptr)) {
262 qCCritical(GstVideoReceiverLog) << "Unable to link recorder queue";
263 break;
264 }
265
266 GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
267 if (bus) {
268 gst_bus_enable_sync_message_emission(bus);
269 (void) g_signal_connect(bus, "sync-message", G_CALLBACK(_onBusMessage), this);
270 // HwBuffers facade chains every compiled context bridge so they don't clobber each
271 // other via gst_bus_set_sync_handler. Must run before GST_STATE_PLAYING — upstream
272 // queries context during PAUSED→PLAYING. No-op when no bridge-using GPU path is compiled.
273 gst_bus_set_sync_handler(bus, HwBuffers::onBusSyncMessage, nullptr, nullptr);
274 gst_clear_object(&bus);
275 }
276
277 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-initial");
278 running = (gst_element_set_state(_pipeline, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE);
279 } while(0);
280
281 if (!running) {
282 qCCritical(GstVideoReceiverLog) << "Failed";
283
284 if (_pipeline) {
285 (void) gst_element_set_state(_pipeline, GST_STATE_NULL);
286 (void) gst_element_get_state(_pipeline, nullptr, nullptr, GST_CLOCK_TIME_NONE);
287 gst_clear_object(&_pipeline);
288 }
289
290 if (!pipelineUp) {
291 gst_clear_object(&_recorderValve);
292 gst_clear_object(&recorderQueue);
293 gst_clear_object(&_decoderValve);
294 gst_clear_object(&decoderQueue);
295 gst_clear_object(&_tee);
296 gst_clear_object(&_source);
297 }
298
300 } else {
301 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-started");
302 qCDebug(GstVideoReceiverLog) << "Started" << _redactedUri();
303
304 // _watchdogTimer lives on `this` (GUI thread); the emit runs synchronously on the
305 // worker thread, so the timer start has to be queued separately or QObject warns.
306 QMetaObject::invokeMethod(this, [this]() { _watchdogTimer.start(1000); }, Qt::QueuedConnection);
308 }
309}
310
312{
313 if (_needDispatch()) {
314 _worker->dispatch([this]() { stop(); });
315 return;
316 }
317
318 if (_uri.isEmpty()) {
319 qCDebug(GstVideoReceiverLog) << "Stop called on empty URI (no-op)";
320 return;
321 }
322
323 qCDebug(GstVideoReceiverLog) << "Stopping" << _redactedUri();
324
325 // Bump the epoch synchronously (atomic — no GUI thread needed) so any in-flight reconnect lambda
326 // is superseded before this stop() returns; cross-callsite QueuedConnection FIFO is not guaranteed.
327 _reconnectEpoch.fetch_add(1, std::memory_order_relaxed);
328 // Only _watchdogTimer.stop() must run on the GUI thread (the timer lives on `this`).
329 QMetaObject::invokeMethod(this, [this]() { _watchdogTimer.stop(); }, Qt::QueuedConnection);
330
331 if (_teeProbeId != 0) {
332 if (_tee) {
333 GstPad *sinkpad = gst_element_get_static_pad(_tee, "sink");
334 if (sinkpad) {
335 gst_pad_remove_probe(sinkpad, _teeProbeId);
336 gst_clear_object(&sinkpad);
337 }
338 }
339 _teeProbeId = 0;
340 }
341
342 if (_pipeline) {
343 GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
344 if (bus) {
345 gst_bus_disable_sync_message_emission(bus);
346 (void) g_signal_handlers_disconnect_by_data(bus, this);
347
348 gboolean recordingValveClosed = TRUE;
349 g_object_get(_recorderValve, "drop", &recordingValveClosed, nullptr);
350
351 if (!recordingValveClosed) {
352 (void) gst_element_send_event(_pipeline, gst_event_new_eos());
353
354 // Wait for splitmuxsink to actually finalize its current fragment. async-finalize
355 // pushes muxer teardown off the streaming thread; the splitmuxsink-fragment-closed
356 // element message is posted (via message-forward=TRUE) exactly when the muxer's
357 // state has gone NULL. EOS is the fallback for older builds / unexpected paths;
358 // ERROR breaks out so we don't burn the full budget on a known failure. Track
359 // elapsed time so unrelated ELEMENT messages don't abort the wait early.
360 const GstClockTime deadline = kEosTimeoutNs;
361 const qint64 startMs = QDateTime::currentMSecsSinceEpoch();
362 bool finalized = false;
363 for (;;) {
364 const qint64 elapsedNs = (QDateTime::currentMSecsSinceEpoch() - startMs)
365 * qint64(GST_MSECOND);
366 if (elapsedNs >= qint64(deadline)) break;
367 const GstClockTime remaining = GstClockTime(qint64(deadline) - elapsedNs);
368 GstMessage *msg = gst_bus_timed_pop_filtered(bus, remaining,
369 (GstMessageType)(GST_MESSAGE_EOS | GST_MESSAGE_ERROR | GST_MESSAGE_ELEMENT));
370 if (!msg) break;
371 switch (GST_MESSAGE_TYPE(msg)) {
372 case GST_MESSAGE_ELEMENT: {
373 const GstStructure *s = gst_message_get_structure(msg);
374 if (s && gst_structure_has_name(s, "splitmuxsink-fragment-closed")) {
375 qCDebug(GstVideoReceiverLog) << "splitmuxsink fragment finalized";
376 finalized = true;
377 }
378 break;
379 }
380 case GST_MESSAGE_EOS:
381 qCDebug(GstVideoReceiverLog) << "End of stream received (fallback path)";
382 finalized = true;
383 break;
384 case GST_MESSAGE_ERROR:
385 qCCritical(GstVideoReceiverLog) << "Error stopping pipeline!";
386 finalized = true;
387 break;
388 default:
389 break;
390 }
391 gst_clear_message(&msg);
392 if (finalized) break;
393 }
394 if (!finalized) {
395 qCWarning(GstVideoReceiverLog) << "splitmuxsink finalize signal not received within"
396 << (kEosTimeoutNs / GST_MSECOND)
397 << "ms — forcing pipeline NULL (recording may be truncated; "
398 << "faststart + reserved-moov-update-period keep the file playable)";
399 }
400 }
401
402 gst_clear_object(&bus);
403 } else {
404 qCCritical(GstVideoReceiverLog) << "gst_pipeline_get_bus() failed";
405 }
406
407 (void) gst_element_set_state(_pipeline, GST_STATE_NULL);
408 (void) gst_element_get_state(_pipeline, nullptr, nullptr, GST_CLOCK_TIME_NONE);
409
410 // FIXME: check if branch is connected and remove all elements from branch
411 if (_fileSink) {
412 _shutdownRecordingBranch();
413 }
414
415 if (_videoSink) {
416 _shutdownDecodingBranch();
417 }
418
419 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-stopped");
420
421 // Lock before nulling so an in-flight _onBusMessage on the streaming thread cannot read
422 // a half-destroyed _pipeline. _acquirePipelineRef takes its own ref under the same lock.
423 {
424 QMutexLocker lock(&_pipelineMutex);
425 gst_clear_object(&_pipeline);
426 _pipeline = nullptr;
427 }
428
429 _recorderValve = nullptr;
430 _decoderValve = nullptr;
431 _tee = nullptr;
432 _source = nullptr;
433
435
436 if (_streaming) {
437 _streaming = false;
438 qCDebug(GstVideoReceiverLog) << "Streaming stopped" << _redactedUri();
440 } else {
441 qCDebug(GstVideoReceiverLog) << "Streaming did not start" << _redactedUri();
442 }
443 }
444
445 qCDebug(GstVideoReceiverLog) << "Stopped" << _redactedUri();
446
447 if (const HwBuffers::PathStats hwStats = HwBuffers::formatPathStats(true); hwStats.totalDelivered > 0) {
448 qCInfo(GstVideoReceiverLog).noquote()
449 << "HW path stats" << _redactedUri() << hwStats.line + HwBuffers::takeExtraPathStats();
450 }
451
453}
454
456{
457 if (!sink) {
458 qCCritical(GstVideoReceiverLog) << "VideoSink is NULL" << _redactedUri();
459 return;
460 }
461
462 if (_needDispatch()) {
463 _worker->dispatch([this, sink]() mutable { startDecoding(sink); });
464 return;
465 }
466
467 qCDebug(GstVideoReceiverLog) << "Starting decoding" << _redactedUri();
468
469 if (!_widget) {
470 qCDebug(GstVideoReceiverLog) << "Video Widget is NULL" << _redactedUri();
472 return;
473 }
474
475 if (!_pipeline) {
476 gst_clear_object(&_videoSink);
477 }
478
479 if (_videoSink || _decoding) {
480 qCDebug(GstVideoReceiverLog) << "Already decoding!" << _redactedUri();
482 return;
483 }
484
485 GstElement *videoSink = GST_ELEMENT(sink);
486 GstPad *pad = gst_element_get_static_pad(videoSink, "sink");
487 if (!pad) {
488 qCCritical(GstVideoReceiverLog) << "Unable to find sink pad of video sink" << _redactedUri();
490 return;
491 }
492
494 _resetVideoSink = true;
495
496 _videoSinkProbeId = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, _videoSinkProbe, this, nullptr);
497 gst_clear_object(&pad);
498
499 _videoSink = videoSink;
500 gst_object_ref(_videoSink);
501
502 _removingDecoder = false;
503
504 if (!_streaming) {
506 return;
507 }
508
509 _ensureVideoSinkInPipeline();
510
511 if (!_addDecoder(_decoderValve)) {
512 qCCritical(GstVideoReceiverLog) << "_addDecoder() failed" << _redactedUri();
513 _shutdownDecodingBranch();
515 return;
516 }
517
518 g_object_set(_decoderValve,
519 "drop", FALSE,
520 nullptr);
521
522 qCDebug(GstVideoReceiverLog) << "Decoding started" << _redactedUri();
523
525}
526
528{
529 if (_needDispatch()) {
530 _worker->dispatch([this]() { stopDecoding(); });
531 return;
532 }
533
534 qCDebug(GstVideoReceiverLog) << "Stopping decoding" << _redactedUri();
535
536 // Gate on _videoSink (set by startDecoding) instead of _decoding (which only flips on
537 // first sink-buffer probe). Without this, stopDecoding() called between
538 // onStartDecodingComplete(OK) and the first frame returns STATUS_INVALID_STATE and
539 // leaves the decoder/sink branch live.
540 if (!_pipeline || !_videoSink) {
541 qCDebug(GstVideoReceiverLog) << "Not decoding!" << _redactedUri();
543 return;
544 }
545
546 g_object_set(_decoderValve,
547 "drop", TRUE,
548 nullptr);
549
550 _removingDecoder = true;
551
552 const bool ret = _unlinkBranch(_decoderValve);
553
554 // FIXME: it is much better to emit onStopDecodingComplete() after decoding is really stopped
555 // (which happens later due to async design) but as for now it is also not so bad...
557}
558
559void GstVideoReceiver::startRecording(const QString &videoFile, FILE_FORMAT format)
560{
561 if (_needDispatch()) {
562 const QString cachedVideoFile = videoFile;
563 _worker->dispatch([this, cachedVideoFile, format]() { startRecording(cachedVideoFile, format); });
564 return;
565 }
566
567 qCDebug(GstVideoReceiverLog) << "Starting recording" << _redactedUri();
568
569 if (!_pipeline) {
570 qCDebug(GstVideoReceiverLog) << "Streaming is not active!" << _redactedUri();
572 return;
573 }
574
575 if (_recording) {
576 qCDebug(GstVideoReceiverLog) << "Already recording!" << _redactedUri();
578 return;
579 }
580
581 qCDebug(GstVideoReceiverLog) << "New video file:" << videoFile << _redactedUri();
582
583 GstPad* probepad = gst_element_get_static_pad(_recorderValve, "src");
584 if (!probepad) {
585 qCCritical(GstVideoReceiverLog) << "gst_element_get_static_pad() failed" << _redactedUri();
587 return;
588 }
589
590 const auto failRecordingStart = [this, &probepad]() {
591 gst_clear_object(&probepad);
592 if (_fileSink) {
593 (void) gst_element_set_state(_fileSink, GST_STATE_NULL);
594 // Safe before and after a successful link. Removing a still-linked bin leaves the
595 // valve's src pad attached to a finalized peer and breaks every later retry.
596 gst_element_unlink(_recorderValve, _fileSink);
597 GstObject* parent = gst_element_get_parent(_fileSink);
598 if (parent) {
599 (void) gst_bin_remove(GST_BIN(parent), _fileSink);
600 gst_clear_object(&parent);
601 }
602 gst_clear_object(&_fileSink);
603 }
605 };
606
607 // forward-sticky-events preserves the exact negotiated caps while the valve is closed. Fall
608 // back to a proxied caps query during the narrow startup window before negotiation completes.
609 GstCaps* inputCaps = gst_pad_get_current_caps(probepad);
610 if (!inputCaps) {
611 inputCaps = gst_pad_query_caps(probepad, nullptr);
612 }
613 _fileSink = _makeFileSink(videoFile, format, inputCaps);
614 gst_clear_caps(&inputCaps);
615 if (!_fileSink) {
616 qCCritical(GstVideoReceiverLog) << "_makeFileSink() failed" << _redactedUri();
617 failRecordingStart();
618 return;
619 }
620
621 _removingRecorder = false;
622
623 if (!gst_bin_add(GST_BIN(_pipeline), _fileSink)) {
624 qCCritical(GstVideoReceiverLog) << "gst_bin_add(file sink) failed" << _redactedUri();
625 failRecordingStart();
626 return;
627 }
628 (void) gst_object_ref(_fileSink); // Keep a reference in addition to the pipeline's ownership.
629
630 if (!gst_element_link(_recorderValve, _fileSink)) {
631 qCCritical(GstVideoReceiverLog) << "Failed to link valve and file sink" << _redactedUri();
632 failRecordingStart();
633 return;
634 }
635
636 if (!gst_element_sync_state_with_parent(_fileSink)) {
637 qCCritical(GstVideoReceiverLog) << "gst_element_sync_state_with_parent(file sink) failed" << _redactedUri();
638 failRecordingStart();
639 return;
640 }
641
642 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-filesink");
643
644 // Install a probe on the recording branch to drop buffers until we hit our first keyframe
645 // When we hit our first keyframe, we can offset the timestamps appropriately according to the first keyframe time
646 // This will ensure the first frame is a keyframe at t=0, and decoding can begin immediately on playback
647 _keyframeWatchId = gst_pad_add_probe(probepad, GST_PAD_PROBE_TYPE_BUFFER, _keyframeWatch, this, nullptr);
648 if (_keyframeWatchId == 0) {
649 qCCritical(GstVideoReceiverLog) << "gst_pad_add_probe(_keyframeWatch) failed" << _redactedUri();
650 failRecordingStart();
651 return;
652 }
653 gst_clear_object(&probepad);
654
655 g_object_set(_recorderValve,
656 "drop", FALSE,
657 nullptr);
658
659 _recordingOutput = videoFile;
660 _recording = true;
661 qCDebug(GstVideoReceiverLog) << "Recording started" << _redactedUri();
664}
665
667{
668 if (_needDispatch()) {
669 _worker->dispatch([this]() { stopRecording(); });
670 return;
671 }
672
673 qCDebug(GstVideoReceiverLog) << "Stopping recording" << _redactedUri();
674
675 if (!_pipeline || !_recording) {
676 qCDebug(GstVideoReceiverLog) << "Not recording!" << _redactedUri();
678 return;
679 }
680
681 g_object_set(_recorderValve,
682 "drop", TRUE,
683 nullptr);
684
685 _removingRecorder = true;
686
687 if (!_unlinkBranch(_recorderValve)) {
688 _removingRecorder = false;
690 return;
691 }
692
693 // EOS event propagates valve→mux→filesink; _shutdownRecordingBranch emits the
694 // complete signal once the muxer index is written and the file is closed.
695 _recordingStopRequested = true;
696}
697
698void GstVideoReceiver::takeScreenshot(const QString &imageFile)
699{
700 if (_needDispatch()) {
701 const QString cachedImageFile = imageFile;
702 _worker->dispatch([this, cachedImageFile]() { takeScreenshot(cachedImageFile); });
703 return;
704 }
705
706 qCDebug(GstVideoReceiverLog) << "taking screenshot" << _redactedUri();
707
708 // FIXME: record screenshot here
710}
711
712void GstVideoReceiver::_watchdog()
713{
714 _worker->dispatch([this]() {
715 if (!_pipeline) {
716 return;
717 }
718
719 const qint64 now = QDateTime::currentSecsSinceEpoch();
720 qint64 lastSourceFrameTime = _lastSourceFrameTime.load(std::memory_order_relaxed);
721 if (lastSourceFrameTime == 0) {
722 lastSourceFrameTime = now;
723 _lastSourceFrameTime.store(now, std::memory_order_relaxed);
724 }
725
726 if (++_statsTickCounter >= 10) {
728 if (const HwBuffers::PathStats hwStats = HwBuffers::formatPathStats(false); hwStats.totalDelivered > 0) {
729 qCDebug(GstVideoReceiverLog).noquote() << "HW path live" << _redactedUri() << hwStats.line;
730 }
731 }
732
733 // Drain QoS updates accumulated since the last tick (see GST_MESSAGE_QOS).
734 if (_qosStatsDirty.exchange(false, std::memory_order_acq_rel)) {
735 emit decoderStatsChanged();
736 }
737
738 qint64 elapsed = now - lastSourceFrameTime;
739 if (elapsed > _timeout) {
740 qCDebug(GstVideoReceiverLog) << "Stream timeout, no frames for" << elapsed << _redactedUri();
741 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-watchdog-timeout");
742 emit timeout();
743 _scheduleReconnect("source watchdog");
744 return;
745 }
746
747 if (_decoding && !_removingDecoder) {
748 qint64 lastVideoFrameTime = _lastVideoFrameTime.load(std::memory_order_relaxed);
749 if (lastVideoFrameTime == 0) {
750 lastVideoFrameTime = now;
751 _lastVideoFrameTime.store(now, std::memory_order_relaxed);
752 }
753
754 elapsed = now - lastVideoFrameTime;
755 if (elapsed > (_timeout * 2)) {
756 qCDebug(GstVideoReceiverLog) << "Video decoder timeout, no frames for" << elapsed << _redactedUri();
757 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-watchdog-timeout");
758 emit timeout();
759 _scheduleReconnect("decoder watchdog");
760 }
761 }
762 });
763}
764
765void GstVideoReceiver::_scheduleReconnect(const char *reason)
766{
767 // Always tear down — even when autoReconnect is off we still want a clean stop.
768 // stop() bumps _reconnectEpoch, so any prior singleShot lambda becomes a no-op.
769 stop();
770
771 if (!_autoReconnect) {
772 qCDebug(GstVideoReceiverLog) << "Auto-reconnect disabled — not retrying after" << reason;
773 return;
774 }
775
776 if (_uri.isEmpty()) {
777 return;
778 }
779
780 // Snapshot on the worker thread — where start() last wrote _timeout and where _uri reads
781 // are already sequenced — so the GUI-thread lambdas below don't read racy members.
782 const uint32_t reconnectTimeout = (_timeout != 0) ? _timeout : 8;
783 const QString uri = _uri;
784
785 // Schedule on the GUI thread (QTimer::singleShot requires its receiver's thread). Worker
786 // is the only caller today, but route through invokeMethod so a future direct GUI-thread
787 // call (e.g. user-initiated retry) stays correct.
788 QMetaObject::invokeMethod(this, [this, reason, reconnectTimeout, uri]() {
789 const int next = std::min(_reconnectAttempts.load(std::memory_order_relaxed) + 1, 30);
790 _reconnectAttempts.store(next, std::memory_order_relaxed);
791 // 1s → 2s → 4s → 8s → 16s, capped at 30s. Capping bounds worst-case "vehicle in flight,
792 // RF down for 5 min" recovery; lower than typical RTSP server keepalive (60s).
793 const int delaySec = std::min(1 << std::min(next - 1, 5), 30);
794 const quint64 epoch = _reconnectEpoch.load(std::memory_order_relaxed);
795 const int attempts = next;
796 qCInfo(GstVideoReceiverLog) << "Scheduling reconnect #" << attempts
797 << "in" << delaySec << "s after" << reason
799 QTimer::singleShot(delaySec * 1000, this, [this, epoch, attempts, reconnectTimeout, uri]() {
800 if (epoch != _reconnectEpoch.load(std::memory_order_relaxed)) return; // superseded by stop()
801 // _pipeline is mutated by the worker under _pipelineMutex; a bare deref here (GUI
802 // thread) races teardown, so probe liveness through the mutex-guarded accessor.
803 GstElement *livePipeline = _acquirePipelineRef();
804 const bool pipelineUp = (livePipeline != nullptr);
805 if (livePipeline) gst_object_unref(livePipeline);
806 if (uri.isEmpty() || pipelineUp) return; // pipeline already came back
807 qCInfo(GstVideoReceiverLog) << "Reconnecting (attempt" << attempts << ")"
809 start(reconnectTimeout);
810 });
811 }, Qt::QueuedConnection);
812}
813
815{
816 _worker->dispatch([this, tag]() {
817 GstElement *pipelineRef = _acquirePipelineRef();
818 if (!pipelineRef) {
819 qCDebug(GstVideoReceiverLog) << "dumpPipelineGraph: pipeline not running";
820 return;
821 }
822 const QByteArray tagUtf8 = tag.toUtf8();
823 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipelineRef), GST_DEBUG_GRAPH_SHOW_ALL, tagUtf8.constData());
824 const QString dotPath = GStreamer::writePipelineDot(pipelineRef, tagUtf8.constData());
825 if (!dotPath.isEmpty()) {
826 qCInfo(GstVideoReceiverLog) << "Pipeline graph saved to" << dotPath;
827 }
828 gst_object_unref(pipelineRef);
829 });
830}
831
832void GstVideoReceiver::_handleEOS()
833{
834 if (!_pipeline) {
835 return;
836 }
837
838 if (_endOfStream) {
839 stop();
840 } else if (_decoding && _removingDecoder) {
841 _shutdownDecodingBranch();
842 } else if (_recording && _removingRecorder) {
843 _shutdownRecordingBranch();
844 } /*else {
845 qCWarning(GstVideoReceiverLog) << "Unexpected EOS!";
846 stop();
847 }*/
848}
849
850GstElement *GstVideoReceiver::_makeDecoder()
851{
852 GstElement *decoder = gst_element_factory_make("decodebin3", nullptr);
853 if (!decoder) {
854 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('decodebin3') failed";
855 }
856 return decoder;
857}
858
859GstElement* GstVideoReceiver::_makeFileSink(const QString& videoFile, FILE_FORMAT format, const GstCaps* inputCaps)
860{
861 GstElement *fileSink = nullptr;
862 GstElement *splitmux = nullptr;
863 GstElement* parser = nullptr;
864 GstElement *bin = nullptr;
865 GstPad *videopad = nullptr;
866 GstPad* parserSrcPad = nullptr;
867 GstPad* parserSinkPad = nullptr;
868 GstPad *ghostpad = nullptr;
869
870 do {
871 if (!isValidFileFormat(format)) {
872 qCCritical(GstVideoReceiverLog) << "Unsupported file format";
873 break;
874 }
875
876 const char* parserFactory = recordingParserFactory(inputCaps);
877 if (parserFactory) {
878 parser = gst_element_factory_make(parserFactory, nullptr);
879 if (!parser) {
880 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make() failed for" << parserFactory;
881 break;
882 }
883 g_object_set(parser, "config-interval", -1, nullptr);
884 }
885
886 // splitmuxsink owns its own muxer + filesink internally, handles request-pad
887 // lifetime, and finalizes asynchronously so EOS no longer wedges the worker
888 // thread (replaces the manual qtmux/matroskamux+filesink combo + "stuck muxer"
889 // bounded-wait in stop()). max-size-time=0 keeps single-file behaviour.
890 splitmux = gst_element_factory_make("splitmuxsink", nullptr);
891 if (!splitmux) {
892 qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('splitmuxsink') failed";
893 break;
894 }
895
896 g_object_set(splitmux,
897 "location", qPrintable(videoFile),
898 "muxer-factory", _kFileMux[format],
899 "max-size-time", G_GUINT64_CONSTANT(0),
900 "max-size-bytes", G_GUINT64_CONSTANT(0),
901 "async-finalize", TRUE,
902 // Surface "splitmuxsink-fragment-closed" element messages on the pipeline bus so
903 // stop() can wait on the precise per-fragment finalize signal instead of EOS
904 // (gstsplitmuxsink.c:send_fragment_opened_closed_msg posts this per fragment,
905 // including the final fragment torn down on EOS).
906 "message-forward", TRUE,
907 nullptr);
908
909 // Crash-safe MP4/MOV: faststart writes moov up-front; reserved-moov-update-period
910 // refreshes the moov on a 1 s cadence so an abrupt kill still leaves a playable file.
911 // matroskamux is naturally streamable; skip the GstStructure dance.
912 if (format == FILE_FORMAT_MP4 || format == FILE_FORMAT_MOV) {
913 GstStructure *muxerProps = gst_structure_new("properties",
914 "faststart", G_TYPE_BOOLEAN, TRUE,
915 "reserved-moov-update-period", G_TYPE_UINT64, G_GUINT64_CONSTANT(1000000000),
916 nullptr);
917 g_object_set(splitmux, "muxer-properties", muxerProps, nullptr);
918 gst_structure_free(muxerProps);
919 }
920
921 bin = gst_bin_new("sinkbin");
922 if (!bin) {
923 qCCritical(GstVideoReceiverLog) << "gst_bin_new('sinkbin') failed";
924 break;
925 }
926
927 // splitmuxsink's video sink pad is a request pad — request once during construction
928 // and ghost it as "sink" so the existing recorderValve→fileSink link works unchanged.
929 // request_pad_simple (1.20+) does the pad-template lookup internally.
930 videopad = gst_element_request_pad_simple(splitmux, "video");
931 if (!videopad) {
932 qCCritical(GstVideoReceiverLog) << "gst_element_request_pad_simple(splitmuxsink, video) failed";
933 break;
934 }
935
936 GstElement* binParser = nullptr;
937 if (parser) {
938 if (!gst_bin_add(GST_BIN(bin), parser)) {
939 qCCritical(GstVideoReceiverLog) << "gst_bin_add(parser) failed";
940 break;
941 }
942 binParser = parser;
943 parser = nullptr; // bin now owns it
944 }
945
946 if (!gst_bin_add(GST_BIN(bin), splitmux)) {
947 qCCritical(GstVideoReceiverLog) << "gst_bin_add(splitmuxsink) failed";
948 break;
949 }
950 splitmux = nullptr; // bin now owns it
951
952 GstPad* ghostTarget = videopad;
953 if (binParser) {
954 parserSrcPad = gst_element_get_static_pad(binParser, "src");
955 parserSinkPad = gst_element_get_static_pad(binParser, "sink");
956 if (!parserSrcPad || !parserSinkPad) {
957 qCCritical(GstVideoReceiverLog) << "gst_element_get_static_pad(parser) failed";
958 break;
959 }
960
961 const GstPadLinkReturn linkResult = gst_pad_link(parserSrcPad, videopad);
962 if (linkResult != GST_PAD_LINK_OK) {
963 qCCritical(GstVideoReceiverLog)
964 << "Failed to link parser and splitmuxsink:" << gst_pad_link_get_name(linkResult);
965 break;
966 }
967 ghostTarget = parserSinkPad;
968 }
969
970 ghostpad = gst_ghost_pad_new("sink", ghostTarget);
971 if (!ghostpad) {
972 qCCritical(GstVideoReceiverLog) << "gst_ghost_pad_new() failed";
973 break;
974 }
975
976 if (!gst_element_add_pad(bin, ghostpad)) {
977 qCCritical(GstVideoReceiverLog) << "gst_element_add_pad(ghost) failed";
978 break;
979 }
980 ghostpad = nullptr; // bin now owns it
981
982 fileSink = bin;
983 bin = nullptr;
984 } while(0);
985
986 gst_clear_object(&ghostpad);
987 gst_clear_object(&parserSinkPad);
988 gst_clear_object(&parserSrcPad);
989 // No release_request_pad: on success splitmux is already NULL (owned by bin), and on failure the
990 // bin/splitmux unref below finalizes splitmuxsink, which reclaims its "video" request pad itself.
991 gst_clear_object(&videopad);
992 gst_clear_object(&splitmux);
993 gst_clear_object(&parser);
994 gst_clear_object(&bin);
995 return fileSink;
996}
997
998void GstVideoReceiver::_onNewSourcePad(GstPad *pad)
999{
1000 // FIXME: check for caps - if this is not video stream (and preferably - one of these which we have to support) then simply skip it
1001 if (!gst_element_link(_source, _tee)) {
1002 qCCritical(GstVideoReceiverLog) << "Unable to link source";
1003 return;
1004 }
1005
1006 if (!_streaming) {
1007 _streaming = true;
1008 qCDebug(GstVideoReceiverLog) << "Streaming started" << _redactedUri();
1010 }
1011
1012 _eosProbeId = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, _eosProbe, this, nullptr);
1013 if (_eosProbeId != 0) {
1014 // Hold a ref so _shutdownDecodingBranch can remove the probe even after _decoder is gone.
1015 _eosProbePad = GST_PAD_CAST(gst_object_ref(pad));
1016 }
1017 if (!_videoSink) {
1018 return;
1019 }
1020
1021 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-new-source-pad");
1022
1023 _ensureVideoSinkInPipeline();
1024
1025 if (!_addDecoder(_decoderValve)) {
1026 qCCritical(GstVideoReceiverLog) << "_addDecoder() failed";
1027 _shutdownDecodingBranch();
1028 return;
1029 }
1030
1031 g_object_set(_decoderValve,
1032 "drop", FALSE,
1033 nullptr);
1034
1035 qCDebug(GstVideoReceiverLog) << "Decoding started" << _redactedUri();
1036}
1037
1038void GstVideoReceiver::_logDecodebin3SelectedCodec(GstElement *decodebin3)
1039{
1040 GValue value = G_VALUE_INIT;
1041 GstIterator *iter = gst_bin_iterate_elements(GST_BIN(decodebin3));
1042 GstElement *child;
1043
1044 while (gst_iterator_next(iter, &value) == GST_ITERATOR_OK) {
1045 child = GST_ELEMENT(g_value_get_object(&value));
1046 GstElementFactory *factory = gst_element_get_factory(child);
1047
1048 if (factory) {
1049 gboolean is_decoder = gst_element_factory_list_is_type(factory, GST_ELEMENT_FACTORY_TYPE_DECODER);
1050 if (is_decoder) {
1051 const gchar *decoderKlass = gst_element_factory_get_klass(factory);
1052 GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory);
1053 const gchar *featureName = gst_plugin_feature_get_name(feature);
1054 const guint rank = gst_plugin_feature_get_rank(feature);
1055 bool isHardwareDecoder = GStreamer::isHardwareDecoderFactory(factory);
1056
1057 QString pluginName = featureName;
1058 GstPlugin *plugin = gst_plugin_feature_get_plugin(feature);
1059 if (plugin) {
1060 pluginName = gst_plugin_get_name(plugin);
1061 gst_object_unref(plugin);
1062 }
1063 qCDebug(GstVideoReceiverLog) << "Decodebin3 selected codec:rank -" << pluginName << "/" << featureName << "-" << decoderKlass << (isHardwareDecoder ? "(HW)" : "(SW)") << ":" << rank;
1064
1065 const QString newName = QString::fromUtf8(featureName);
1066 bool nameChanged = false;
1067 {
1068 QMutexLocker locker(&_decoderNameMutex);
1069 if (newName != _decoderName) {
1070 _decoderName = newName;
1071 nameChanged = true;
1072 }
1073 }
1074 if (nameChanged) {
1075 emit decoderStatsChanged();
1076 }
1077
1078 // Disable QoS on the internal decoder to prevent cascading
1079 // frame drops on live streams. The videodecoder base class
1080 // aggressively advances earliest_time after the first late
1081 // frame, causing all subsequent frames to be dropped.
1082 g_object_set(child, "qos", FALSE, nullptr);
1083 qCDebug(GstVideoReceiverLog) << "Disabled QoS on internal decoder" << featureName;
1084 }
1085 }
1086 g_value_reset(&value);
1087 }
1088 g_value_unset(&value);
1089 gst_iterator_free(iter);
1090}
1091
1092
1093void GstVideoReceiver::_onNewDecoderPad(GstPad *pad)
1094{
1095 qCDebug(GstVideoReceiverLog) << "_onNewDecoderPad" << _redactedUri();
1096
1097 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-new-decoder-pad");
1098
1099 // We should now know what codec decodebin3 selected.
1100 _logDecodebin3SelectedCodec(_decoder);
1101
1102 if (!_addVideoSink(pad)) {
1103 qCCritical(GstVideoReceiverLog) << "_addVideoSink() failed";
1104 }
1105}
1106
1107bool GstVideoReceiver::_addDecoder(GstElement *src)
1108{
1109 _decoder = _makeDecoder();
1110 if (!_decoder) {
1111 qCCritical(GstVideoReceiverLog) << "_makeDecoder() failed";
1112 return false;
1113 }
1114
1115 (void) gst_object_ref(_decoder);
1116
1117 (void) gst_bin_add(GST_BIN(_pipeline), _decoder);
1118 (void) gst_element_sync_state_with_parent(_decoder);
1119
1120 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-decoder");
1121
1122 if (!gst_element_link(src, _decoder)) {
1123 qCCritical(GstVideoReceiverLog) << "Unable to link decoder";
1124 gst_element_set_state(_decoder, GST_STATE_NULL);
1125 (void) gst_element_get_state(_decoder, nullptr, nullptr, GST_CLOCK_TIME_NONE);
1126 (void) gst_bin_remove(GST_BIN(_pipeline), _decoder);
1127 gst_clear_object(&_decoder);
1128 return false;
1129 }
1130
1131 GstPad *srcPad = nullptr;
1132 (void) gst_element_foreach_src_pad(_decoder, grabFirstSrcPad, &srcPad);
1133
1134 if (srcPad) {
1135 _onNewDecoderPad(srcPad);
1136 } else {
1137 (void) g_signal_connect(_decoder, "pad-added", G_CALLBACK(_onNewPad), this);
1138 }
1139
1140 gst_clear_object(&srcPad);
1141 return true;
1142}
1143
1144void GstVideoReceiver::_ensureVideoSinkInPipeline()
1145{
1146 if (!_videoSink || !_pipeline) {
1147 return;
1148 }
1149
1150 GstObject *parent = gst_element_get_parent(_videoSink);
1151 if (parent) {
1152 gst_object_unref(parent);
1153 return;
1154 }
1155
1156 g_object_set(_videoSink,
1157 "sync", (_buffer >= 0),
1158 NULL);
1159
1160 (void) gst_object_ref(_videoSink);
1161 (void) gst_bin_add(GST_BIN(_pipeline), _videoSink);
1162
1163 // PAUSED (not READY) triggers downstream caps negotiation before source data arrives.
1164 (void) gst_element_set_state(_videoSink, GST_STATE_PAUSED);
1165}
1166
1167bool GstVideoReceiver::_addVideoSink(GstPad *pad)
1168{
1169 GstCaps *caps = gst_pad_query_caps(pad, nullptr);
1170
1171 _ensureVideoSinkInPipeline();
1172
1173 GstPad *sinkPad = gst_element_get_static_pad(_videoSink, "sink");
1174 GstPadLinkReturn linkRet = sinkPad ? gst_pad_link(pad, sinkPad) : GST_PAD_LINK_WRONG_HIERARCHY;
1175 if (linkRet != GST_PAD_LINK_OK) {
1176 qCCritical(GstVideoReceiverLog) << "Unable to link decoder pad to video sink, result:" << linkRet;
1177
1178 // _ensureVideoSinkInPipeline() added it before linking; detach for the next retry.
1179 GstObject *parent = gst_element_get_parent(_videoSink);
1180 if (parent) {
1181 (void) gst_element_set_state(_videoSink, GST_STATE_NULL);
1182 (void) gst_element_get_state(_videoSink, nullptr, nullptr, GST_CLOCK_TIME_NONE);
1183 (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink);
1184 gst_clear_object(&parent);
1185 }
1186
1187 gst_clear_object(&sinkPad);
1188 gst_clear_caps(&caps);
1189 return false;
1190 }
1191 gst_clear_object(&sinkPad);
1192
1193 (void) gst_element_sync_state_with_parent(_videoSink);
1194
1195 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-videosink");
1196
1197 // Determine video size. Errors here are non-fatal.
1198 QSize videoSize;
1199 do {
1200 if (!_decoderValve) {
1201 qCCritical(GstVideoReceiverLog) << "Unable to determine video size - _decoderValve is NULL"
1202 << _redactedUri();
1203 break;
1204 }
1205
1206 GstPad *valveSrcPad = gst_element_get_static_pad(_decoderValve, "src");
1207 if (!valveSrcPad) {
1208 qCCritical(GstVideoReceiverLog) << "gst_element_get_static_pad() failed";
1209 break;
1210 }
1211
1212 GstCaps *valveSrcPadCaps = gst_pad_query_caps(valveSrcPad, nullptr);
1213 if (!valveSrcPadCaps) {
1214 qCCritical(GstVideoReceiverLog) << "gst_pad_query_caps() failed";
1215 gst_clear_object(&valveSrcPad);
1216 break;
1217 }
1218
1219 const GstStructure *structure = gst_caps_get_structure(valveSrcPadCaps, 0);
1220 if (!structure) {
1221 qCCritical(GstVideoReceiverLog) << "Unable to determine video size - structure is NULL"
1222 << _redactedUri();
1223 gst_clear_object(&valveSrcPad);
1224 break;
1225 }
1226
1227 gint width = 0;
1228 gint height = 0;
1229 (void) gst_structure_get_int(structure, "width", &width);
1230 (void) gst_structure_get_int(structure, "height", &height);
1231
1232 // Swap W×H for 90°/270° streams so QML AR is computed on display dimensions.
1233 gint orientation = 0;
1234 if (gst_structure_get_int(structure, "video-orientation", &orientation)
1235 && (orientation == GST_VIDEO_ORIENTATION_90R
1236 || orientation == GST_VIDEO_ORIENTATION_90L
1237 || orientation == GST_VIDEO_ORIENTATION_UL_LR
1238 || orientation == GST_VIDEO_ORIENTATION_UR_LL)) {
1239 videoSize.setWidth(height);
1240 videoSize.setHeight(width);
1241 } else {
1242 videoSize.setWidth(width);
1243 videoSize.setHeight(height);
1244 }
1245
1246 gst_clear_caps(&valveSrcPadCaps);
1247 gst_clear_object(&valveSrcPad);
1248 } while (false);
1249 emit videoSizeChanged(videoSize);
1250
1251 gst_clear_caps(&caps);
1252 return true;
1253}
1254
1255void GstVideoReceiver::_noteTeeFrame()
1256{
1257 _lastSourceFrameTime.store(QDateTime::currentSecsSinceEpoch(), std::memory_order_relaxed);
1258 // Successful frame arrival: drop the reconnect backoff so the next failure starts at 1 s,
1259 // not minutes-into-the-curve. This probe runs on the streaming thread while the backoff
1260 // increment runs on the GUI thread; post the reset there too so all mutation of
1261 // _reconnectAttempts is single-threaded and the increment can't clobber the reset.
1262 if (_reconnectAttempts.load(std::memory_order_relaxed) != 0) {
1263 QMetaObject::invokeMethod(
1264 this, [this]() { _reconnectAttempts.store(0, std::memory_order_relaxed); }, Qt::QueuedConnection);
1265 }
1266 const quint64 sourceFrames = _sourceFrameCount.fetch_add(1, std::memory_order_relaxed) + 1;
1267 if (sourceFrames == 1) {
1268 qCInfo(GstVideoReceiverLog).noquote() << "Source receiving frames (tee):" << _redactedUri();
1269 } else if ((sourceFrames % 300) == 0) {
1270 qCDebug(GstVideoReceiverLog).noquote()
1271 << "Source flow: teeFrames=" << sourceFrames << "decoding=" << _decoding << _redactedUri();
1272 }
1273}
1274
1275void GstVideoReceiver::_noteVideoSinkFrame()
1276{
1277 _lastVideoFrameTime.store(QDateTime::currentSecsSinceEpoch(), std::memory_order_relaxed);
1278 if (!_decoding) {
1279 _decoding = true;
1280 qCDebug(GstVideoReceiverLog) << "Decoding started";
1282 }
1283}
1284
1285void GstVideoReceiver::_noteEndOfStream()
1286{
1287 _endOfStream = true;
1288}
1289
1290bool GstVideoReceiver::_unlinkBranch(GstElement *from)
1291{
1292 GstPad *src = gst_element_get_static_pad(from, "src");
1293 if (!src) {
1294 qCCritical(GstVideoReceiverLog) << "gst_element_get_static_pad() failed";
1295 return false;
1296 }
1297
1298 GstPad *sink = gst_pad_get_peer(src);
1299 if (!sink) {
1300 gst_clear_object(&src);
1301 qCCritical(GstVideoReceiverLog) << "gst_pad_get_peer() failed";
1302 return false;
1303 }
1304
1305 if (!gst_pad_unlink(src, sink)) {
1306 gst_clear_object(&src);
1307 gst_clear_object(&sink);
1308 qCCritical(GstVideoReceiverLog) << "gst_pad_unlink() failed";
1309 return false;
1310 }
1311
1312 gst_clear_object(&src);
1313
1314 // Send EOS at the beginning of the branch
1315 const gboolean ret = gst_pad_send_event(sink, gst_event_new_eos());
1316
1317 gst_clear_object(&sink);
1318
1319 if (!ret) {
1320 qCCritical(GstVideoReceiverLog) << "Branch EOS was NOT sent";
1321 return false;
1322 }
1323
1324 qCDebug(GstVideoReceiverLog) << "Branch EOS was sent";
1325
1326 return true;
1327}
1328
1329void GstVideoReceiver::_shutdownDecodingBranch()
1330{
1331 if (_decoder) {
1332 GstObject *parent = gst_element_get_parent(_decoder);
1333 if (parent) {
1334 (void) gst_bin_remove(GST_BIN(_pipeline), _decoder);
1335 (void) gst_element_set_state(_decoder, GST_STATE_NULL);
1336 (void) gst_element_get_state(_decoder, nullptr, nullptr, GST_CLOCK_TIME_NONE);
1337 gst_clear_object(&parent);
1338 }
1339
1340 gst_clear_object(&_decoder);
1341 }
1342
1343 if (_videoSinkProbeId != 0 && _videoSink) {
1344 GstPad *sinkpad = gst_element_get_static_pad(_videoSink, "sink");
1345 if (sinkpad) {
1346 gst_pad_remove_probe(sinkpad, _videoSinkProbeId);
1347 gst_clear_object(&sinkpad);
1348 }
1349 }
1350 _videoSinkProbeId = 0;
1351
1352 if (_eosProbeId != 0 && _eosProbePad) {
1353 // Probe was installed on the source pad in _onNewSourcePad; remove from that exact pad — not from _decoder, which may already be cleared above.
1354 gst_pad_remove_probe(_eosProbePad, _eosProbeId);
1355 }
1356 _eosProbeId = 0;
1357 gst_clear_object(&_eosProbePad);
1358
1360
1361 if (_videoSink) {
1362 GstObject *parent = gst_element_get_parent(_videoSink);
1363 if (parent) {
1364 (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink);
1365 (void) gst_element_set_state(_videoSink, GST_STATE_NULL);
1366 (void) gst_element_get_state(_videoSink, nullptr, nullptr, GST_CLOCK_TIME_NONE);
1367 gst_clear_object(&parent);
1368 }
1369 gst_clear_object(&_videoSink);
1370 }
1371
1372 _removingDecoder = false;
1373
1374 if (_decoding) {
1375 _decoding = false;
1376 qCDebug(GstVideoReceiverLog) << "Decoding stopped";
1378 }
1379
1380 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-decoding-stopped");
1381}
1382
1383void GstVideoReceiver::_shutdownRecordingBranch()
1384{
1385 if (_keyframeWatchId != 0 && _recorderValve) {
1386 GstPad *probepad = gst_element_get_static_pad(_recorderValve, "src");
1387 if (probepad) {
1388 gst_pad_remove_probe(probepad, _keyframeWatchId);
1389 gst_clear_object(&probepad);
1390 }
1391 _keyframeWatchId = 0;
1392 }
1393
1394 gst_bin_remove(GST_BIN(_pipeline), _fileSink);
1395 gst_element_set_state(_fileSink, GST_STATE_NULL);
1396 (void) gst_element_get_state(_fileSink, nullptr, nullptr, GST_CLOCK_TIME_NONE);
1397 gst_clear_object(&_fileSink);
1398
1399 _removingRecorder = false;
1400
1401 if (_recording) {
1402 _recording = false;
1403 qCDebug(GstVideoReceiverLog) << "Recording stopped";
1405 }
1406
1407 if (_recordingStopRequested) {
1408 _recordingStopRequested = false;
1410 }
1411
1412 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-recording-stopped");
1413}
1414
1415bool GstVideoReceiver::_needDispatch()
1416{
1417 return _worker->needDispatch();
1418}
1419
1420GstElement *GstVideoReceiver::_acquirePipelineRef() const
1421{
1422 QMutexLocker lock(&_pipelineMutex);
1423 if (!_pipeline) return nullptr;
1424 return GST_ELEMENT(gst_object_ref(_pipeline));
1425}
1426
1427gboolean GstVideoReceiver::_onBusMessage(GstBus * /* bus */, GstMessage *msg, gpointer data)
1428{
1429 if (!msg || !data) {
1430 qCCritical(GstVideoReceiverLog) << "Invalid parameters in _onBusMessage: msg=" << msg << "data=" << data;
1431 return TRUE;
1432 }
1433
1434 GstVideoReceiver *pThis = static_cast<GstVideoReceiver*>(data);
1435
1436 if (GST_MESSAGE_TYPE(msg) != GST_MESSAGE_ERROR) {
1438 }
1439
1440 switch (GST_MESSAGE_TYPE(msg)) {
1441 case GST_MESSAGE_ERROR: {
1442 gchar *debug = nullptr;
1443 GError *error = nullptr;
1444 gst_message_parse_error(msg, &error, &debug);
1445 const bool recoverableH265PaciError = isRecoverableH265PaciError(msg, error, debug);
1446
1447 if (debug) {
1448 qCDebug(GstVideoReceiverLog) << "GStreamer debug:" << debug;
1449 g_clear_pointer(&debug, g_free);
1450 }
1451
1452 if (error) {
1453 if (recoverableH265PaciError) {
1454 qCWarning(GstVideoReceiverLog)
1455 << "Ignoring unsupported H.265 RTP PACI packet from rtph265depay:" << error->message;
1456 } else {
1457 qCCritical(GstVideoReceiverLog) << "GStreamer error:" << error->message;
1458 }
1459 g_clear_error(&error);
1460 }
1461
1462 if (recoverableH265PaciError) {
1463 break;
1464 }
1465
1467
1468 if (GstElement *pipelineRef = pThis->_acquirePipelineRef()) {
1469 // Native dump path (no-op without GST_DEBUG_DUMP_DOT_DIR) plus an unconditional
1470 // CacheLocation fallback so field-bug-report bundles include pipeline topology.
1471 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipelineRef), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-error");
1472 const QString dotPath = GStreamer::writePipelineDot(pipelineRef, "pipeline-error");
1473 if (!dotPath.isEmpty()) {
1474 qCInfo(GstVideoReceiverLog) << "Pipeline graph saved to" << dotPath;
1475 }
1476 gst_object_unref(pipelineRef);
1477 }
1478
1479 // GPU-side ERROR handling (cached-device drop) runs in HwBuffers::dispatchBusMessage above.
1480 // _scheduleReconnect calls stop() then queues a backoff retry if autoReconnect is on.
1481 pThis->_worker->dispatch([pThis]() {
1482 qCDebug(GstVideoReceiverLog) << "Stopping because of error";
1483 pThis->_scheduleReconnect("pipeline error");
1484 });
1485 break;
1486 }
1487 case GST_MESSAGE_WARNING: {
1488 // GStreamer posts WARNING for caps mismatches, decoder fallbacks, clock drift —
1489 // surfacing keeps these visible without escalating to STATUS_FAIL.
1490 gchar *debug = nullptr;
1491 GError *error = nullptr;
1492 gst_message_parse_warning(msg, &error, &debug);
1493 qCWarning(GstVideoReceiverLog) << "GStreamer warning:"
1494 << (error ? error->message : "(no message)")
1495 << "debug:" << (debug ? debug : "(none)");
1496 g_clear_error(&error);
1497 g_clear_pointer(&debug, g_free);
1498 break;
1499 }
1500 case GST_MESSAGE_EOS:
1501 pThis->_worker->dispatch([pThis]() {
1502 qCDebug(GstVideoReceiverLog) << "Received EOS";
1503 pThis->_handleEOS();
1504 });
1505 break;
1506 case GST_MESSAGE_STREAM_COLLECTION: {
1507 GstStreamCollection *collection = nullptr;
1508 gst_message_parse_stream_collection(msg, &collection);
1509 if (!collection) {
1510 break;
1511 }
1512 // SELECT_STREAMS keeps decodebin3 from instantiating audio decoder branches.
1513 GList *selectedIds = nullptr;
1514 const guint nStreams = gst_stream_collection_get_size(collection);
1515 for (guint i = 0; i < nStreams; ++i) {
1516 GstStream *stream = gst_stream_collection_get_stream(collection, i);
1517 const GstStreamType type = gst_stream_get_stream_type(stream);
1518 if (type & GST_STREAM_TYPE_VIDEO) {
1519 selectedIds = g_list_append(selectedIds,
1520 g_strdup(gst_stream_get_stream_id(stream)));
1521 }
1522 }
1523 if (selectedIds) {
1524 GstEvent *event = gst_event_new_select_streams(selectedIds);
1525 gst_element_send_event(GST_ELEMENT(GST_MESSAGE_SRC(msg)), event);
1526 g_list_free_full(selectedIds, g_free);
1527 }
1528 gst_object_unref(collection);
1529 break;
1530 }
1531 case GST_MESSAGE_QOS: {
1532 guint64 processed = 0, dropped = 0;
1533 gst_message_parse_qos_stats(msg, nullptr, &processed, &dropped);
1534
1535 gint64 jitter = 0;
1536 gdouble proportion = 0;
1537 gint quality = 0;
1538 gst_message_parse_qos_values(msg, &jitter, &proportion, &quality);
1539
1540 pThis->_processedFrames.store(processed, std::memory_order_relaxed);
1541 pThis->_droppedFrames.store(dropped, std::memory_order_relaxed);
1542 pThis->_currentJitterNs.store(jitter, std::memory_order_relaxed);
1543 pThis->_qosProportion.store(proportion, std::memory_order_relaxed);
1544 pThis->_qosQuality.store(quality, std::memory_order_relaxed);
1545 // GstBaseSink can post QOS per dropped buffer; defer the emit to the 1 Hz
1546 // watchdog tick so QML isn't flooded from the streaming thread.
1547 pThis->_qosStatsDirty.store(true, std::memory_order_release);
1548 break;
1549 }
1550 case GST_MESSAGE_ELEMENT: {
1551 const GstStructure *structure = gst_message_get_structure(msg);
1552 if (structure && gst_structure_has_name(structure, "qgc-caps-info")) {
1553 gint w = 0, h = 0;
1554 const gchar *fmt = gst_structure_get_string(structure, "format");
1555 gst_structure_get_int(structure, "width", &w);
1556 gst_structure_get_int(structure, "height", &h);
1557 const QString format = QString::fromUtf8(fmt ? fmt : "");
1558 const QSize resolution(w, h);
1559 // src compared by address only on the GUI thread; never dereferenced (may be gone by then).
1560 void *src = GST_MESSAGE_SRC(msg);
1561 QMetaObject::invokeMethod(pThis, [pThis, format, resolution, src]() {
1562 for (auto *c : QGCQVideoSinkController::controllersOf(pThis)) {
1563 if (static_cast<const void*>(c->element()) == src) {
1564 c->updateNegotiation(format, resolution);
1565 }
1566 }
1567 }, Qt::QueuedConnection);
1568 break;
1569 }
1570 if (!gst_structure_has_name(structure, "GstBinForwarded")) {
1571 break;
1572 }
1573
1574 GstMessage *forward_msg = nullptr;
1575 gst_structure_get(structure, "message", GST_TYPE_MESSAGE, &forward_msg, NULL);
1576 if (!forward_msg) {
1577 break;
1578 }
1579
1580 if (GST_MESSAGE_TYPE(forward_msg) == GST_MESSAGE_EOS) {
1581 pThis->_worker->dispatch([pThis]() {
1582 qCDebug(GstVideoReceiverLog) << "Received branch EOS";
1583 pThis->_handleEOS();
1584 });
1585 }
1586
1587 gst_clear_message(&forward_msg);
1588 break;
1589 }
1590 case GST_MESSAGE_STATE_CHANGED: {
1591 GstElement *pipelineRef = pThis->_acquirePipelineRef();
1592 if (!pipelineRef) break;
1593 const bool fromPipeline = (GST_MESSAGE_SRC(msg) == GST_OBJECT(pipelineRef));
1594 if (!fromPipeline) {
1595 gst_object_unref(pipelineRef);
1596 break;
1597 }
1598 GstState oldState = GST_STATE_NULL, newState = GST_STATE_NULL;
1599 gst_message_parse_state_changed(msg, &oldState, &newState, nullptr);
1600 if (newState == GST_STATE_PLAYING && oldState != GST_STATE_PLAYING) {
1601 GstClockTime min = 0, max = 0;
1602 GstQuery *q = gst_query_new_latency();
1603 if (gst_element_query(pipelineRef, q)) {
1604 gboolean live = FALSE;
1605 gst_query_parse_latency(q, &live, &min, &max);
1606 }
1607 gst_query_unref(q);
1608 const QString decName = pThis->decoderName();
1609 qCDebug(GstVideoReceiverLog).noquote()
1610 << "Pipeline PLAYING:" << pThis->_redactedUri()
1611 << "decoder:" << (decName.isEmpty() ? QStringLiteral("(pending)") : decName)
1612 << "min-latency:" << (min / 1000000) << "ms"
1613 << "max-latency:" << (max / 1000000) << "ms";
1614 }
1615 gst_object_unref(pipelineRef);
1616 break;
1617 }
1618 case GST_MESSAGE_LATENCY:
1619 pThis->_worker->dispatch([pThis]() {
1620 GstElement* pipeline = pThis->_acquirePipelineRef();
1621 if (pipeline) {
1622 (void) gst_bin_recalculate_latency(GST_BIN(pipeline));
1623 gst_object_unref(pipeline);
1624 }
1625 });
1626 // Re-prime sink-side latency tracking after the pipeline recalculation (e.g. RTSP
1627 // jitter-buffer reconfigure). Controllers live on the GUI thread; hop there to query.
1628 QMetaObject::invokeMethod(pThis, [pThis]() {
1629 for (auto* c : QGCQVideoSinkController::controllersOf(pThis))
1630 c->refreshLatency();
1631 }, Qt::QueuedConnection);
1632 break;
1633 default:
1634 break;
1635 }
1636
1637 return TRUE;
1638}
1639
1640void GstVideoReceiver::_onNewPad(GstElement *element, GstPad *pad, gpointer data)
1641{
1642 GstVideoReceiver *self = static_cast<GstVideoReceiver*>(data);
1643
1644 if (element == self->_source) {
1645 self->_onNewSourcePad(pad);
1646 } else if (element == self->_decoder) {
1647 self->_onNewDecoderPad(pad);
1648 } else {
1649 qCDebug(GstVideoReceiverLog) << "Unexpected call!";
1650 }
1651}
1652
1653GstPadProbeReturn GstVideoReceiver::_teeProbe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1654{
1655 Q_UNUSED(pad); Q_UNUSED(info)
1656
1657 if (user_data) {
1658 GstVideoReceiver *pThis = static_cast<GstVideoReceiver*>(user_data);
1659 pThis->_noteTeeFrame();
1660 }
1661
1662 return GST_PAD_PROBE_OK;
1663}
1664
1665GstPadProbeReturn GstVideoReceiver::_videoSinkProbe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1666{
1667 Q_UNUSED(pad); Q_UNUSED(info)
1668
1669 if (user_data) {
1670 GstVideoReceiver *pThis = static_cast<GstVideoReceiver*>(user_data);
1671
1672 if (pThis->_resetVideoSink) {
1673 pThis->_resetVideoSink = false;
1674
1675#if 0 // FIXME: this makes MPEG2-TS playing smooth but breaks RTSP
1676 gst_pad_send_event(pad, gst_event_new_flush_start());
1677 gst_pad_send_event(pad, gst_event_new_flush_stop(TRUE));
1678
1679 GstBuffer* buf;
1680
1681 if ((buf = gst_pad_probe_info_get_buffer(info)) != nullptr) {
1682 GstSegment* seg;
1683
1684 if ((seg = gst_segment_new()) != nullptr) {
1685 gst_segment_init(seg, GST_FORMAT_TIME);
1686
1687 seg->start = buf->pts;
1688
1689 gst_pad_send_event(pad, gst_event_new_segment(seg));
1690
1691 gst_segment_free(seg);
1692 seg = nullptr;
1693 }
1694
1695 gst_pad_set_offset(pad, -static_cast<gint64>(buf->pts));
1696 }
1697#endif
1698 }
1699
1700 pThis->_noteVideoSinkFrame();
1701 }
1702
1703 return GST_PAD_PROBE_OK;
1704}
1705
1706GstPadProbeReturn GstVideoReceiver::_eosProbe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1707{
1708 Q_UNUSED(pad);
1709 Q_ASSERT(user_data);
1710
1711 if (info) {
1712 const GstEvent *event = gst_pad_probe_info_get_event(info);
1713 if (GST_EVENT_TYPE(event) == GST_EVENT_EOS) {
1714 GstVideoReceiver *pThis = static_cast<GstVideoReceiver*>(user_data);
1715 pThis->_noteEndOfStream();
1716 }
1717 }
1718
1719 return GST_PAD_PROBE_OK;
1720}
1721
1722GstPadProbeReturn GstVideoReceiver::_keyframeWatch(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1723{
1724 if (!info || !user_data) {
1725 qCCritical(GstVideoReceiverLog) << "Invalid arguments";
1726 return GST_PAD_PROBE_DROP;
1727 }
1728
1729 GstBuffer *buf = gst_pad_probe_info_get_buffer(info);
1730 if (GST_BUFFER_FLAG_IS_SET(buf, GST_BUFFER_FLAG_DELTA_UNIT)) {
1731 // wait for a keyframe
1732 return GST_PAD_PROBE_DROP;
1733 }
1734
1735 // set media file '0' offset to current timeline position - we don't want to touch other elements in the graph, except these which are downstream!
1736 gst_pad_set_offset(pad, -static_cast<gint64>(buf->pts));
1737
1738 qCDebug(GstVideoReceiverLog) << "Got keyframe, stop dropping buffers";
1739
1740 GstVideoReceiver *pThis = static_cast<GstVideoReceiver*>(user_data);
1741 emit pThis->recordingStarted(pThis->recordingOutput());
1742
1743 return GST_PAD_PROBE_REMOVE;
1744}
1745
1747 : QThread(parent)
1748{
1749 qCDebug(GstVideoReceiverLog) << this;
1750}
1751
1753{
1754 qCDebug(GstVideoReceiverLog) << this;
1755}
1756
1758{
1759 return (QThread::currentThread() != this);
1760}
1761
1763{
1764 QMutexLocker lock(&_taskQueueSync);
1765 _taskQueue.enqueue(task);
1766 _taskQueueUpdate.wakeOne();
1767}
1768
1770{
1771 if (needDispatch()) {
1772 dispatch([this]() { _shutdown = true; });
1773 (void) QThread::wait(2000);
1774 } else {
1775 QThread::quit();
1776 }
1777}
1778
1779void GstVideoWorker::run()
1780{
1781 while (!_shutdown) {
1782 _taskQueueSync.lock();
1783
1784 while (_taskQueue.isEmpty()) {
1785 _taskQueueUpdate.wait(&_taskQueueSync);
1786 }
1787
1788 const Task task = _taskQueue.dequeue();
1789
1790 _taskQueueSync.unlock();
1791
1792 task();
1793 }
1794}
GstElement * binParser
std::function< void()> Task
struct _GstElement GstElement
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
int32_t height
Definition UBXPlan.cc:44
void stopDecoding() override
void takeScreenshot(const QString &imageFile) override
Q_INVOKABLE void dumpPipelineGraph(const QString &tag=QStringLiteral("manual"))
void decoderStatsChanged()
QString decoderName() const
GstVideoReceiver(QObject *parent=nullptr)
void start(uint32_t timeout) override
void stopRecording() override
void startRecording(const QString &videoFile, FILE_FORMAT format) override
void stop() override
void startDecoding(void *sink) override
void dispatch(Task task)
bool needDispatch() const
GstVideoWorker(QObject *parent=nullptr)
void recordingStarted(const QString &filename)
QTimer _watchdogTimer
std::atomic< qint64 > _lastSourceFrameTime
bool lowLatency() const
std::atomic< qint64 > _lastVideoFrameTime
void videoSizeChanged(QSize size)
void streamingChanged(bool active)
void nameChanged(const QString &name)
uint32_t _timeout
QString uri() const
QString _recordingOutput
void recordingChanged(bool active)
std::atomic< bool > _decoding
void onStartRecordingComplete(STATUS status)
VideoSinkHandle sink() const
void onTakeScreenshotComplete(STATUS status)
void decodingChanged(bool active)
void onStartComplete(STATUS status)
static bool isValidFileFormat(FILE_FORMAT format)
void onStopDecodingComplete(STATUS status)
void onStopRecordingComplete(STATUS status)
QString recordingOutput() const
void onStartDecodingComplete(STATUS status)
QQuickItem * _widget
void onStopComplete(STATUS status)
std::atomic< bool > _autoReconnect
RTSP/UDP auto-reconnect with exponential backoff on watchdog/error.
GstElement * create(const QString &uri, const Config &config)
@ None
No playout buffering; omits an explicit buffer or sets rtspsrc latency to zero.
@ Buffered
rtpjitterbuffer with drop-on-latency=FALSE.
@ DropOnLatency
rtpjitterbuffer with drop-on-latency=TRUE.
QString writePipelineDot(GstElement *pipeline, const char *tag)
bool isHardwareDecoderFactory(GstElementFactory *factory)
QString takeExtraPathStats() noexcept
Path-specific extras after formatPathStats (GL reuse/sync waits); reads-and-clears,...
Definition HwBuffers.cc:320
GstBusSyncReply onBusSyncMessage(GstBus *, GstMessage *msg, gpointer) noexcept
Bus sync handler (GstBusSyncHandler) installed on every pipeline; no-op when no GPU path compiled.
Definition HwBuffers.cc:169
void dispatchBusMessage(GstMessage *msg) noexcept
Receiver-side bus hook; drops cached GPU devices on GST_MESSAGE_ERROR. No-op when no GPU paths compil...
Definition HwBuffers.cc:114
PathStats formatPathStats(bool reset) noexcept
Definition HwBuffers.cc:296
QByteArray format(const QList< LogEntry > &entries, int fmt)
QString redactedUrlForLogging(const QUrl &url)
Formatted per-path counters + delivered total; reset=true reads-and-clears (teardown),...
Definition HwBuffers.h:62