load_graph/stop_node/start_node each called notify() (push to all
clients) and then sent a direct response — the requesting client
got two identical messages, breaking test recv sequencing.
Use thread_local tl_requester to skip the requesting connection
in the push. notify() is called synchronously from the command,
so the thread_local is visible from the status_cb. Other clients
still receive the push; the requester gets only the direct response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers connection, protocol robustness, get_status, load_graph,
stop/start_node, crash detection, and multi-client push.
Runs against a live studio-manager; --skip-nodes skips tests
that need node binaries and MXL.
Run: ./tests/.venv/bin/python3 tests/test_ws_api.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GetDisplayName returns CFStringRef on macOS (not const char*).
Add #ifdef __APPLE__ handling in DeckLinkSender.hpp and
DeckLinkReceiver.hpp, and link -framework CoreFoundation in
decklinkin/decklinkout CMakeLists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Linux, GetBytes lives on IDeckLinkVideoBuffer (via QueryInterface),
not directly on IDeckLinkVideoInputFrame. Also restores StartAccess/
EndAccess around the copy for correct buffer lifecycle management.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DeckLinkReceiver:
- Group private members with section comments (SDK objects / sync / frame state)
- Change wait_for_format timeout param from uint64_t to int (matches
std::chrono::milliseconds and all call sites)
decklinkin main.cpp:
- Wrap start_capture in try/catch — logs error and returns cleanly on
device init failure (consistent with NDIInNode pattern)
- Remove redundant zero-init on frame_buf (vector<uint8_t> zero-inits anyway)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DeckLinkReceiver:
- Make InputCallback a private nested class — no longer exposed publicly
- DeckLinkReceiver owns all shared state (mutex, CVs, frame buffer)
- Replace get_input_callback() with clean wait_for_frame() API
- Fix device list leak: enumerate_devices stores raw IDeckLink* in
raw_devices; destructor releases all of them + selected_device's AddRef
- Remove dead members: device_config, device_status, deckLink_notification
- Remove dead SourceInfo::stride field; rename SourceInfo → VideoInfo
- Remove dead video_source_info public member
- Remove printf; use no logging in receiver (caller logs)
- Remove commented-out notification code
- Fix dead return false after throw in enumerate_devices
- Fix dead null check after new InputCallback
- Replace IDeckLinkVideoBuffer QueryInterface with simpler GetBytes()
- Replace plain bool frame_ready/format_detected with consistent usage
under mutex (no longer mixing atomic + CV pattern)
- Call StopStreams/DisableVideoInput in destructor
- Consistent snake_case naming throughout
decklinkin main.cpp:
- Rename NodeDeckLinkIn → DeckLinkInNode
- Get device_index from config().value("device_index", 0u)
- Remove unused includes: <time.h>, <algorithm>, <cstdio>, <DeckLinkAPI.h>
- Use clean receiver.wait_for_frame() instead of reaching into callback
- Use mxlGetCurrentIndex resync after sleep (consistent with other nodes)
- Fix return node.execute() (was node.execute(); return 0)
- Fix main() spacing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After each video frame sleep, use mxlGetCurrentIndex instead of a
simple +1 increment — if audio processing ate into the next frame's
time we now skip the stale index rather than writing a late grain.
Same pattern ndiin already uses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard swr_close/swr_init at EOF inside if (has_audio) — calling
swr_init(nullptr) on a video-only file crashed at first loop
- Change open_file from bool to void — it never returned false, only
threw, so the if (!open_file()) check in the constructor was dead code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VideoReader:
- Guard get_source_info/allocate_conversion_buffers behind have_video
check — prevents crash on audio-only files
- Remove unused audiobuf parameter from get_next_frame
- Remove dead return statements after throw
- Remove unused SourceInfo::stride field and AudioInfo struct
- Pass const std::string& instead of by value in constructor/open_file
- Remove redundant struct keyword on SourceInfo source_info{}
- Fix video_stream_index never guarded against -1 in open_file
- Check av_image_alloc and avcodec_parameters_to_context return values
- Remove extra av_packet_unref after seek (was harmless but confusing)
- Use SWS_BILINEAR for sws_getContext flags instead of 0
- Replace NULL with nullptr in sws_getContext
- Remove unused #include <libavutil/pixdesc.h>
videoin main.cpp:
- Early return if have_video is false after open
- Inline make_video_flow_def call (remove intermediate variable)
- Add grain count log line matching other nodes
- Change continue to break when get_next_frame returns false
- Make video_rate const
- Remove double blank line before main()
- Remove trailing spaces
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass MXL grain stride (from sliceSizes[0]) into get_next_frame so
YUV422P10toV210 writes with the correct line width instead of a
self-computed value that may not match the MXL buffer.
Fix audio stream detection: condition was inverted (!= -1 → == -1),
so the first audio stream was never picked up.
Add return false at end of get_next_frame to fix UB when g_running
goes false and the loop exits without returning.
Replace av_frame_unref/av_packet_unref with av_frame_free/av_packet_free
in destructor — unref only releases data, not the struct itself.
Add av_freep(&p10_data[0]) in destructor to free av_image_alloc memory.
Remove unused p10_buffer and v210_buffer allocations.
Read filename from config("file") instead of hardcoded path.
Add early return if mxlCreateFlowWriter fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Edges that share the same source node and port now get the same MXL flow
UUID. One writer, multiple independent readers — each tracks its own
position in the ring buffer. Enables 1-to-N routing in graph.json without
any splitter node.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
studio-manager now accepts an optional path argument:
dmf-studio-manager graph.json
load_graph() parses nodes and edges from JSON, generating fresh UUIDs
for each MXL flow on every launch. Falls back to hardcoded build_graph()
when no argument is given.
graph.json at repo root defines the current testpattern→ndiout pipeline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Readers (ndiout, fakesink) now handle MXL_ERR_FLOW_INVALID by releasing
and recreating the flow reader, then realigning the index to current time.
This lets consumer nodes survive a producer restart without exiting.
All mxlCreateInstance/FlowWriter/FlowReader options args changed from
nullptr to "" to match MXL reference implementation style.
mxlReleaseFlowReader calls guarded with null checks so cleanup is safe
when a mid-run reconnect attempt fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
studio-manager: call mxlGarbageCollectFlows before launching nodes to
clean up stale flow directories left by previous crashed runs.
shared/NodeBase.hpp: add mxl_status_str(mxlStatus) — converts error
codes to readable names (e.g. MXL_ERR_OUT_OF_RANGE_TOO_LATE). All
nodes now log these names instead of raw integers.
shared/FlowDef.hpp: use "audio/float32" as media_type for 32-bit audio
flows, matching the MXL SDK examples. audio/L{n} kept for other depths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flow_id is now optional (config().contains). When absent: skip the
MXL reader, active-wait, and NDI video path entirely. Video buffers
and NDI video frames are heap-allocated only when has_video.
Loop pacing: video branch sleeps 1ms on TOO_EARLY as before; audio-only
path sleeps 1ms when no chunk was available (audio_advanced == false)
to avoid busy-spinning.
NDI sender name changed from the video flow UUID to node_id(), which
is stable and human-readable for both video and audio-only modes.
To use audio-only: wire only audio_flow_id in build_graph, omit flow_id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>