Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:

jobs:
build-and-test:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: GCC release
cxx: g++
build_type: Release
sanitize: OFF
- name: Clang release
cxx: clang++
build_type: Release
sanitize: OFF
- name: GCC ASan+UBSan
cxx: g++
build_type: Debug
sanitize: ON

steps:
- uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
cmake ninja-build pkg-config g++ clang libgtest-dev ffmpeg \
libavformat-dev libavcodec-dev libavfilter-dev libavdevice-dev \
libavutil-dev libswscale-dev libswresample-dev

- name: Configure
run: >
cmake -S . -B build -G Ninja
-DCMAKE_CXX_COMPILER=${{ matrix.cxx }}
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DNDISTREAMER_SANITIZE=${{ matrix.sanitize }}
-DNDISTREAMER_WERROR=ON

- name: Build
run: cmake --build build

- name: Test
run: ctest --test-dir build --output-on-failure --timeout 120
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Changelog

## 2.0.0

A ground-up rewrite. The goal is unchanged (stream media as an NDI source from Linux), but the
architecture, timing model and feature set are all new.

### Highlights

- **One pipeline for every decoder.** The three duplicated applications (software, VAAPI, CUDA) are
replaced by a single threaded pipeline: demux → decode → process → present. Bounded queues connect
the stages and a shared presentation clock drives the output.
- **Accurate A/V timing.** Frames are sent at their presentation timestamps, and audio is the master
clock while it plays. NDI timecodes are derived from media timestamps, so receivers that
synchronize on timecode get exact alignment. Variable frame rates, late-starting streams, long
pauses and inputs without timestamps are handled.
- **Graceful overload.** When the machine can't keep up, video sheds work in stages while audio
keeps playing. In order: drop late frames, skip processing of late frames, then decode keyframes
only. Live sources tolerate clock drift and mid-GOP joins.
- **Generic hardware decoding.** `-t auto|vaapi|cuda|vdpau|...` accepts any FFmpeg device type.
When the device can't handle a stream, that stream falls back to software. GPU frames are
downloaded directly into NDI's buffer layout.
- **Lower CPU use.** Formats NDI accepts natively are passed through instead of always being
converted to UYVY as 1.x did. Video is submitted with NDI's asynchronous API, and software
decoding now uses multiple threads. For 1080p60 H.264, passthrough cut sender CPU from 66 % of a
core (forcing UYVY) to 36 % with software decoding and 22 % with VAAPI.
- **No NDI SDK needed to build.** The NDI runtime is loaded at start-up from the usual locations,
`NDI_RUNTIME_DIR_V6`, or `--ndi-lib`. The previous `/opt/ndi` SDK location is still searched.

### New features

- Seamless looping (`--loop`) with a continuous timeline. MP3 encoder delay and padding are
accounted for.
- Any FFmpeg input: network streams (RTSP, SRT, HLS, ...), capture devices (`-f v4l2`, `x11grab`,
`pulse`, ...) and protocol options (`-o key=value`).
- FFmpeg video filters (`--vf`), including GPU filters on hardware frames.
- Output pixel format choice (`-p auto|uyvy|nv12|i420|bgra|rgba`).
- Multichannel audio is preserved and sent as 32-bit float. Optional `--audio-rate` and
`--audio-channels`.
- Stream selection (`--video-stream`, `--audio-stream`, `--no-video`, `--no-audio`), NDI groups
(`-g`), and audio-only or video-only inputs.
- Interlaced content is flagged correctly for NDI.
- Clean shutdown on Ctrl+C or SIGTERM, levelled logging (`-v`/`-q`), `--stats`, `--version`, and
meaningful exit codes.

### Fixes compared to 1.x

- Files without an audio stream no longer crash (the stream-count check was wrong and the code
dereferenced a missing audio stream).
- The NDI frame rate is no longer taken from `codecpar->framerate`, which is often 0/0. It now comes
from the container's real frame rate.
- Decoders are drained at the end of the input, so the last frames are no longer lost.
- Audio is no longer forced to 16-bit stereo, so no precision or channels are lost.
- Corrupt packets are skipped instead of stopping the stream.
- The CPU no longer busy-waits on queues, and a per-frame NV12 allocation and copy is gone.
- Builds with current FFmpeg: `avcodec_close` was removed upstream.

### Compatibility notes

- The command line is backward compatible: `-i`, `-s` and `-t software|vaapi|cuda` still work.
- Debug builds no longer produce a separately named `ndistreamer_debug` binary.
- Requires FFmpeg 6.0 or newer and a C++20 compiler (GCC 13+, Clang 17+).

### Development

- 85 automated tests, including whole-pipeline tests against a fake `libndi.so`. These check
pacing, A/V alignment, loop continuity and frame lifetime. Tests also verify the NDI ABI against
the official SDK headers and stream end to end to a real NDI receiver when the SDK is available.
- CI on Ubuntu 24.04 (FFmpeg 6.1) with GCC, Clang, and ASan/UBSan.
146 changes: 95 additions & 51 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,58 +1,102 @@
cmake_minimum_required(VERSION 3.20)

