Skip to content

Commit 56a05e4

Browse files
committed
onvif: Auto-create OBS Media Source from discovered RTSP stream
Builds on the previous WS-Discovery dialog patch: once the user picks a camera, also offer to drop an FFmpeg-based Media Source into the current scene pointing at the camera's RTSP stream. The dialog grows a "Also create an OBS Media Source for this camera's stream" checkbox (default checked) and a stream picker that lists every RTSP URI returned by GetStreamUri so the user can choose Main vs Sub before clicking "Use Selected Camera". On accept, settings.cpp: - Takes the URI from the picker (falls back to the first usable URI), - Embeds the dialog's username/password as Basic auth into the URL so the source still works after the per-session token in the camera's reply expires, - Creates an "ffmpeg_source" with restart_on_activate and hw_decode, - Adds it to the currently active scene. If the user unchecks the box, only the PTZ device is created. If GetStreamUri came back empty (anonymous probe was rejected, or the camera has no streamable profile), the Media Source step silently no-ops. Signed-off-by: Jonatã Bolzan Loss <jonata@jonata.org>
1 parent ee4ebc9 commit 56a05e4

5 files changed

Lines changed: 89 additions & 4 deletions

File tree

data/locale/en-GB.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ PTZ.ONVIF.Discovery.AuthFailed="Authentication failed. Check the username and pa
9696
PTZ.ONVIF.Discovery.ManualPrompt="Don't see your camera? Add manually:"
9797
PTZ.ONVIF.Discovery.ManualAdd="Add"
9898
PTZ.ONVIF.Discovery.ManualLabel="(manual)"
99+
PTZ.ONVIF.Discovery.CreateMediaSource="Also create an OBS Media Source for this camera's stream"
99100
PTZ.ONVIF.SpeedBoost="Speed Boost (multiplies normalized ONVIF velocity; spec max is 1.0)"
100101
PTZ.ONVIF.MediaProfile="Media Profile"
101102
PTZ.ONVIF.NoProfilesYet="(not loaded yet — apply and reopen)"

src/onvif-discovery.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
#include <QCryptographicHash>
1313
#include <QDateTime>
14+
#include <QCheckBox>
15+
#include <QComboBox>
1416
#include <QDialogButtonBox>
1517
#include <QFile>
1618
#include <QFormLayout>
@@ -682,6 +684,16 @@ OnvifDiscoveryDialog::OnvifDiscoveryDialog(QWidget *parent) : QDialog(parent)
682684
connect(m_manualHostEdit, &QLineEdit::returnPressed, this, &OnvifDiscoveryDialog::onAddManualClicked);
683685
connect(m_manualPortEdit, &QLineEdit::returnPressed, this, &OnvifDiscoveryDialog::onAddManualClicked);
684686

687+
auto *mediaSourceRow = new QHBoxLayout();
688+
m_addMediaSourceCheck = new QCheckBox(obs_module_text("PTZ.ONVIF.Discovery.CreateMediaSource"), this);
689+
m_addMediaSourceCheck->setChecked(true);
690+
m_streamCombo = new QComboBox(this);
691+
m_streamCombo->setEnabled(false);
692+
m_streamCombo->setMinimumContentsLength(20);
693+
mediaSourceRow->addWidget(m_addMediaSourceCheck);
694+
mediaSourceRow->addWidget(m_streamCombo, 1);
695+
layout->addLayout(mediaSourceRow);
696+
685697
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
686698
m_okButton = buttons->button(QDialogButtonBox::Ok);
687699
m_okButton->setText(obs_module_text("PTZ.ONVIF.Discovery.UseCamera"));
@@ -769,6 +781,8 @@ void OnvifDiscoveryDialog::onSelectionChanged()
769781
m_selected = OnvifCameraInfo();
770782
m_streams.clear();
771783
m_streamStatus.clear();
784+
m_streamCombo->clear();
785+
m_streamCombo->setEnabled(false);
772786
m_detailView->clear();
773787
return;
774788
}
@@ -779,6 +793,8 @@ void OnvifDiscoveryDialog::onSelectionChanged()
779793
m_okButton->setEnabled(true);
780794
m_streams.clear();
781795
m_streamStatus.clear();
796+
m_streamCombo->clear();
797+
m_streamCombo->setEnabled(false);
782798
rebuildDetail();
783799
maybeProbeStreams();
784800
}
@@ -811,6 +827,15 @@ void OnvifDiscoveryDialog::onStreamsReady(const QList<OnvifMediaProbe::StreamInf
811827
m_streams = streams;
812828
m_streamStatus.clear();
813829
rebuildDetail();
830+
/* Re-populate the stream picker so the user can choose Main vs Sub
831+
* for the auto-created Media Source. Falls back to disabled when no
832+
* streams are available. */
833+
m_streamCombo->clear();
834+
for (const auto &s : m_streams) {
835+
QString label = s.profileName.isEmpty() ? s.profileToken : s.profileName;
836+
m_streamCombo->addItem(label, s.uri);
837+
}
838+
m_streamCombo->setEnabled(m_streamCombo->count() > 0);
814839
}
815840

