A high-performance transcoder that converts NDI (Network Device Interface) video streams to SRT (Secure Reliable Transport) with embedded SMPTE 12M-1 LTC timecode metadata. The application is designed for professional broadcast workflows where accurate timecode preservation is critical. Vibe-coded using Cursor.
The app was built specifically with Canon PTZ cameras in mind (CR-N and CR-X families). Their NDI output is the single IP-based stream that has includes timestamps/timing metadata embedded that's derived from the internal NTP-synchronised RTC clock and isn't set to 0 at boot/stream start.
This means that it should be technically possible to use OBS with @ikenainanodesu OBS source plugin (https://github.com/ikenainanodesu/OBS-SEI-Stamper) to get +/- 1 frame synchronization with multiple cameras inside OBS.
P.S.: I've raised the issue with Canon's support team and the response doesn't give hope for proper SEI in SRT, RTMP or any other supported output stream any time soon. But in all fairness - as per engineering team's response - "With RTP, PTS contains a monotonic time starting from a random value for the initial frame (as following standard). Since the RTCP Sender Report includes both an RTC synchronized to NTP and the RTP timestamp, it is possible to use this for synchronization."
P.S. 2: It's up to the sender app to embed timecodes in NDI stream. The default seems to be no timecode data so with other camera brands YMMV.
NDI2SRT bridges the gap between NDI-based production environments and SRT streaming infrastructure, ensuring that timecode information from the source is properly embedded in the output H.264 stream for downstream processing by professional video tools.
- GStreamer: 1.20+ with base/good/bad/ugly plugins
- SRT Plugin: gst-plugins-bad with SRT support
- NDI SDK: NewTek NDI SDK and GStreamer NDI plugin providing
ndisrc - Platform Support: Linux (Debian/Ubuntu) and macOS
sudo apt-get update
sudo apt-get install -y cmake build-essential pkg-config \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav
# Install NDI SDK + GStreamer NDI plugin (varies by distro/vendor)brew install cmake pkg-config gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav
# Install NDI SDK + GStreamer NDI plugin (ensure `ndisrc` is available)
# Optionally use VideoToolbox encoder: vtenc_h264# Clone and build
git clone <repository-url>
cd ndi2srt
cmake -S . -B build
cmake --build build --config Release -j
# The binary will be available at: build/ndi2srtBefore connecting to an NDI source, you can discover what's available on your network:
# List available NDI sources
./ndi2srt --discoverThis will automatically scan your network for active NDI sources and display:
- Source Names: Exact names to use with
--ndi-name - Network Information: IP addresses and ports
- Device Classes: Source types (Audio/Video/Network)
Example output:
Available NDI sources:
=====================
1. "Studio Camera 1" (class: Source/Audio/Video/Network)
NDI Name: "Studio Camera 1"
URL Address: 192.168.1.100:5962
2. "OBS Virtual Camera" (class: Source/Audio/Video/Network)
NDI Name: "OBS Virtual Camera"
URL Address: 127.0.0.1:5962
# Caller mode (connect to remote SRT server)
./ndi2srt --ndi-name "My NDI Source" --srt-uri "srt://receiver:9000?mode=caller" \
--encoder x264enc --bitrate 6000 --zerolatency
# Listener mode (wait for SRT client connection)
./ndi2srt --ndi-name "My NDI Source" --srt-uri "srt://:9000?mode=listener" --encoder x264enc# Output MPEG-TS to stdout for further processing
./ndi2srt --ndi-name "My NDI Source" --stdout --timeout 10 | \
ffmpeg -i - -c copy output.mp4
# Real-time streaming to FFmpeg
./ndi2srt --ndi-name "My NDI Source" --stdout | \
ffmpeg -i - -f mpegts udp://127.0.0.1:1234# macOS using VideoToolbox hardware encoder
./ndi2srt --ndi-name "My NDI Source" --srt-uri "srt://receiver:9000?mode=caller" \
--encoder vtenc_h264 --bitrate 8000
# Custom timestamp mode for NDI source
./ndi2srt --ndi-name "My NDI Source" --srt-uri "srt://receiver:9000?mode=caller" \
--timestamp-mode timecode --encoder x264enc
# Custom GOP size for broadcast workflows
./ndi2srt --ndi-name "My NDI Source" --stdout --gop-size 25 --bitrate 8000 --timeout 10 > broadcast.ts
# Debug mode with verbose output
./ndi2srt --ndi-name "My NDI Source" --stdout --verbose --timeout 5 > output.ts 2>debug.logThe application provides a comprehensive set of command-line options organized into logical groups:
--ndi-name <name>- NDI source name to connect to
--srt-uri <uri>- SRT endpoint URI (srt://host:port?mode=caller)--stdout- Output MPEG-TS to stdout instead of SRT
--encoder <name>- Video encoder: x264enc, vtenc_h264, openh264enc--bitrate <kbps>- Video bitrate in kbps (default: 6000)--gop-size <frames>- GOP size in frames (0 = auto, default: 0)
--no-audio- Disable audio processing--zerolatency- Enable ultra-low latency mode (default: on)--no-sei- Disable SEI timecode injection--timeout <seconds>- Auto-exit after specified seconds (0 = disabled)--dump-ts <path>- Save MPEG-TS to file for debugging--timestamp-mode <mode>- NDI timestamp mode: auto, timecode, timestamp, etc.--verbose- Enable debug stderr messages--discover- Discover and list available NDI sources--help,-h- Show usage information
Run ./ndi2srt --help for the complete, up-to-date help message.
The application is built on GStreamer and constructs a real-time transcoding pipeline that processes NDI streams frame-by-frame. The core pipeline consists of:
- NDI Source (
ndisrc): Captures the NDI stream with configurable timestamp modes - NDI Demuxer (
ndisrcdemux): Separates video and audio streams - Video Processing: Converts to I420 format and applies H.264 encoding
- Metadata Injection: Injects SMPTE timecode via H.264 SEI (Supplemental Enhancement Information)
- Output: Streams to SRT endpoint or outputs MPEG-TS to stdout
In the default operation mode the timecode originates from the NDI source as UTC-based LTC (Linear Time Code) metadata embedded in the GstVideoTimeCodeMeta. This metadata contains:
- Hours, Minutes, Seconds, Frames: Standard SMPTE timecode components
- Drop Frame Flag: Indicates whether the source uses drop-frame timecode
- Frame Rate: Derived from the NDI stream's timing information
The timecode flows through the pipeline as follows:
- Source Extraction: NDI source provides
GstVideoTimeCodeMetawith each video frame - PTS Fallback: If timecode metadata is unavailable, the system falls back to deriving timecode from presentation timestamps (PTS)
- SEI Construction: Timecode values are packed into H.264 Picture Timing SEI payloads
- Stream Injection: SEI NAL units are inserted into the H.264 bitstream before each video frame
The application implements a sophisticated frame metadata injection system that ensures compliance with H.264 standards and FFmpeg compatibility:
- Picture Timing SEI (Payload Type 1): Standard H.264 SEI containing timecode information
- SPS VUI Patching: Modifies Sequence Parameter Set to signal
pic_struct_present_flag=1andtiming_info_present_flag=1 - HRD Compliance: Ensures compliance with ISO/IEC 14496-10-2005 D.1.2 requirements
The SEI injection occurs at the encoder output stage using a pad probe that:
- Analyzes Access Units: Scans H.264 NAL units to identify frame boundaries
- SPS Management: Caches and injects patched SPS with proper VUI flags
- SEI Placement: Inserts Picture Timing SEI after AUD (Access Unit Delimiter) or at frame start
- Buffer Reconstruction: Rebuilds complete H.264 Access Units with injected metadata
The injected timecode follows SMPTE-12M standard:
- Frame Count: 0-23 (for 24fps) or 0-29 (for 30fps)
- Seconds: 0-59
- Minutes: 0-59
- Hours: 0-23
- Drop Frame: Boolean flag for drop-frame timecode standards
- Codec: H.264 (AVC) using x264enc
- Profile: Baseline/High Profile with VUI parameters
- Bitrate: Configurable (default: 6000 kbps)
- Latency: Ultra-low latency mode with
tune=zerolatency - Keyframe Interval: Configurable GOP size (
--gop-size <frames>, 0 = auto) - VUI Insertion: Disabled (
insert-vui=false) to allow manual SEI control
- Container: MPEG-TS (Transport Stream)
- H.264 Format: Annex B byte-stream with start codes
- Alignment: Access Unit (AU) aligned for proper parsing
- SEI Structure: Picture Timing SEI per frame with timecode data
- Codec Support: AAC (default), MP3, AC3, SMPTE 302M
- Bitrate Control: Configurable via
--audio-bitrate <kbps>(0 = auto) - Sample Rate: Fixed at 48 kHz from NDI source
- Channels: Stereo (2 channels)
- Format: Varies by codec (S16LE for most, experimental for SMPTE 302M)
- Disable Option: Use
--no-audioto exclude audio from output - Sync: Maintained with video timing
- Protocol: SRT (Secure Reliable Transport)
- Connection: Caller or Listener mode
- Latency: Optimized for sub-100ms end-to-end latency
- Reliability: Built-in error correction and retransmission
- Format: Raw MPEG-TS stream to standard output
- Use Case: Piping to FFmpeg for further processing
- Example:
./ndi2srt --stdout | ffmpeg -i - -c copy output.mp4
- Pipeline Construction: Uses
gst_parse_launchfor dynamic pipeline building - Pad Probes: Custom probe functions for metadata injection
- Buffer Management: Direct buffer manipulation for SEI insertion
- State Management: Proper pipeline state transitions and cleanup
- Buffer Pooling: Efficient buffer allocation and recycling
- SEI Caching: Cached SPS and SEI structures to minimize recomputation
- Zero-Copy: Minimizes memory copies where possible
- Stream Recovery: Automatic recovery from NDI connection issues
- SEI Validation: Ensures injected metadata meets H.264 standards
- Pipeline Monitoring: Bus message handling for error detection
The injected timecode is fully compatible with FFmpeg tools:
- ffprobe: Extracts timecode as
SMPTE 12-1 timecodeside data - ffmpeg: Recognizes timecode metadata for filtering and processing
- Side Data: Accessible via
%{metadata:timecode}in drawtext filters
- SEI Standards: Follows ITU-T H.264 Annex D specifications
- VUI Parameters: Compliant with Video Usability Information standards
- Bitstream Format: Valid Annex B byte-stream with proper start codes
- Latency: Sub-100ms end-to-end for typical configurations
- CPU Usage: Optimized for real-time processing
- Memory: Efficient buffer management with minimal overhead
- Scalability: Supports multiple concurrent NDI sources
# Discover available NDI sources first
./ndi2srt --discover
# Live production to SRT distribution
./ndi2srt --ndi-name "PC.LOCAL (Studio Camera 1)" --srt-uri "srt://cdn.example.com:9000?mode=caller" \
--encoder x264enc --bitrate 8000 --verbose
# Multiple NDI sources to different SRT endpoints
./ndi2srt --ndi-name "PC.LOCAL (Camera 1)" --srt-uri "srt://endpoint1:9000?mode=caller" &
./ndi2srt --ndi-name "PC.LOCAL (Camera 2)" --srt-uri "srt://endpoint2:9000?mode=caller" &# NDI to YouTube Live via FFmpeg
./ndi2srt --ndi-name "PC.LOCAL (OBS Virtual Camera)" --stdout | \
ffmpeg -i - -c copy -f flv rtmp://a.rtmp.youtube.com/live2/STREAM_KEY
# Local recording with timecode
./ndi2srt --ndi-name "PC.LOCAL (Screen Capture)" --stdout --timeout 3600 | \
ffmpeg -i - -c copy "recording_$(date +%Y%m%d_%H%M%S).mp4"# Default AAC with automatic bitrate
./ndi2srt --ndi-name "PC. LOCAL (Camera 1)" --stdout --audio-codec aac
# AAC with custom bitrate
./ndi2srt --ndi-name "PC.LOCAL (Camera 1)" --stdout --audio-codec aac --audio-bitrate 192# MP3 with 128 kbps bitrate
./ndi2srt --ndi-name "PC.LOCAL (Camera 1)" --stdout --audio-codec mp3 --audio-bitrate 128# AC3 for broadcast applications
./ndi2srt --ndi-name "PC.LOCAL (Camera 1)" --stdout --audio-codec ac3 --audio-bitrate 256# SMPTE 302M uncompressed audio (bitrate ignored)
# Note: Requires experimental codec support
./ndi2srt --ndi-name "PC.LOCAL (Camera 1)" --stdout --audio-codec smpte302m# Video-only output
./ndi2srt --ndi-name "PC.LOCAL (Camera 1)" --stdout --no-audio# Capture debug output and verify timecode injection
./ndi2srt --ndi-name "PC.LOCAL (Test Source)" --stdout --verbose --timeout 10 > test.ts 2>debug.log
# Verify timecode extraction
ffprobe -show_entries frame=side_data -of json test.ts | grep -A 5 "SMPTE 12-1 timecode"- NDI Source Not Found: Ensure NDI source is running and discoverable. Check NDI source name for spelling mistakes.
- SEI Injection Failing: Check verbose output for SPS VUI parsing errors
- Timecode Not Extracting: Verify ffprobe shows "SMPTE 12-1 timecode" side data
- High Latency: Check network conditions and encoder settings
Use --verbose flag to enable detailed logging:
- SPS VUI parameter analysis
- SEI injection confirmation
- Timecode value logging
- Buffer processing details
- Bitrate: Adjust based on network capacity and quality requirements
- Encoder: Use hardware encoders (vtenc_h264) when available
- Latency: Balance between
--zerolatencyand compression efficiency
This architecture ensures that professional broadcast workflows can maintain accurate timecode synchronization when transitioning from NDI production environments to SRT distribution networks, while providing the flexibility to output to various downstream systems.