Skip to content

Commit 7c049eb

Browse files
authored
feat: Add NDI Discovery Server capability (#1702)
1 parent 480e9b0 commit 7c049eb

File tree

6 files changed

+115
-24
lines changed

6 files changed

+115
-24
lines changed

src/modules/newtek/consumer/newtek_ndi_consumer.cpp

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
#include <common/assert.h>
3737
#include <common/diagnostics/graph.h>
38+
#include <common/env.h>
3839
#include <common/except.h>
3940
#include <common/executor.h>
4041
#include <common/future.h>
@@ -57,10 +58,13 @@ struct newtek_ndi_consumer : public core::frame_consumer
5758
const int instance_no_;
5859
const std::wstring name_;
5960
const bool allow_fields_;
61+
const std::string discovery_server_url_;
62+
const bool use_advertiser_;
63+
const bool allow_monitoring_;
6064

6165
core::video_format_desc format_desc_;
6266
int channel_index_;
63-
NDIlib_v5* ndi_lib_;
67+
NDIlib_v6* ndi_lib_;
6468
NDIlib_video_frame_v2_t ndi_video_frame_;
6569
NDIlib_audio_frame_interleaved_32s_t ndi_audio_frame_;
6670
std::shared_ptr<uint8_t> field_data_;
@@ -78,13 +82,17 @@ struct newtek_ndi_consumer : public core::frame_consumer
7882
executor executor_;
7983

8084
std::unique_ptr<NDIlib_send_instance_t, std::function<void(NDIlib_send_instance_t*)>> ndi_send_instance_;
85+
std::unique_ptr<NDIlib_send_advertiser_instance_t, std::function<void(NDIlib_send_advertiser_instance_t*)>> ndi_advertiser_instance_;
8186

8287
public:
83-
newtek_ndi_consumer(std::wstring name, bool allow_fields)
88+
newtek_ndi_consumer(std::wstring name, bool allow_fields, std::string discovery_server_url = "", bool use_advertiser = false, bool allow_monitoring = true)
8489
: name_(!name.empty() ? name : default_ndi_name())
8590
, instance_no_(instances_++)
8691
, frame_no_(0)
8792
, allow_fields_(allow_fields)
93+
, discovery_server_url_(discovery_server_url)
94+
, use_advertiser_(use_advertiser)
95+
, allow_monitoring_(allow_monitoring)
8896
, channel_index_(0)
8997
, executor_(L"ndi_consumer[" + std::to_wstring(instance_no_) + L"]")
9098
{
@@ -114,6 +122,9 @@ struct newtek_ndi_consumer : public core::frame_consumer
114122
format_desc_ = format_desc;
115123
channel_index_ = channel_info.index;
116124

125+
// Make sure to stop the advertiser before recreating the sender
126+
ndi_advertiser_instance_.reset();
127+
117128
NDIlib_send_create_t NDI_send_create_desc;
118129

119130
auto tmp_name = u8(name_);
@@ -125,6 +136,51 @@ struct newtek_ndi_consumer : public core::frame_consumer
125136
ndi_send_instance_ = {new NDIlib_send_instance_t(ndi_lib_->send_create(&NDI_send_create_desc)),
126137
[this](auto p) { this->ndi_lib_->send_destroy(*p); }};
127138

139+
// Create and configure NDI advertiser if enabled
140+
if (use_advertiser_) {
141+
if (!ndi_lib_->send_advertiser_create) {
142+
CASPAR_LOG(warning) << L"NDI advertiser requested but not supported by this NDI SDK version (requires NDI 5.5+)";
143+
} else {
144+
// Use constructor for proper initialization
145+
NDIlib_send_advertiser_create_t advertiser_create_desc(
146+
discovery_server_url_.empty() ? nullptr : discovery_server_url_.c_str()
147+
);
148+
149+
auto advertiser_instance = ndi_lib_->send_advertiser_create(&advertiser_create_desc);
150+
151+
if (!advertiser_instance) {
152+
CASPAR_LOG(warning) << L"Failed to create NDI advertiser for sender '" << name_ << L"'"
153+
<< (discovery_server_url_.empty()
154+
? L" (using default discovery)"
155+
: L" with server: " + u16(discovery_server_url_));
156+
} else {
157+
ndi_advertiser_instance_ = {
158+
new NDIlib_send_advertiser_instance_t(advertiser_instance),
159+
[this](auto p) {
160+
if (p && *p && this->ndi_lib_->send_advertiser_del_sender && this->ndi_lib_->send_advertiser_destroy) {
161+
// Remove sender before destroying advertiser
162+
this->ndi_lib_->send_advertiser_del_sender(*p, *ndi_send_instance_);
163+
this->ndi_lib_->send_advertiser_destroy(*p);
164+
}
165+
}
166+
};
167+
168+
bool added = ndi_lib_->send_advertiser_add_sender(
169+
*ndi_advertiser_instance_,
170+
*ndi_send_instance_,
171+
allow_monitoring_
172+
);
173+
174+
if (added) {
175+
CASPAR_LOG(info) << L"NDI sender '" << name_ << L"' registered with discovery server"
176+
<< (discovery_server_url_.empty() ? L"" : L" at " + u16(discovery_server_url_));
177+
} else {
178+
CASPAR_LOG(warning) << L"Failed to register NDI sender '" << name_ << L"' with advertiser (sender may already be registered)";
179+
}
180+
}
181+
}
182+
}
183+
128184
ndi_video_frame_.xres = format_desc.width;
129185
ndi_video_frame_.yres = format_desc.height;
130186
ndi_video_frame_.frame_rate_N = format_desc.framerate.numerator() * format_desc.field_count;
@@ -250,8 +306,11 @@ struct newtek_ndi_consumer : public core::frame_consumer
250306
core::monitor::state state() const override
251307
{
252308
core::monitor::state state;
253-
state["ndi/name"] = name_;
254-
state["ndi/allow_fields"] = allow_fields_;
309+
state["ndi/name"] = name_;
310+
state["ndi/allow_fields"] = allow_fields_;
311+
state["ndi/use_advertiser"] = use_advertiser_;
312+
state["ndi/allow_monitoring"] = allow_monitoring_;
313+
state["ndi/discovery_server_url"] = discovery_server_url_;
255314
return state;
256315
}
257316
};
@@ -270,9 +329,16 @@ create_ndi_consumer(const std::vector<std::wstring>& par
270329
if (channel_info.depth != common::bit_depth::bit8)
271330
CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info("Newtek NDI consumer only supports 8-bit color depth."));
272331

273-
std::wstring name = get_param(L"NAME", params, L"");
274-
bool allow_fields = contains_param(L"ALLOW_FIELDS", params);
275-
return spl::make_shared<newtek_ndi_consumer>(name, allow_fields);
332+
std::wstring name = get_param(L"NAME", params, L"");
333+
bool allow_fields = contains_param(L"ALLOW_FIELDS", params);
334+
bool use_advertiser = contains_param(L"USE_ADVERTISER", params);
335+
bool allow_monitoring = get_param(L"ALLOW_MONITORING", params, true);
336+
std::wstring discovery_server_url_w = get_param(L"DISCOVERY_SERVER", params, L"");
337+
if (discovery_server_url_w.empty())
338+
discovery_server_url_w = env::properties().get(L"configuration.ndi.discovery-server", L"");
339+
std::string discovery_server_url = ndi::apply_default_discovery_port(u8(discovery_server_url_w));
340+
341+
return spl::make_shared<newtek_ndi_consumer>(name, allow_fields, discovery_server_url, use_advertiser, allow_monitoring);
276342
}
277343