cmake_minimum_required(VERSION 3.14)
project(NDIStreamer)
project(ndistreamer
VERSION 2.0.0
DESCRIPTION "Stream any media file, network stream or capture device as an NDI source"
HOMEPAGE_URL "https://github.com/lowlevelmetal/ndi-streamer"
LANGUAGES CXX)

# C++ Standard
set(CMAKE_CXX_STANDARD 20) # Or 20 for C++20
set(CMAKE_CXX_STANDARD_REQUIRED True) # Ensure the compiler respects the version
set(CMAKE_CXX_EXTENSIONS OFF) # Disable compiler-specific extensions like gnu++17
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
set(CMAKE_EXPORT_COMPILE_COMMANDS "ON")

message(STATUS "CMAKE_MODULE_PATH: ${CMAKE_MODULE_PATH}")

# Set debug flags
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -D_DEBUG")

# Find NDI SDK
find_package(NDISDK REQUIRED)

# Find FFMPEG
find_package(FFMPEG REQUIRED)

# Include NDI SDK headers
include_directories(${NDI_INCLUDE_DIR} ${FFMPEG_INCLUDE_DIRS})

# Define sources
set(SOURCES
src/averror.cpp
src/demuxer.cpp
src/decoder.cpp
src/audioresampler.cpp
src/ndi.cpp
src/frametimer.cpp
src/frame.cpp
src/asyncndisource.cpp
src/softwareapp.cpp
src/vaapiapp.cpp
src/cudaapp.cpp
src/simplefilter.cpp
src/ndistreamer.cpp
src/cudadecoder.cpp
src/vaapidecoder.cpp)

# Set executable name
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_executable(ndistreamer_debug ${SOURCES})
target_link_libraries(ndistreamer_debug ${NDI_LIB} ${FFMPEG_LIBRARIES})
else()
add_executable(ndistreamer ${SOURCES})
target_link_libraries(ndistreamer ${NDI_LIB} ${FFMPEG_LIBRARIES})

option(NDISTREAMER_BUILD_TESTS "Build the test suite (requires GoogleTest)" ${PROJECT_IS_TOP_LEVEL})
option(NDISTREAMER_SANITIZE "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
option(NDISTREAMER_WERROR "Treat compiler warnings as errors" OFF)
set(NDI_SDK_DIR "" CACHE PATH
"Optional NDI SDK root. Not needed to build; enables ABI verification and end-to-end tests")

# Dependencies ---------------------------------------------------------------

find_package(PkgConfig REQUIRED)
# FFmpeg 6.0 or newer.
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
libavformat>=60
libavcodec>=60
libavfilter>=9
libavdevice>=60
libavutil>=58
libswscale>=7
libswresample>=4)
find_package(Threads REQUIRED)

# Newer FFmpeg lets filter sources carry color properties.
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_INCLUDES ${FFMPEG_INCLUDE_DIRS})
check_cxx_source_compiles([[
#include <cstdint>
extern "C" {
#include <libavfilter/buffersrc.h>
}
int main() { AVBufferSrcParameters p{}; p.color_space = AVCOL_SPC_BT709; p.color_range = AVCOL_RANGE_MPEG; return 0; }
]] NDISTREAMER_HAVE_BUFFERSRC_COLOR)
unset(CMAKE_REQUIRED_INCLUDES)

# Compiler settings ----------------------------------------------------------

add_library(ndistreamer_warnings INTERFACE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(ndistreamer_warnings INTERFACE -Wall -Wextra -Wpedantic -Wshadow -Wnon-virtual-dtor
$<$<BOOL:${NDISTREAMER_WERROR}>:-Werror>)
endif()

# Enable testing
enable_testing()
if(NDISTREAMER_SANITIZE)
add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined)
add_link_options(-fsanitize=address,undefined)
endif()

# Targets --------------------------------------------------------------------

configure_file(src/version.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/generated/version.hpp @ONLY)

add_library(ndistreamer_core STATIC
src/av/audio_processor.cpp
src/av/decoder.cpp
src/av/ffmpeg.cpp
src/av/input.cpp
src/av/video_processor.cpp
src/frames.cpp
src/ndi/runtime.cpp
src/ndi/sender.cpp
src/options.cpp
src/streamer.cpp
src/util/clock.cpp
src/util/log.cpp)
target_include_directories(ndistreamer_core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}/generated)
target_link_libraries(ndistreamer_core
PUBLIC PkgConfig::FFMPEG Threads::Threads ${CMAKE_DL_LIBS}
PRIVATE ndistreamer_warnings)
target_compile_definitions(ndistreamer_core PRIVATE
$<$<BOOL:${NDISTREAMER_HAVE_BUFFERSRC_COLOR}>:NDISTREAMER_HAVE_BUFFERSRC_COLOR>)

# Add tests directory
add_subdirectory(tests)
add_executable(ndistreamer src/main.cpp)
target_link_libraries(ndistreamer PRIVATE ndistreamer_core ndistreamer_warnings)

include(GNUInstallDirs)
install(TARGETS ndistreamer RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

# Tests ----------------------------------------------------------------------

if(NDISTREAMER_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
Loading
Loading