Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ Single OBS MODULE shared library. All source is C11.
audio: resample, write to jitter buffer
video: keyframe gate, push decoded frame (PTS in ns) onto video queue

[video thread]: pop video queue, HW frame transfer, format conversion,
OBS async video output
[video thread]: pop video queue, HW frame transfer, hold until due,
format conversion, OBS async video output

[audio thread]: drain jitter buffer, speed correction, concealment,
OBS audio output
Expand All @@ -85,7 +85,7 @@ Buffer regulation happens through playback speed only, asymmetric like IRLToolki
- **`src/receiver-stream.c`**: stream open/close, demuxer options, reconnection, disconnect fade out, periodic stats logging.
- **`src/receiver-decode.c`**: packet to decoder plumbing with corruption burst handling and throttled decoder flushes.
- **`src/receiver-audio.c`**: the audio core. Intake side (receiver thread): PTS repair, resample to interleaved float, write to the PTS aware jitter buffer. Pre-keyframe audio is discarded (not staged) to avoid decoder warm-up artifacts. Output side (audio thread): sample counter output clock, constant rate submission, swr based speed correction, dropout concealment, hidden backlog trims.
- **`src/receiver-video.c`**: decoded video frame handling, keyframe gate, resolution change detection. Also owns `irl_video_request_clear`: the receiver thread drops the queue and raises a flag, and the *video* thread is what actually calls `obs_source_output_video(source, NULL)`. Clearing from the receiver thread instead would race a frame already inside the format conversion, which would repaint the frozen frame right after the clear.
- **`src/receiver-video.c`**: decoded video frame handling, keyframe gate, resolution change detection, and the video output pacing loop. Frames are copied out of the hardware pool as soon as they arrive (which returns the decoder's surface) and then held in a video-thread-private pacing queue until their mapped timestamp is due, the way OBS's own media source paces in `mp_media_sleep`. This is what keeps libobs's async queue about one frame deep: handing it a frame early makes it hold that frame, and past `MAX_ASYNC_FRAMES` (30) held frames `cache_video` silently discards the entire queue. Also owns `irl_video_request_clear`: the receiver thread drops the queue and raises a flag, and the *video* thread is what actually calls `obs_source_output_video(source, NULL)`. Clearing from the receiver thread instead would race a frame already inside the format conversion, which would repaint the frozen frame right after the clear.
- **`src/audio-buffer.c`**: thread safe ring buffer sized in milliseconds with a parallel PTS chunk queue. Mutex protected. Supports fade-out reads.
- **`src/video-handler.c`**: converts AVFrames to OBS video. Maps pixel formats (I420, NV12, I010, P010, etc.), handles HW frame transfer, falls back to swscale for unsupported formats. Maps video PTS through the audio playout offset for lip sync.
- **`src/pts-repair.c`**: three tier PTS discontinuity repair. Small gaps interpolated, medium gaps get silence, large gaps trigger full reset.
Expand All @@ -103,7 +103,7 @@ Buffer regulation happens through playback speed only, asymmetric like IRLToolki

- **Main/OBS thread**: calls create, destroy, update, tick, get_properties, and the activate/deactivate/show/hide callbacks (used only when "Close Stream When Inactive" is on)
- **Receiver thread**: owns demux/decode FFmpeg state. Writes to the audio buffer (mutex protected) and pushes decoded video frames (PTS pre-converted to nanoseconds) onto the video queue. Never blocks on GPU or OBS video delivery.
- **Video thread**: pops the video queue, does the HW frame transfer and format conversion (owns sws_ctx), and calls `obs_source_output_video`. Queue overflow drops the oldest frame (`video_queue_drops`).
- **Video thread**: pops the video queue, does the HW frame transfer, paces each frame to its due time, then converts (owns sws_ctx) and calls `obs_source_output_video`. Queue overflow drops the oldest frame (`video_queue_drops`). The pacing queue it holds those frames in needs no lock — the receiver thread never touches it, and a clear is routed through `video_clear_pending` — but its counters are mirrored under `video_queue_lock` for the stats line.
- **Audio thread**: drains the jitter buffer and submits audio to OBS via `obs_source_output_audio`, paced against the sample counter output clock. Shared timing state is protected by `audio_state_lock` (lock order: `audio_state_lock` before the buffer mutex).

Config fields marked `/* hot */` in `struct irl_config` are written by `irl_source_update` while the worker threads run, so every cross-thread read goes through `os_atomic_load_long` / `os_atomic_load_bool` (not C11 `_Atomic`, which MSVC does not support without an experimental flag). The remaining fields are only written while the threads are stopped, where `irl_thread_create` and `irl_thread_join` supply the happens-before edge.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ Stats are exposed through OBS's `proc_handler` API under the `get_stats` call, a
| `obs_lead_ms` | int | How far ahead of real time audio is queued inside OBS (healthy is roughly 60 to 100ms) |
| `audio_decoder_flushes` | int | Number of audio decoder flushes after repeated decode errors |
| `video_decoder_flushes` | int | Number of video decoder flushes after repeated decode errors |
| `video_lead_ms` | int | How far ahead of real time the last video frame was timestamped. Tracks the audio buffer; a value climbing well past Target Buffer and staying there means concealment has inflated the A/V mapping |
| `video_lead_excess` | int | Frames whose lead exceeded what OBS's async queue can absorb. Harmless while the lead is steady; sustained growth is what makes OBS drop queued video |
| `stream_delay_ms` | int | End-to-end stream delay (SRT latency + decode + buffering) |
| `low_latency_audio` | bool | Whether OBS async unbuffered low-latency mode is enabled |
| `reconnect_count` | int | Number of reconnect attempts since the source was created |
Expand Down
62 changes: 58 additions & 4 deletions deps/build-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ fetch() {
fi

echo "download: ${url}"
curl -fsSL --retry 3 --retry-delay 2 -o "${path}.tmp" "${url}"
# --retry alone only covers timeouts and 5xx; a refused or reset
# connection is not "transient" to curl and fails on the first try.
# ffmpeg.org does both often enough to have cost a CI run.
curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors \
--retry-connrefused --connect-timeout 30 \
-o "${path}.tmp" "${url}"

local got
got="$(sha256_of "${path}.tmp")"
Expand Down Expand Up @@ -272,15 +277,49 @@ build_zlib() {
"zlib-${ZLIB_VERSION}.tar.gz" "${ZLIB_SHA256}"
extract "zlib-${ZLIB_VERSION}.tar.gz" "zlib-${ZLIB_VERSION}"

# ZLIB_BUILD_TESTING is what 1.3.2 renamed ZLIB_BUILD_EXAMPLES to; both
# are passed so either version builds only the library.
#
# ZLIB_BUILD_SHARED/STATIC are zlib's own switches and both default ON;
# it does not honour BUILD_SHARED_LIBS from cmake_common. Left alone it
# installs z.dll plus an *import* z.lib alongside the static library,
# and since that import lib already occupies the name FFmpeg links
# against, ensure_msvc_lib_name below would accept it and quietly give
# the plugin a runtime DLL dependency the bundled stack exists to avoid.
cmake -S "$(npath "${src}/zlib-${ZLIB_VERSION}")" \
-B "$(npath "${src}/zlib-${ZLIB_VERSION}/build")" \
"${cmake_common[@]}" \
-DZLIB_BUILD_SHARED=OFF \
-DZLIB_BUILD_STATIC=ON \
-DZLIB_BUILD_TESTING=OFF \
-DZLIB_BUILD_EXAMPLES=OFF
cmake --build "$(npath "${src}/zlib-${ZLIB_VERSION}/build")" --parallel "${jobs}"
cmake --install "$(npath "${src}/zlib-${ZLIB_VERSION}/build")"

# zlib's CMake calls its static output zlibstatic; FFmpeg asks for z.lib.
ensure_msvc_lib_name z zlibstatic zlib
# Belt and braces: if a future zlib renames those switches the way 1.3.2
# renamed its static target, fail here rather than link a DLL.
if [[ -f ${prefix}/bin/z.dll || -f ${prefix}/bin/zlib1.dll ]]; then
echo "zlib installed a DLL; the bundled stack must be static" >&2
exit 1
fi

# FFmpeg's MSVC flag translator hardcodes -lz to zlib.lib rather than
# z.lib like every other -l name:
#
# -lz) echo zlib.lib ;;
# -l*) echo ${flag#-l}.lib ;;
#
# Under zlib 1.3.1 that name existed by accident, as the *shared*
# import library (1.3.1 named the DLL target zlib; 1.3.2 renamed it to
# z). So the Windows build has been linking zlib dynamically all along,
# and turning the DLL off is what finally made the missing name visible
# as LNK1181: cannot open input file 'zlib.lib'.
#
# Provide both spellings from the static archive: zlib.lib is what
# FFmpeg links, z.lib is what the generic -l handling in the CMake
# description below resolves.
ensure_msvc_lib_name z zs zlibstatic zlib
ensure_msvc_lib_name zlib zs zlibstatic z

# zconf.h.cmakein still carries an autoconf-era block:
#
Expand Down Expand Up @@ -604,9 +643,24 @@ build_ffmpeg() {
# the only thing that distinguishes a missing library from one whose
# name or link order the toolchain got wrong.
if ! (cd "${ff}" && ./configure "${args[@]}"); then
local cfglog="${ff}/ffbuild/config.log"
echo
# A tail alone is not enough. Autodetected libraries are probed
# early and configure only dies about them in a sweep at the very
# end ("$lib requested but not found"), so by the time it fails
# the probe that actually explains it is a thousand lines above
# the tail and nothing in the visible output names a cause.
echo "---- probes for the libraries we require ----" >&2
local l
for l in zlib mbedtls libsrt librist ffnvcodec; do
echo "== ${l} ==" >&2
grep -n -B2 -A25 \
-e "check_pkg_config ${l} " \
-e "check_lib ${l} " \
"${cfglog}" >&2 || echo "(no probe logged)" >&2
done
echo "---- tail of ffbuild/config.log ----" >&2
tail -60 "${ff}/ffbuild/config.log" >&2 || true
tail -60 "${cfglog}" >&2 || true
exit 1
fi

Expand Down
16 changes: 8 additions & 8 deletions deps/versions.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# Bumping a version here also requires updating the matching SHA256.

# Windows only. Linux and macOS use the system zlib; Windows has none.
ZLIB_VERSION=1.3.1
ZLIB_SHA256=9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
ZLIB_VERSION=1.3.2
ZLIB_SHA256=bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16

FFMPEG_VERSION=9.0
FFMPEG_SHA256=7f607a00dd0d28a729d5a4811205812eef01cf6ef6155025febb6f36a9062d52
Expand All @@ -17,14 +17,14 @@ FFMPEG_SHA256=7f607a00dd0d28a729d5a4811205812eef01cf6ef6155025febb6f36a9062d52
SRT_VERSION=1.5.6
SRT_SHA256=2c4980c2c4cfd142d21b829d939dc51db9c6628af5967fff62fd7290769569c7

MBEDTLS_VERSION=3.6.4
MBEDTLS_SHA256=ec35b18a6c593cf98c3e30db8b98ff93e8940a8c4e690e66b41dfc011d678110
MBEDTLS_VERSION=3.6.7
MBEDTLS_SHA256=a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6

# RIST ingest. obs-deps pins 0.2.7; this is the same story as libsrt.
LIBRIST_VERSION=0.2.18
LIBRIST_SHA256=9a2d16dcdb9fb067b7ba4259a3976ff6f8df9a62dbec7f32f19a0b60ec0c114a
LIBRIST_VERSION=0.2.20
LIBRIST_SHA256=9e40eeb87f014790531ad41326cc271b930a65962e4b15231b301fc59b29fe31

# Headers only. FFmpeg loads nvcuda/nvcuvid at runtime, so this adds no
# build-time or load-time dependency on a CUDA install.
NVCODEC_VERSION=13.0.19.0
NVCODEC_SHA256=86d15d1a7c0ac73a0eafdfc57bebfeba7da8264595bf531cf4d8db1c22940116
NVCODEC_VERSION=13.1.15.0
NVCODEC_SHA256=2255bc74d038b95aa4be30f5f66322c2176acbdb90ada1851db6993536fbeaf7
Loading
Loading