816841
void OnvifDiscoveryDialog::onStreamProbeError(const QString &msg)
@@ -882,3 +907,22 @@ QString OnvifDiscoveryDialog::selectedPassword() const
882907
{
883908
return m_passEdit ? m_passEdit->text() : QString();
884909
}
910+
911+
QString OnvifDiscoveryDialog::selectedStreamUri() const
912+
{
913+
if (m_streamCombo && m_streamCombo->currentIndex() >= 0) {
914+
QString uri = m_streamCombo->currentData().toString();
915+
if (!uri.isEmpty())
916+
return uri;
917+
}
918+
for (const auto &s : m_streams) {
919+
if (!s.uri.isEmpty())
920+
return s.uri;
921+
}
922+
return QString();
923+
}
924+
925+
bool OnvifDiscoveryDialog::createMediaSourceRequested() const
926+
{
927+
return m_addMediaSourceCheck && m_addMediaSourceCheck->isChecked();
928+
}

src/onvif-discovery.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#include <QList>
1818
#include <QMetaType>
1919

20+
class QCheckBox;
21+
class QComboBox;
2022
class QNetworkReply;
2123
class QTableWidget;
2224
class QLineEdit;
@@ -132,6 +134,12 @@ class OnvifDiscoveryDialog : public QDialog {
132134
OnvifCameraInfo selectedCamera() const { return m_selected; }
133135
QString selectedUsername() const;
134136
QString selectedPassword() const;
137+
/* Returns the first usable RTSP stream URI we fetched (empty if
138+
* streams weren't probed or all came back empty). */
139+
QString selectedStreamUri() const;
140+
/* True if the user wants us to also create an OBS Media Source from
141+
* the picked stream. */
142+
bool createMediaSourceRequested() const;
135143

136144
private slots:
137145
void onRescanClicked();
@@ -160,6 +168,8 @@ private slots:
160168
QLineEdit *m_manualHostEdit;
161169
QLineEdit *m_manualPortEdit;
162170
QPushButton *m_addManualButton;
171+
QCheckBox *m_addMediaSourceCheck;
172+
QComboBox *m_streamCombo;
163173
QPlainTextEdit *m_detailView;
164174
QList<OnvifCameraInfo> m_cameras;
165175
OnvifCameraInfo m_selected;

src/ptz-onvif.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,7 @@ void PTZOnvif::handleResponse(QString response)
333333
* GetStreamUri responses with `&channel=`/`&protocol=`). That's
334334
* invalid XML and Qt's QDomDocument rejects the whole document.
335335
* Replace any bare '&' not starting a known entity with '&amp;'. */
336-
static const QRegularExpression ampFix(
337-
QStringLiteral("&(?!(?:amp|lt|gt|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);)"));
336+
static const QRegularExpression ampFix(QStringLiteral("&(?!(?:amp|lt|gt|quot|apos|#[0-9]+|#x[0-9a-fA-F]+);)"));
338337
response.replace(ampFix, "&amp;");
339338

340339
QDomDocument doc;
@@ -865,8 +864,8 @@ obs_properties_t *PTZOnvif::get_obs_properties()
865864
obs_properties_add_float_slider(config, "speed_boost", obs_module_text("PTZ.ONVIF.SpeedBoost"), 0.1, 10.0,
866865
0.01);
867866
obs_property_t *prof = obs_properties_add_list(config, "profile_token",
868-
obs_module_text("PTZ.ONVIF.MediaProfile"),
869-
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
867+
obs_module_text("PTZ.ONVIF.MediaProfile"), OBS_COMBO_TYPE_LIST,
868+
OBS_COMBO_FORMAT_STRING);
870869
if (m_mediaProfiles.isEmpty()) {
871870
obs_property_list_add_string(prof, obs_module_text("PTZ.ONVIF.NoProfilesYet"), "");
872871
obs_property_set_enabled(prof, false);

src/settings.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,37 @@ void PTZSettings::on_addPTZ_clicked()
429429
if (!p.isEmpty())
430430
obs_data_set_string(cfg, "password", QT_TO_UTF8(p));
431431
ptzDeviceList.make_device(cfg);
432+
433+
/* Optionally create an OBS Media Source from the
434+
* camera's first RTSP stream URI and drop it into
435+
* the current scene. Embeds Basic-auth credentials
436+
* into the URL so it survives token expiry. */
437+
QString uri = dlg.selectedStreamUri();
438+
if (dlg.createMediaSourceRequested() && !uri.isEmpty()) {
439+
QUrl url(uri);
440+
if (url.isValid() && !u.isEmpty()) {
441+
url.setUserName(u);
442+
if (!p.isEmpty())
443+
url.setPassword(p);
444+
}
445+
QString sourceName = cam.name.isEmpty() ? QString("ONVIF %1").arg(cam.host) : cam.name;
446+
OBSData settings = obs_data_create();
447+
obs_data_release(settings);
448+
obs_data_set_string(settings, "input", QT_TO_UTF8(url.toString()));
449+
obs_data_set_bool(settings, "is_local_file", false);
450+
obs_data_set_bool(settings, "restart_on_activate", true);
451+
obs_data_set_bool(settings, "hw_decode", true);
452+
obs_data_set_string(settings, "input_format", "");
453+
454+
OBSSourceAutoRelease source =
455+
obs_source_create("ffmpeg_source", QT_TO_UTF8(sourceName), settings, nullptr);
456+
if (source) {
457+
OBSSourceAutoRelease sceneSrc = obs_frontend_get_current_scene();
458+
obs_scene_t *scene = sceneSrc ? obs_scene_from_source(sceneSrc) : nullptr;
459+
if (scene)
460+
obs_scene_add(scene, source);
461+
}
462+
}
432463
}
433464
}
434465
#endif

0 commit comments

Comments
 (0)