21#include <QtCore/QDateTime>
22#include <QtCore/QMutexLocker>
24#include <QtQuick/QQuickItem>
29#include <gst/video/video.h>
35constexpr GstClockTime kEosTimeoutNs = 3 * GST_SECOND;
39gboolean grabFirstSrcPad(
GstElement * , GstPad *pad, gpointer userData)
41 *
static_cast<GstPad **
>(userData) = GST_PAD(gst_object_ref(pad));
45bool isRecoverableH265PaciError(GstMessage *msg,
const GError *
error,
const gchar *debug)
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")) {
52 GstObject *src = GST_MESSAGE_SRC(msg);
53 if (!src || !GST_IS_ELEMENT(src)) {
57 GstElementFactory *factory = gst_element_get_factory(GST_ELEMENT(src));
58 return factory && (g_strcmp0(GST_OBJECT_NAME(factory),
"rtph265depay") == 0);
61const char* recordingParserFactory(
const GstCaps* caps)
63 if (!caps || gst_caps_is_any(caps) || gst_caps_is_empty(caps)) {
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")) {
72 if (gst_structure_has_name(structure,
"video/x-h265")) {
86 qCDebug(GstVideoReceiverLog) <<
this;
89 (void) connect(&
_watchdogTimer, &QTimer::timeout,
this, &GstVideoReceiver::_watchdog);
97 qCDebug(GstVideoReceiverLog) <<
this;
100QString GstVideoReceiver::_redactedUri()
const
107 if (_needDispatch()) {
113 qCDebug(GstVideoReceiverLog) <<
"Already running!" << _redactedUri();
118 if (
_uri.isEmpty()) {
119 qCDebug(GstVideoReceiverLog) <<
"Failed because URI is not specified";
127 qCDebug(GstVideoReceiverLog) <<
"Starting" << _redactedUri() <<
", lowLatency" <<
lowLatency()
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.";
142 bool running =
false;
143 bool pipelineUp =
false;
149 _tee = gst_element_factory_make(
"tee",
nullptr);
151 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make('tee') failed";
155 GstPad *pad = gst_element_get_static_pad(_tee,
"sink");
157 qCCritical(GstVideoReceiverLog) <<
"gst_element_get_static_pad() failed";
163 _teeProbeId = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, _teeProbe,
this,
nullptr);
164 gst_clear_object(&pad);
165 if (_teeProbeId == 0) {
167 qCCritical(GstVideoReceiverLog) <<
"gst_pad_add_probe(_teeProbe) failed";
171 decoderQueue = gst_element_factory_make(
"queue",
nullptr);
173 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make('queue') failed";
180 g_object_set(decoderQueue,
182 "max-size-buffers", 2,
184 "max-size-time", G_GUINT64_CONSTANT(0),
187 _decoderValve = gst_element_factory_make(
"valve",
nullptr);
188 if (!_decoderValve) {
189 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make('valve') failed";
193 g_object_set(_decoderValve,
197 recorderQueue = gst_element_factory_make(
"queue",
nullptr);
198 if (!recorderQueue) {
199 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make('queue') failed";
203 _recorderValve = gst_element_factory_make(
"valve",
nullptr);
204 if (!_recorderValve) {
205 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make('valve') failed";
209 g_object_set(_recorderValve,
214 gst_util_set_object_arg(G_OBJECT(_recorderValve),
"drop-mode",
"forward-sticky-events");
216 _pipeline = gst_pipeline_new(
"receiver");
218 qCCritical(GstVideoReceiverLog) <<
"gst_pipeline_new() failed";
222 g_object_set(_pipeline,
223 "message-forward", TRUE,
238 qCCritical(GstVideoReceiverLog) <<
"SourceFactory::create() failed";
242 gst_bin_add_many(GST_BIN(_pipeline), _source, _tee, decoderQueue, _decoderValve, recorderQueue, _recorderValve,
nullptr);
246 GstPad *srcPad =
nullptr;
247 (void) gst_element_foreach_src_pad(_source, grabFirstSrcPad, &srcPad);
250 _onNewSourcePad(srcPad);
251 gst_clear_object(&srcPad);
253 (void) g_signal_connect(_source,
"pad-added", G_CALLBACK(_onNewPad),
this);
256 if (!gst_element_link_many(_tee, decoderQueue, _decoderValve,
nullptr)) {
257 qCCritical(GstVideoReceiverLog) <<
"Unable to link decoder queue";
261 if (!gst_element_link_many(_tee, recorderQueue, _recorderValve,
nullptr)) {
262 qCCritical(GstVideoReceiverLog) <<
"Unable to link recorder queue";
266 GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
268 gst_bus_enable_sync_message_emission(bus);
269 (void) g_signal_connect(bus,
"sync-message", G_CALLBACK(_onBusMessage),
this);
274 gst_clear_object(&bus);
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);
282 qCCritical(GstVideoReceiverLog) <<
"Failed";
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);
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);
301 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-started");
302 qCDebug(GstVideoReceiverLog) <<
"Started" << _redactedUri();
306 QMetaObject::invokeMethod(
this, [
this]() {
_watchdogTimer.start(1000); }, Qt::QueuedConnection);
313 if (_needDispatch()) {
318 if (
_uri.isEmpty()) {
319 qCDebug(GstVideoReceiverLog) <<
"Stop called on empty URI (no-op)";
323 qCDebug(GstVideoReceiverLog) <<
"Stopping" << _redactedUri();
327 _reconnectEpoch.fetch_add(1, std::memory_order_relaxed);
329 QMetaObject::invokeMethod(
this, [
this]() {
_watchdogTimer.stop(); }, Qt::QueuedConnection);
331 if (_teeProbeId != 0) {
333 GstPad *sinkpad = gst_element_get_static_pad(_tee,
"sink");
335 gst_pad_remove_probe(sinkpad, _teeProbeId);
336 gst_clear_object(&sinkpad);
343 GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
345 gst_bus_disable_sync_message_emission(bus);
346 (void) g_signal_handlers_disconnect_by_data(bus,
this);
348 gboolean recordingValveClosed = TRUE;
349 g_object_get(_recorderValve,
"drop", &recordingValveClosed,
nullptr);
351 if (!recordingValveClosed) {
352 (void) gst_element_send_event(_pipeline, gst_event_new_eos());
360 const GstClockTime deadline = kEosTimeoutNs;
361 const qint64 startMs = QDateTime::currentMSecsSinceEpoch();
362 bool finalized =
false;
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));
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";
380 case GST_MESSAGE_EOS:
381 qCDebug(GstVideoReceiverLog) <<
"End of stream received (fallback path)";
384 case GST_MESSAGE_ERROR:
385 qCCritical(GstVideoReceiverLog) <<
"Error stopping pipeline!";
391 gst_clear_message(&msg);
392 if (finalized)
break;
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)";
402 gst_clear_object(&bus);
404 qCCritical(GstVideoReceiverLog) <<
"gst_pipeline_get_bus() failed";
407 (void) gst_element_set_state(_pipeline, GST_STATE_NULL);
408 (void) gst_element_get_state(_pipeline,
nullptr,
nullptr, GST_CLOCK_TIME_NONE);
412 _shutdownRecordingBranch();
416 _shutdownDecodingBranch();
419 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-stopped");
424 QMutexLocker lock(&_pipelineMutex);
425 gst_clear_object(&_pipeline);
429 _recorderValve =
nullptr;
430 _decoderValve =
nullptr;
438 qCDebug(GstVideoReceiverLog) <<
"Streaming stopped" << _redactedUri();
441 qCDebug(GstVideoReceiverLog) <<
"Streaming did not start" << _redactedUri();
445 qCDebug(GstVideoReceiverLog) <<
"Stopped" << _redactedUri();
448 qCInfo(GstVideoReceiverLog).noquote()
458 qCCritical(GstVideoReceiverLog) <<
"VideoSink is NULL" << _redactedUri();
462 if (_needDispatch()) {
467 qCDebug(GstVideoReceiverLog) <<
"Starting decoding" << _redactedUri();
470 qCDebug(GstVideoReceiverLog) <<
"Video Widget is NULL" << _redactedUri();
476 gst_clear_object(&_videoSink);
480 qCDebug(GstVideoReceiverLog) <<
"Already decoding!" << _redactedUri();
486 GstPad *pad = gst_element_get_static_pad(videoSink,
"sink");
488 qCCritical(GstVideoReceiverLog) <<
"Unable to find sink pad of video sink" << _redactedUri();
496 _videoSinkProbeId = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, _videoSinkProbe,
this,
nullptr);
497 gst_clear_object(&pad);
499 _videoSink = videoSink;
500 gst_object_ref(_videoSink);
509 _ensureVideoSinkInPipeline();
511 if (!_addDecoder(_decoderValve)) {
512 qCCritical(GstVideoReceiverLog) <<
"_addDecoder() failed" << _redactedUri();
513 _shutdownDecodingBranch();
518 g_object_set(_decoderValve,
522 qCDebug(GstVideoReceiverLog) <<
"Decoding started" << _redactedUri();
529 if (_needDispatch()) {
534 qCDebug(GstVideoReceiverLog) <<
"Stopping decoding" << _redactedUri();
540 if (!_pipeline || !_videoSink) {
541 qCDebug(GstVideoReceiverLog) <<
"Not decoding!" << _redactedUri();
546 g_object_set(_decoderValve,
552 const bool ret = _unlinkBranch(_decoderValve);
561 if (_needDispatch()) {
562 const QString cachedVideoFile = videoFile;
567 qCDebug(GstVideoReceiverLog) <<
"Starting recording" << _redactedUri();
570 qCDebug(GstVideoReceiverLog) <<
"Streaming is not active!" << _redactedUri();
576 qCDebug(GstVideoReceiverLog) <<
"Already recording!" << _redactedUri();
581 qCDebug(GstVideoReceiverLog) <<
"New video file:" << videoFile << _redactedUri();
583 GstPad* probepad = gst_element_get_static_pad(_recorderValve,
"src");
585 qCCritical(GstVideoReceiverLog) <<
"gst_element_get_static_pad() failed" << _redactedUri();
590 const auto failRecordingStart = [
this, &probepad]() {
591 gst_clear_object(&probepad);
593 (void) gst_element_set_state(_fileSink, GST_STATE_NULL);
596 gst_element_unlink(_recorderValve, _fileSink);
597 GstObject* parent = gst_element_get_parent(_fileSink);
599 (void) gst_bin_remove(GST_BIN(parent), _fileSink);
600 gst_clear_object(&parent);
602 gst_clear_object(&_fileSink);
609 GstCaps* inputCaps = gst_pad_get_current_caps(probepad);
611 inputCaps = gst_pad_query_caps(probepad,
nullptr);
613 _fileSink = _makeFileSink(videoFile, format, inputCaps);
614 gst_clear_caps(&inputCaps);
616 qCCritical(GstVideoReceiverLog) <<
"_makeFileSink() failed" << _redactedUri();
617 failRecordingStart();
623 if (!gst_bin_add(GST_BIN(_pipeline), _fileSink)) {
624 qCCritical(GstVideoReceiverLog) <<
"gst_bin_add(file sink) failed" << _redactedUri();
625 failRecordingStart();
628 (void) gst_object_ref(_fileSink);
630 if (!gst_element_link(_recorderValve, _fileSink)) {
631 qCCritical(GstVideoReceiverLog) <<
"Failed to link valve and file sink" << _redactedUri();
632 failRecordingStart();
636 if (!gst_element_sync_state_with_parent(_fileSink)) {
637 qCCritical(GstVideoReceiverLog) <<
"gst_element_sync_state_with_parent(file sink) failed" << _redactedUri();
638 failRecordingStart();
642 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-with-filesink");
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();
653 gst_clear_object(&probepad);
655 g_object_set(_recorderValve,
661 qCDebug(GstVideoReceiverLog) <<
"Recording started" << _redactedUri();
668 if (_needDispatch()) {
673 qCDebug(GstVideoReceiverLog) <<
"Stopping recording" << _redactedUri();
676 qCDebug(GstVideoReceiverLog) <<
"Not recording!" << _redactedUri();
681 g_object_set(_recorderValve,
687 if (!_unlinkBranch(_recorderValve)) {
695 _recordingStopRequested =
true;
700 if (_needDispatch()) {
701 const QString cachedImageFile = imageFile;
706 qCDebug(GstVideoReceiverLog) <<
"taking screenshot" << _redactedUri();
712void GstVideoReceiver::_watchdog()
719 const qint64 now = QDateTime::currentSecsSinceEpoch();
721 if (lastSourceFrameTime == 0) {
722 lastSourceFrameTime = now;
729 qCDebug(GstVideoReceiverLog).noquote() <<
"HW path live" << _redactedUri() << hwStats.line;
734 if (_qosStatsDirty.exchange(
false, std::memory_order_acq_rel)) {
738 qint64 elapsed = now - lastSourceFrameTime;
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");
743 _scheduleReconnect(
"source watchdog");
749 if (lastVideoFrameTime == 0) {
750 lastVideoFrameTime = now;
754 elapsed = now - lastVideoFrameTime;
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");
759 _scheduleReconnect(
"decoder watchdog");
765void GstVideoReceiver::_scheduleReconnect(
const char *reason)
772 qCDebug(GstVideoReceiverLog) <<
"Auto-reconnect disabled — not retrying after" << reason;
776 if (
_uri.isEmpty()) {
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);
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;
803 GstElement *livePipeline = _acquirePipelineRef();
804 const bool pipelineUp = (livePipeline !=
nullptr);
805 if (livePipeline) gst_object_unref(livePipeline);
806 if (
uri.isEmpty() || pipelineUp)
return;
807 qCInfo(GstVideoReceiverLog) <<
"Reconnecting (attempt" << attempts <<
")"
809 start(reconnectTimeout);
811 }, Qt::QueuedConnection);
817 GstElement *pipelineRef = _acquirePipelineRef();
819 qCDebug(GstVideoReceiverLog) <<
"dumpPipelineGraph: pipeline not running";
822 const QByteArray tagUtf8 = tag.toUtf8();
823 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipelineRef), GST_DEBUG_GRAPH_SHOW_ALL, tagUtf8.constData());
825 if (!dotPath.isEmpty()) {
826 qCInfo(GstVideoReceiverLog) <<
"Pipeline graph saved to" << dotPath;
828 gst_object_unref(pipelineRef);
832void GstVideoReceiver::_handleEOS()
841 _shutdownDecodingBranch();
843 _shutdownRecordingBranch();
852 GstElement *decoder = gst_element_factory_make(
"decodebin3",
nullptr);
854 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make('decodebin3') failed";
859GstElement* GstVideoReceiver::_makeFileSink(
const QString& videoFile, FILE_FORMAT format,
const GstCaps* inputCaps)
865 GstPad *videopad =
nullptr;
866 GstPad* parserSrcPad =
nullptr;
867 GstPad* parserSinkPad =
nullptr;
868 GstPad *ghostpad =
nullptr;
872 qCCritical(GstVideoReceiverLog) <<
"Unsupported file format";
876 const char* parserFactory = recordingParserFactory(inputCaps);
878 parser = gst_element_factory_make(parserFactory,
nullptr);
880 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make() failed for" << parserFactory;
883 g_object_set(parser,
"config-interval", -1,
nullptr);
890 splitmux = gst_element_factory_make(
"splitmuxsink",
nullptr);
892 qCCritical(GstVideoReceiverLog) <<
"gst_element_factory_make('splitmuxsink') failed";
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,
906 "message-forward", TRUE,
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),
917 g_object_set(splitmux,
"muxer-properties", muxerProps,
nullptr);
918 gst_structure_free(muxerProps);
921 bin = gst_bin_new(
"sinkbin");
923 qCCritical(GstVideoReceiverLog) <<
"gst_bin_new('sinkbin') failed";
930 videopad = gst_element_request_pad_simple(splitmux,
"video");
932 qCCritical(GstVideoReceiverLog) <<
"gst_element_request_pad_simple(splitmuxsink, video) failed";
938 if (!gst_bin_add(GST_BIN(bin), parser)) {
939 qCCritical(GstVideoReceiverLog) <<
"gst_bin_add(parser) failed";
946 if (!gst_bin_add(GST_BIN(bin), splitmux)) {
947 qCCritical(GstVideoReceiverLog) <<
"gst_bin_add(splitmuxsink) failed";
952 GstPad* ghostTarget = videopad;
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";
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);
967 ghostTarget = parserSinkPad;
970 ghostpad = gst_ghost_pad_new(
"sink", ghostTarget);
972 qCCritical(GstVideoReceiverLog) <<
"gst_ghost_pad_new() failed";
976 if (!gst_element_add_pad(bin, ghostpad)) {
977 qCCritical(GstVideoReceiverLog) <<
"gst_element_add_pad(ghost) failed";
986 gst_clear_object(&ghostpad);
987 gst_clear_object(&parserSinkPad);
988 gst_clear_object(&parserSrcPad);
991 gst_clear_object(&videopad);
992 gst_clear_object(&splitmux);
993 gst_clear_object(&parser);
994 gst_clear_object(&bin);
998void GstVideoReceiver::_onNewSourcePad(GstPad *pad)
1001 if (!gst_element_link(_source, _tee)) {
1002 qCCritical(GstVideoReceiverLog) <<
"Unable to link source";
1008 qCDebug(GstVideoReceiverLog) <<
"Streaming started" << _redactedUri();
1012 _eosProbeId = gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, _eosProbe,
this,
nullptr);
1013 if (_eosProbeId != 0) {
1015 _eosProbePad = GST_PAD_CAST(gst_object_ref(pad));
1021 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-with-new-source-pad");
1023 _ensureVideoSinkInPipeline();
1025 if (!_addDecoder(_decoderValve)) {
1026 qCCritical(GstVideoReceiverLog) <<
"_addDecoder() failed";
1027 _shutdownDecodingBranch();
1031 g_object_set(_decoderValve,
1035 qCDebug(GstVideoReceiverLog) <<
"Decoding started" << _redactedUri();
1038void GstVideoReceiver::_logDecodebin3SelectedCodec(
GstElement *decodebin3)
1040 GValue value = G_VALUE_INIT;
1041 GstIterator *iter = gst_bin_iterate_elements(GST_BIN(decodebin3));
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);
1049 gboolean is_decoder = gst_element_factory_list_is_type(factory, GST_ELEMENT_FACTORY_TYPE_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);
1057 QString pluginName = featureName;
1058 GstPlugin *plugin = gst_plugin_feature_get_plugin(feature);
1060 pluginName = gst_plugin_get_name(plugin);
1061 gst_object_unref(plugin);
1063 qCDebug(GstVideoReceiverLog) <<
"Decodebin3 selected codec:rank -" << pluginName <<
"/" << featureName <<
"-" << decoderKlass << (isHardwareDecoder ?
"(HW)" :
"(SW)") <<
":" << rank;
1065 const QString newName = QString::fromUtf8(featureName);
1068 QMutexLocker locker(&_decoderNameMutex);
1069 if (newName != _decoderName) {
1070 _decoderName = newName;
1082 g_object_set(child,
"qos", FALSE,
nullptr);
1083 qCDebug(GstVideoReceiverLog) <<
"Disabled QoS on internal decoder" << featureName;
1086 g_value_reset(&value);
1088 g_value_unset(&value);
1089 gst_iterator_free(iter);
1093void GstVideoReceiver::_onNewDecoderPad(GstPad *pad)
1095 qCDebug(GstVideoReceiverLog) <<
"_onNewDecoderPad" << _redactedUri();
1097 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-with-new-decoder-pad");
1100 _logDecodebin3SelectedCodec(_decoder);
1102 if (!_addVideoSink(pad)) {
1103 qCCritical(GstVideoReceiverLog) <<
"_addVideoSink() failed";
1107bool GstVideoReceiver::_addDecoder(
GstElement *src)
1109 _decoder = _makeDecoder();
1111 qCCritical(GstVideoReceiverLog) <<
"_makeDecoder() failed";
1115 (void) gst_object_ref(_decoder);
1117 (void) gst_bin_add(GST_BIN(_pipeline), _decoder);
1118 (void) gst_element_sync_state_with_parent(_decoder);
1120 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-with-decoder");
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);
1131 GstPad *srcPad =
nullptr;
1132 (void) gst_element_foreach_src_pad(_decoder, grabFirstSrcPad, &srcPad);
1135 _onNewDecoderPad(srcPad);
1137 (void) g_signal_connect(_decoder,
"pad-added", G_CALLBACK(_onNewPad),
this);
1140 gst_clear_object(&srcPad);
1144void GstVideoReceiver::_ensureVideoSinkInPipeline()
1146 if (!_videoSink || !_pipeline) {
1150 GstObject *parent = gst_element_get_parent(_videoSink);
1152 gst_object_unref(parent);
1156 g_object_set(_videoSink,
1160 (void) gst_object_ref(_videoSink);
1161 (void) gst_bin_add(GST_BIN(_pipeline), _videoSink);
1164 (void) gst_element_set_state(_videoSink, GST_STATE_PAUSED);
1167bool GstVideoReceiver::_addVideoSink(GstPad *pad)
1169 GstCaps *caps = gst_pad_query_caps(pad,
nullptr);
1171 _ensureVideoSinkInPipeline();
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;
1179 GstObject *parent = gst_element_get_parent(_videoSink);
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);
1187 gst_clear_object(&sinkPad);
1188 gst_clear_caps(&caps);
1191 gst_clear_object(&sinkPad);
1193 (void) gst_element_sync_state_with_parent(_videoSink);
1195 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-with-videosink");
1200 if (!_decoderValve) {
1201 qCCritical(GstVideoReceiverLog) <<
"Unable to determine video size - _decoderValve is NULL"
1206 GstPad *valveSrcPad = gst_element_get_static_pad(_decoderValve,
"src");
1208 qCCritical(GstVideoReceiverLog) <<
"gst_element_get_static_pad() failed";
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);
1219 const GstStructure *structure = gst_caps_get_structure(valveSrcPadCaps, 0);
1221 qCCritical(GstVideoReceiverLog) <<
"Unable to determine video size - structure is NULL"
1223 gst_clear_object(&valveSrcPad);
1229 (void) gst_structure_get_int(structure,
"width", &width);
1230 (void) gst_structure_get_int(structure,
"height", &
height);
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);
1242 videoSize.setWidth(width);
1243 videoSize.setHeight(
height);
1246 gst_clear_caps(&valveSrcPadCaps);
1247 gst_clear_object(&valveSrcPad);
1251 gst_clear_caps(&caps);
1255void GstVideoReceiver::_noteTeeFrame()
1262 if (_reconnectAttempts.load(std::memory_order_relaxed) != 0) {
1263 QMetaObject::invokeMethod(
1264 this, [
this]() { _reconnectAttempts.store(0, std::memory_order_relaxed); }, Qt::QueuedConnection);
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();
1275void GstVideoReceiver::_noteVideoSinkFrame()
1280 qCDebug(GstVideoReceiverLog) <<
"Decoding started";
1285void GstVideoReceiver::_noteEndOfStream()
1290bool GstVideoReceiver::_unlinkBranch(
GstElement *from)
1292 GstPad *src = gst_element_get_static_pad(from,
"src");
1294 qCCritical(GstVideoReceiverLog) <<
"gst_element_get_static_pad() failed";
1298 GstPad *
sink = gst_pad_get_peer(src);
1300 gst_clear_object(&src);
1301 qCCritical(GstVideoReceiverLog) <<
"gst_pad_get_peer() failed";
1305 if (!gst_pad_unlink(src,
sink)) {
1306 gst_clear_object(&src);
1307 gst_clear_object(&
sink);
1308 qCCritical(GstVideoReceiverLog) <<
"gst_pad_unlink() failed";
1312 gst_clear_object(&src);
1315 const gboolean ret = gst_pad_send_event(
sink, gst_event_new_eos());
1317 gst_clear_object(&
sink);
1320 qCCritical(GstVideoReceiverLog) <<
"Branch EOS was NOT sent";
1324 qCDebug(GstVideoReceiverLog) <<
"Branch EOS was sent";
1329void GstVideoReceiver::_shutdownDecodingBranch()
1332 GstObject *parent = gst_element_get_parent(_decoder);
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);
1340 gst_clear_object(&_decoder);
1343 if (_videoSinkProbeId != 0 && _videoSink) {
1344 GstPad *sinkpad = gst_element_get_static_pad(_videoSink,
"sink");
1346 gst_pad_remove_probe(sinkpad, _videoSinkProbeId);
1347 gst_clear_object(&sinkpad);
1350 _videoSinkProbeId = 0;
1352 if (_eosProbeId != 0 && _eosProbePad) {
1354 gst_pad_remove_probe(_eosProbePad, _eosProbeId);
1357 gst_clear_object(&_eosProbePad);
1362 GstObject *parent = gst_element_get_parent(_videoSink);
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);
1369 gst_clear_object(&_videoSink);
1376 qCDebug(GstVideoReceiverLog) <<
"Decoding stopped";
1380 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-decoding-stopped");
1383void GstVideoReceiver::_shutdownRecordingBranch()
1385 if (_keyframeWatchId != 0 && _recorderValve) {
1386 GstPad *probepad = gst_element_get_static_pad(_recorderValve,
"src");
1388 gst_pad_remove_probe(probepad, _keyframeWatchId);
1389 gst_clear_object(&probepad);
1391 _keyframeWatchId = 0;
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);
1403 qCDebug(GstVideoReceiverLog) <<
"Recording stopped";
1407 if (_recordingStopRequested) {
1408 _recordingStopRequested =
false;
1412 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-recording-stopped");
1415bool GstVideoReceiver::_needDispatch()
1420GstElement *GstVideoReceiver::_acquirePipelineRef()
const
1422 QMutexLocker lock(&_pipelineMutex);
1423 if (!_pipeline)
return nullptr;
1424 return GST_ELEMENT(gst_object_ref(_pipeline));
1427gboolean GstVideoReceiver::_onBusMessage(GstBus * , GstMessage *msg, gpointer data)
1429 if (!msg || !data) {
1430 qCCritical(GstVideoReceiverLog) <<
"Invalid parameters in _onBusMessage: msg=" << msg <<
"data=" << data;
1436 if (GST_MESSAGE_TYPE(msg) != GST_MESSAGE_ERROR) {
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);
1448 qCDebug(GstVideoReceiverLog) <<
"GStreamer debug:" << debug;
1449 g_clear_pointer(&debug, g_free);
1453 if (recoverableH265PaciError) {
1454 qCWarning(GstVideoReceiverLog)
1455 <<
"Ignoring unsupported H.265 RTP PACI packet from rtph265depay:" <<
error->message;
1457 qCCritical(GstVideoReceiverLog) <<
"GStreamer error:" <<
error->message;
1459 g_clear_error(&
error);
1462 if (recoverableH265PaciError) {
1468 if (
GstElement *pipelineRef = pThis->_acquirePipelineRef()) {
1471 GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(pipelineRef), GST_DEBUG_GRAPH_SHOW_ALL,
"pipeline-error");
1473 if (!dotPath.isEmpty()) {
1474 qCInfo(GstVideoReceiverLog) <<
"Pipeline graph saved to" << dotPath;
1476 gst_object_unref(pipelineRef);
1481 pThis->_worker->
dispatch([pThis]() {
1482 qCDebug(GstVideoReceiverLog) <<
"Stopping because of error";
1483 pThis->_scheduleReconnect(
"pipeline error");
1487 case GST_MESSAGE_WARNING: {
1490 gchar *debug =
nullptr;
1491 GError *
error =
nullptr;
1492 gst_message_parse_warning(msg, &
error, &debug);
1493 qCWarning(GstVideoReceiverLog) <<
"GStreamer warning:"
1495 <<
"debug:" << (debug ? debug :
"(none)");
1496 g_clear_error(&
error);
1497 g_clear_pointer(&debug, g_free);
1500 case GST_MESSAGE_EOS:
1501 pThis->_worker->
dispatch([pThis]() {
1502 qCDebug(GstVideoReceiverLog) <<
"Received EOS";
1503 pThis->_handleEOS();
1506 case GST_MESSAGE_STREAM_COLLECTION: {
1507 GstStreamCollection *collection =
nullptr;
1508 gst_message_parse_stream_collection(msg, &collection);
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)));
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);
1528 gst_object_unref(collection);
1531 case GST_MESSAGE_QOS: {
1532 guint64 processed = 0, dropped = 0;
1533 gst_message_parse_qos_stats(msg,
nullptr, &processed, &dropped);
1536 gdouble proportion = 0;
1538 gst_message_parse_qos_values(msg, &jitter, &proportion, &quality);
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);
1547 pThis->_qosStatsDirty.store(
true, std::memory_order_release);
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")) {
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);
1560 void *src = GST_MESSAGE_SRC(msg);
1561 QMetaObject::invokeMethod(pThis, [pThis, format, resolution, src]() {
1563 if (
static_cast<const void*
>(c->element()) == src) {
1564 c->updateNegotiation(format, resolution);
1567 }, Qt::QueuedConnection);
1570 if (!gst_structure_has_name(structure,
"GstBinForwarded")) {
1574 GstMessage *forward_msg =
nullptr;
1575 gst_structure_get(structure,
"message", GST_TYPE_MESSAGE, &forward_msg, NULL);
1580 if (GST_MESSAGE_TYPE(forward_msg) == GST_MESSAGE_EOS) {
1581 pThis->_worker->
dispatch([pThis]() {
1582 qCDebug(GstVideoReceiverLog) <<
"Received branch EOS";
1583 pThis->_handleEOS();
1587 gst_clear_message(&forward_msg);
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);
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);
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";
1615 gst_object_unref(pipelineRef);
1618 case GST_MESSAGE_LATENCY:
1619 pThis->_worker->
dispatch([pThis]() {
1620 GstElement* pipeline = pThis->_acquirePipelineRef();
1622 (void) gst_bin_recalculate_latency(GST_BIN(pipeline));
1623 gst_object_unref(pipeline);
1628 QMetaObject::invokeMethod(pThis, [pThis]() {
1630 c->refreshLatency();
1631 }, Qt::QueuedConnection);
1640void GstVideoReceiver::_onNewPad(
GstElement *element, GstPad *pad, gpointer data)
1644 if (element == self->_source) {
1645 self->_onNewSourcePad(pad);
1646 }
else if (element == self->_decoder) {
1647 self->_onNewDecoderPad(pad);
1649 qCDebug(GstVideoReceiverLog) <<
"Unexpected call!";
1653GstPadProbeReturn GstVideoReceiver::_teeProbe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1655 Q_UNUSED(pad); Q_UNUSED(info)
1659 pThis->_noteTeeFrame();
1662 return GST_PAD_PROBE_OK;
1665GstPadProbeReturn GstVideoReceiver::_videoSinkProbe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1667 Q_UNUSED(pad); Q_UNUSED(info)
1676 gst_pad_send_event(pad, gst_event_new_flush_start());
1677 gst_pad_send_event(pad, gst_event_new_flush_stop(TRUE));
1681 if ((buf = gst_pad_probe_info_get_buffer(info)) !=
nullptr) {
1684 if ((seg = gst_segment_new()) !=
nullptr) {
1685 gst_segment_init(seg, GST_FORMAT_TIME);
1687 seg->start = buf->pts;
1689 gst_pad_send_event(pad, gst_event_new_segment(seg));
1691 gst_segment_free(seg);
1695 gst_pad_set_offset(pad, -
static_cast<gint64
>(buf->pts));
1700 pThis->_noteVideoSinkFrame();
1703 return GST_PAD_PROBE_OK;
1706GstPadProbeReturn GstVideoReceiver::_eosProbe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1709 Q_ASSERT(user_data);
1712 const GstEvent *
event = gst_pad_probe_info_get_event(info);
1713 if (GST_EVENT_TYPE(event) == GST_EVENT_EOS) {
1715 pThis->_noteEndOfStream();
1719 return GST_PAD_PROBE_OK;
1722GstPadProbeReturn GstVideoReceiver::_keyframeWatch(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
1724 if (!info || !user_data) {
1725 qCCritical(GstVideoReceiverLog) <<
"Invalid arguments";
1726 return GST_PAD_PROBE_DROP;
1729 GstBuffer *buf = gst_pad_probe_info_get_buffer(info);
1730 if (GST_BUFFER_FLAG_IS_SET(buf, GST_BUFFER_FLAG_DELTA_UNIT)) {
1732 return GST_PAD_PROBE_DROP;
1736 gst_pad_set_offset(pad, -
static_cast<gint64
>(buf->pts));
1738 qCDebug(GstVideoReceiverLog) <<
"Got keyframe, stop dropping buffers";
1743 return GST_PAD_PROBE_REMOVE;
1749 qCDebug(GstVideoReceiverLog) <<
this;
1754 qCDebug(GstVideoReceiverLog) <<
this;
1759 return (QThread::currentThread() !=
this);
1764 QMutexLocker lock(&_taskQueueSync);
1765 _taskQueue.enqueue(task);
1766 _taskQueueUpdate.wakeOne();
1772 dispatch([
this]() { _shutdown =
true; });
1773 (void) QThread::wait(2000);
1779void GstVideoWorker::run()
1781 while (!_shutdown) {
1782 _taskQueueSync.lock();
1784 while (_taskQueue.isEmpty()) {
1785 _taskQueueUpdate.wait(&_taskQueueSync);
1788 const Task task = _taskQueue.dequeue();
1790 _taskQueueSync.unlock();
std::function< void()> Task
struct _GstElement GstElement
#define QGC_LOGGING_CATEGORY(name, categoryStr)
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 startDecoding(void *sink) override
bool needDispatch() const
GstVideoWorker(QObject *parent=nullptr)
void recordingStarted(const QString &filename)
std::atomic< qint64 > _lastSourceFrameTime
std::atomic< qint64 > _lastVideoFrameTime
void videoSizeChanged(QSize size)
void streamingChanged(bool active)
void nameChanged(const QString &name)
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)
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,...
GstBusSyncReply onBusSyncMessage(GstBus *, GstMessage *msg, gpointer) noexcept
Bus sync handler (GstBusSyncHandler) installed on every pipeline; no-op when no GPU path compiled.
void dispatchBusMessage(GstMessage *msg) noexcept
Receiver-side bus hook; drops cached GPU devices on GST_MESSAGE_ERROR. No-op when no GPU paths compil...
PathStats formatPathStats(bool reset) noexcept
QString redactedUrlForLogging(const QUrl &url)
JitterBuffer jitterBuffer
Formatted per-path counters + delivered total; reset=true reads-and-clears (teardown),...