libpromeki 1.0.0-alpha
PROfessional MEdia toolKIt
 
Loading...
Searching...
No Matches
ndimediaio.h
Go to the documentation of this file.
1
8#pragma once
9
10
11#include <promeki/config.h>
12#if PROMEKI_ENABLE_NDI
13#include <promeki/namespace.h>
14#include <promeki/atomic.h>
15#include <promeki/audiobuffer.h>
16#include <promeki/basicthread.h>
17#include <promeki/audiodesc.h>
18#include <promeki/audiomarker.h>
19#include <promeki/clock.h>
21#include <promeki/framenumber.h>
22#include <promeki/framerate.h>
23#include <promeki/imagedesc.h>
24#include <promeki/mediadesc.h>
28#include <promeki/metadata.h>
29#include <promeki/mutex.h>
30#include <promeki/ndiclock.h>
31#include <promeki/pacinggate.h>
33#include <promeki/queue.h>
34#include <promeki/string.h>
36
37// Forward-declare the NDI SDK's opaque instance types so we can hold
38// instance handles in the class body without dragging the full SDK
39// headers into the public include. The .cpp pulls in
40// <Processing.NDI.Lib.h> to actually drive the API.
41struct NDIlib_send_instance_type;
42struct NDIlib_recv_instance_type;
43
44PROMEKI_NAMESPACE_BEGIN
45
124class NdiMediaIO : public DedicatedThreadMediaIO {
125 PROMEKI_OBJECT(NdiMediaIO, DedicatedThreadMediaIO)
126 public:
128 static inline const MediaIOStats::ID StatsFramesSent{"NdiFramesSent"};
130 static inline const MediaIOStats::ID StatsAudioFramesSent{"NdiAudioFramesSent"};
132 static inline const MediaIOStats::ID StatsBytesSent{"NdiBytesSent"};
133
135 static inline const MediaIOStats::ID StatsFramesReceived{"NdiFramesReceived"};
137 static inline const MediaIOStats::ID StatsAudioFramesReceived{"NdiAudioFramesReceived"};
139 static inline const MediaIOStats::ID StatsMetadataReceived{"NdiMetadataReceived"};
141 static inline const MediaIOStats::ID StatsDroppedReceives{"NdiDroppedReceives"};
148 static inline const MediaIOStats::ID StatsAudioSilenceFilled{"NdiAudioSilenceFilled"};
154 static inline const MediaIOStats::ID StatsAudioGapEvents{"NdiAudioGapEvents"};
155
182 static MediaIOAllocator::Ptr makePinnedHostAllocator();
183
185 NdiMediaIO(ObjectBase *parent = nullptr);
186
188 ~NdiMediaIO() override;
189
199 int instanceID() const { return _instanceId; }
200
213 Error proposeInput(const MediaDesc &offered, MediaDesc *preferred) const override;
214
215 protected:
216 Error executeCmd(MediaIOCommandOpen &cmd) override;
217 Error executeCmd(MediaIOCommandClose &cmd) override;
218 Error executeCmd(MediaIOCommandRead &cmd) override;
219 Error executeCmd(MediaIOCommandWrite &cmd) override;
220 Error executeCmd(MediaIOCommandStats &cmd) override;
250 Error executeCmd(MediaIOCommandSetClock &cmd) override;
251 void cancelBlockingWork() override;
252
253 private:
254 Error openSink(const MediaIO::Config &cfg, const MediaDesc &mediaDesc);
255 void closeSink();
256
257 Error openSource(const MediaIO::Config &cfg);
258 void closeSource();
259 void captureLoop();
260
261 Error sendVideo(const UncompressedVideoPayload &vp);
262 Error sendAudio(const PcmAudioPayload &ap);
263
302 void ingestNdiAudio(int64_t timestampTicks, size_t samples, size_t channels,
303 float rate, const uint8_t *planarFloatData,
304 size_t channelStrideBytes);
305
306 friend struct NdiMediaIOTestAccess;
307
308 // NDI handles — opaque pointers to SDK-managed state.
309 NDIlib_send_instance_type *_send = nullptr;
310 NDIlib_recv_instance_type *_recv = nullptr;
311
312 // Resolved at openSink() — used to pre-allocate the FLTP
313 // conversion buffer once per stream rather than per frame.
314 size_t _audioChannels = 0;
315 float _audioSampleRate = 0.0f;
316
317 // Configuration captured at open time.
318 String _sendName;
319 String _sendGroups;
320 String _extraIps;
321 bool _sendClockVideo = false;
322 bool _sendClockAudio = false;
323 bool _sinkMode = false;
324
325 // Resolved video shape — populated at openSink time.
326 ImageDesc _imageDesc;
327 FrameRate _frameRate;
328
329 // Telemetry — atomic so executeCmd(Stats) can read without
330 // racing the strand worker that mutates these.
331 Atomic<int64_t> _framesSent{0};
332 Atomic<int64_t> _audioFramesSent{0};
333 Atomic<int64_t> _bytesSent{0};
334
335 // ---- Source-mode state ----
336 //
337 // The capture thread loops on @c NDIlib_recv_capture_v3
338 // with a short timeout, demultiplexes the returned
339 // frames into the queues / ring below, and frees the
340 // SDK buffers via the matching recv_free_* call. All
341 // queues are mutex-protected internally; the strand
342 // drains them in @c executeCmd(MediaIOCommandRead).
343 BasicThread _captureThread;
344 int _instanceId = 0;
345 Atomic<bool> _stopFlag{false};
346 Atomic<bool> _readCancelled{false};
347 // Reader-side video queue. Capacity is small (matches
348 // V4L2's "drop oldest, count drop") because NDI pushes
349 // at the source's true frame rate and we want timing-
350 // accurate delivery rather than a long backlog.
351 static constexpr int VideoQueueDepth = 2;
352 Queue<UncompressedVideoPayload::Ptr> _videoQueue;
353 AudioBuffer _audioRing;
354 Mutex _audioMutex;
355 // Predicted timestamp for the next arriving sample,
356 // computed as @c lastFrame.timestamp + samples * 1e7
357 // / rate. Stays in NDI 100ns ticks; guarded by
358 // @ref _audioMutex. The capture thread compares each
359 // new frame's timestamp against this value to detect
360 // gaps; the gap (if positive and within sanity
361 // bounds) is bridged with
362 // @ref AudioBuffer::pushSilence so the ring's sample
363 // count and the sender's media timeline stay in
364 // sync. Zero when no prior frame has been pushed
365 // since the last reset.
366 //
367 // The PTS of the first sample currently in the ring
368 // is recovered directly from AudioBuffer's anchor
369 // queue on drain — push() lays a sender-anchored
370 // MediaTimeStamp per timestamped frame, so coalesced
371 // multi-frame drains correctly report the oldest
372 // still-buffered timestamp without any additional
373 // tracking here.
374 int64_t _audioNextSampleTicks = 0;
375 // Markers accumulated for the next drained payload.
376 // Each entry's @c offset is the sample index within
377 // the eventual payload (i.e. the value of
378 // @c _audioRing.available() at the moment the marker
379 // was appended); @c length is the sample count
380 // covered. The list is stamped onto the drained
381 // payload's @ref Metadata::AudioMarkers in
382 // executeCmd(Read) and then cleared.
383 AudioMarkerList _audioMarkersSinceDrain;
384 // Total samples of silence injected by the gap-fill
385 // logic since the last open. Atomic so the strand's
386 // executeCmd(Stats) can publish it without taking
387 // @ref _audioMutex. Mirrored as
388 // @ref StatsAudioSilenceFilled.
389 Atomic<int64_t> _audioSilenceSamples{0};
390 // Total contiguous silence-fill events since the
391 // last open. Mirrored as @ref StatsAudioGapEvents.
392 Atomic<int64_t> _audioGapEvents{0};
393 Mutex _metadataMutex;
394 Metadata _pendingMetadata;
395 bool _hasPendingMetadata = false;
396
397 // Reader configuration captured at openSource time.
398 // The bit-depth tag controls how P216 frames are tagged
399 // when emitted (see @ref NdiReceiveBitDepth).
400 int _captureTimeoutMs = 100;
401 int _bitDepthHint = 0; // 0 = Auto (16-bit).
402
403 // Reader-side telemetry.
404 Atomic<int64_t> _framesReceived{0};
405 Atomic<int64_t> _audioFramesReceived{0};
406 Atomic<int64_t> _metadataReceived{0};
407 Atomic<int64_t> _droppedReceives{0};
408
409 // Source-mode clock — driven by NDI per-frame
410 // timestamps from the capture thread. Owned via
411 // Clock::Ptr so the bound port-group can keep it
412 // alive past close if any consumer still holds a
413 // reference.
414 Clock::Ptr _sourceClock;
415
416 // External sink-mode pacing — one PacingGate per
417 // stream so video and audio can drift independently
418 // around the same clock (a single gate would conflate
419 // their timelines). Both gates share the same Clock
420 // bound via executeCmd(MediaIOCommandSetClock). The
421 // gates' wait() runs on the dedicated worker thread
422 // inside sendVideo / sendAudio (same thread as the
423 // setter, so no synchronization required). When no
424 // clock is bound the gates are no-ops and the SDK's
425 // internal clock_video / clock_audio flags remain in
426 // control.
427 PacingGate _videoGate;
428 PacingGate _audioGate;
429};
430
435class NdiFactory : public MediaIOFactory {
436 public:
437 NdiFactory() = default;
438
439 String name() const override { return String("Ndi"); }
440 String displayName() const override { return String("NDI Stream"); }
441 String description() const override {
442 return String("NDI (Network Device Interface) media transport "
443 "(uncompressed video + audio + metadata over IP)");
444 }
445
446 bool canBeSource() const override { return true; }
447 bool canBeSink() const override { return true; }
448
449 StringList schemes() const override { return {String("ndi")}; }
450 bool canHandlePath(const String &path) const override;
451 StringList enumerate() const override;
452 Error urlToConfig(const Url &url, Config *outConfig) const override;
453
454 Config::SpecMap configSpecs() const override;
455
456 MediaIO *create(const Config &config, ObjectBase *parent = nullptr) const override;
457};
458
459PROMEKI_NAMESPACE_END
460
461
462#endif // PROMEKI_ENABLE_NDI