278344
spl::shared_ptr<core::frame_consumer>
@@ -281,13 +347,19 @@ create_preconfigured_ndi_consumer(const boost::property_tree::wptree&
281347
const std::vector<spl::shared_ptr<core::video_channel>>& channels,
282348
const core::channel_info& channel_info)
283349
{
284-
auto name = ptree.get(L"name", L"");
285-
bool allow_fields = ptree.get(L"allow-fields", false);
350+
auto name = ptree.get(L"name", L"");
351+
bool allow_fields = ptree.get(L"allow-fields", false);
352+
bool use_advertiser = ptree.get(L"use-advertiser", false);
353+
bool allow_monitoring = ptree.get(L"allow-monitoring", true);
354+
std::wstring discovery_server_url_w = ptree.get(L"discovery-server", L"");
355+
if (discovery_server_url_w.empty())
356+
discovery_server_url_w = env::properties().get(L"configuration.ndi.discovery-server", L"");
357+
std::string discovery_server_url = ndi::apply_default_discovery_port(u8(discovery_server_url_w));
286358

287359
if (channel_info.depth != common::bit_depth::bit8)
288360
CASPAR_THROW_EXCEPTION(caspar_exception() << msg_info("Newtek NDI consumer only supports 8-bit color depth."));
289361

290-
return spl::make_shared<newtek_ndi_consumer>(name, allow_fields);
362+
return spl::make_shared<newtek_ndi_consumer>(name, allow_fields, discovery_server_url, use_advertiser, allow_monitoring);
291363
}
292364

293365
}} // namespace caspar::newtek

src/modules/newtek/interop/Processing.NDI.Send.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,4 @@ void NDIlib_send_set_failover(NDIlib_send_instance_t p_instance, const NDIlib_so
143143
// Retrieve the source information for the given sender instance. This pointer is valid until NDIlib_send_destroy is called.
144144
PROCESSINGNDILIB_API
145145
const NDIlib_source_t* NDIlib_send_get_source_name(NDIlib_send_instance_t p_instance);
146+

src/modules/newtek/producer/newtek_ndi_producer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ struct newtek_ndi_producer : public core::frame_producer
7676

7777
spl::shared_ptr<core::frame_factory> frame_factory_;
7878
core::video_format_desc format_desc_;
79-
NDIlib_v5* ndi_lib_;
79+
NDIlib_v6* ndi_lib_;
8080
NDIlib_framesync_instance_t ndi_framesync_;
8181
NDIlib_recv_instance_t ndi_recv_instance_;
8282
spl::shared_ptr<diagnostics::graph> graph_;

src/modules/newtek/util/ndi.cpp

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ const std::wstring& dll_name()
5050
static std::mutex find_instance_mutex;
5151
static std::shared_ptr<NDIlib_find_instance_t> find_instance;
5252

53-
NDIlib_v5* load_library()
53+
NDIlib_v6* load_library()
5454
{
55-
static NDIlib_v5* ndi_lib = nullptr;
55+
static NDIlib_v6* ndi_lib = nullptr;
5656

5757
if (ndi_lib)
5858
return ndi_lib;
@@ -61,24 +61,30 @@ NDIlib_v5* load_library()
6161
const char* runtime_dir = getenv(NDILIB_REDIST_FOLDER);
6262

6363
#ifdef _WIN32
64-
auto module = LoadLibrary(dll_path.c_str());
64+
HMODULE module = LoadLibrary(dll_path.c_str());
6565

6666
if (!module && runtime_dir) {
6767
dll_path = boost::filesystem::path(runtime_dir) / NDILIB_LIBRARY_NAME;
6868
module = LoadLibrary(dll_path.c_str());
6969
}
7070

71-
FARPROC NDIlib_v5_load = NULL;
71+
FARPROC NDIlib_v6_load = NULL;
7272
if (module) {
7373
CASPAR_LOG(info) << L"Loaded " << dll_path;
7474
static std::shared_ptr<void> lib(module, FreeLibrary);
75-
NDIlib_v5_load = GetProcAddress(module, "NDIlib_v5_load");
75+
NDIlib_v6_load = GetProcAddress(module, "NDIlib_v6_load");
7676
}
7777

78-
if (!NDIlib_v5_load) {
78+
if (!NDIlib_v6_load) {
7979
not_installed();
8080
}
8181

82+
ndi_lib = (NDIlib_v6*)(((const NDIlib_v6* (*)(void))NDIlib_v6_load)());
83+
84+
if (!ndi_lib->NDIlib_initialize()) {
85+
not_initialized();
86+
}
87+
8288
#else
8389
// Try to load the library
8490
void* hNDILib = dlopen(NDILIB_LIBRARY_NAME, RTLD_LOCAL | RTLD_LAZY);
@@ -89,24 +95,23 @@ NDIlib_v5* load_library()
8995
}
9096

9197
// The main NDI entry point for dynamic loading if we got the library
92-
const NDIlib_v5* (*NDIlib_v5_load)(void) = NULL;
98+
const NDIlib_v6* (*NDIlib_v6_load)(void) = NULL;
9399
if (hNDILib) {
94100
CASPAR_LOG(info) << L"Loaded " << dll_path;
95101
static std::shared_ptr<void> lib(hNDILib, dlclose);
96-
*((void**)&NDIlib_v5_load) = dlsym(hNDILib, "NDIlib_v5_load");
102+
*((void**)&NDIlib_v6_load) = dlsym(hNDILib, "NDIlib_v6_load");
97103
}
98104

99-
if (!NDIlib_v5_load) {
105+
if (!NDIlib_v6_load) {
100106
not_installed();
101107
}
102108

103-
#endif
104-
105-
ndi_lib = (NDIlib_v5*)(NDIlib_v5_load());
109+
ndi_lib = (NDIlib_v6*)(NDIlib_v6_load());
106110

107111
if (!ndi_lib->NDIlib_initialize()) {
108112
not_initialized();
109113
}
114+
#endif
110115

111116
NDIlib_find_create_t find_instance_options = {};
112117
find_instance_options.show_local_sources = true;
@@ -141,6 +146,14 @@ void not_initialized()
141146
CASPAR_THROW_EXCEPTION(not_supported() << msg_info("Unable to initialize NDI on this system."));
142147
}
143148

149+
std::string apply_default_discovery_port(std::string url)
150+
{
151+
if (!url.empty() && url.find(':') == std::string::npos) {
152+
url += ":5959";
153+
}
154+
return url;
155+
}
156+
144157
std::wstring list_command(protocol::amcp::command_context& ctx)
145158
{
146159
auto ndi_lib = load_library();

src/modules/newtek/util/ndi.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727
namespace caspar { namespace newtek { namespace ndi {
2828

2929
const std::wstring& dll_name();
30-
NDIlib_v5* load_library();
30+
NDIlib_v6* load_library();
3131
std::map<std::string, NDIlib_source_t> get_current_sources();
3232
void not_initialized();
3333
void not_installed();
34+
std::string apply_default_discovery_port(std::string url);
3435

3536
std::wstring list_command(protocol::amcp::command_context& ctx);
3637

src/shell/casparcg.config

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
</system-audio>
6666
<ndi>
6767
<auto-load>false [true|false]</auto-load>
68+
<discovery-server>[hostname:port] (Global NDI Discovery Server URL applied to all consumers. Per-consumer discovery-server overrides this value.)</discovery-server>
6869
</ndi>
6970
<video-modes>
7071
<video-mode>
@@ -159,6 +160,9 @@
159160
<ndi>
160161
<name>[custom name]</name>
161162
<allow-fields>false [true|false]</allow-fields>
163+
<use-advertiser>false [true|false]</use-advertiser>
164+
<allow-monitoring>true [true|false]</allow-monitoring>
165+
<discovery-server>[hostname:port] (NDI Discovery Server URL, comma-separated for multiple servers. Requires use-advertiser=true)</discovery-server>
162166
</ndi>
163167
<ffmpeg>
164168
<path>[file|url]</path>

0 commit comments

Comments
 (0)