Skip to content

Fix audio and receiver issues, improve performance and safety - #2

Merged
datagutt merged 15 commits into
masterfrom
review/fixes-2026-05
May 9, 2026
Merged

Fix audio and receiver issues, improve performance and safety#2
datagutt merged 15 commits into
masterfrom
review/fixes-2026-05

Conversation

@datagutt

@datagutt datagutt commented May 9, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved audio-video synchronization with refined timestamp calculations
    • Enhanced thread state management for more stable and reliable streaming
    • Improved audio buffer trimming and fill management for consistent playback
  • Performance

    • Optimized memory efficiency through shared scratch buffer reuse in audio and video processing

Review Change Stack

datagutt added 15 commits May 9, 2026 23:46
pts * 1000000000LL * tb_num / tb_den overflows int64 well within
plausible session lengths. with a 90 kHz video timebase the
pts * 1e9 multiplication overflows around the 28h mark, and FFmpeg
genpts/PTS remappings can land us there sooner. av_rescale_q does
the same conversion safely with proper rounding.
- av_packet_alloc / av_frame_alloc in irl_receiver_thread: bail out
  cleanly instead of crashing on the first deref.
- av_strdup in apply_demuxer_options: was passed straight into strtok_r
  on NULL.
- swr_alloc_set_opts2 takes a SwrContext** that it allocates if NULL,
  so the prior swr_alloc() call was a no-op leak path. Drop it and
  check the v2 API's return code plus the resulting pointer.
audio_buffer_init published buf->data before pthread_mutex_init.
audio_buffer_fill_ms_locked (called from main thread without
audio_state_lock by the stats proc_handler) gates locking on
buf->data != NULL, so it could lock an uninitialised mutex. Init
the mutex first.

audio_buffer_free now keys off sample_rate, so a calloc failure
(capacity=0, data=NULL) doesn't leak the mutex.
if the audio thread created OK but the receiver thread create failed,
thread_active stayed true with an uninitialised receiver_thread
handle, and irl_receiver_stop would deadlock joining it. log, unwind,
and join the audio thread on failure.
…cting

these flags are read from FFmpeg's interrupt callback (potentially on
a foreign network thread) and written from main and receiver threads.
volatile bool gives no atomicity or memory ordering guarantees on its
own. switch to OBS's os_atomic_load_bool / os_atomic_store_bool
(seq_cst on both POSIX and Windows backends).

declarations stay volatile bool, which matches the os_atomic_* API.
receiver-thread writes to fields read by the audio thread under
audio_state_lock now go through the same lock, eliminating the
torn-read / lost-update windows the previous code relied on x86
atomicity for.

covers:
- latest_audio_stream_pts_ns and decoded_frame_samples after each
  decoded audio frame (receiver-audio.c).
- latest_video_stream_pts_ns when video frame lands (receiver-video.c).
- fade_in_pending / fade_in_frames_remaining /
  startup_audio_warmup_remaining_ms in irl_prepare_new_connection
  and irl_handle_stream_read_error (receiver-stream.c).
- audio_recovery_until_us via irl_mark_audio_recovery, which is now
  always called inside the lock from receiver-side paths
  (receiver-audio.c, receiver-decode.c, receiver-stream.c).

frame_timestamp (called from receiver thread) snapshots the audio
playout fields under the lock so the video PTS mapping sees a
consistent state instead of a torn pair.
irl_source_get_stats runs on the main OBS thread and previously read
audio_buffer state plus a pile of receiver-thread-owned counters
without synchronisation. the worst case was racing the receiver
thread's audio_buffer_reconfigure (free + realloc of buf->data)
while the stats reader was inside audio_buffer_fill_ms_locked.

snapshot everything under the lock, then format calldata after
release. the snapshot also gives the consumer a single consistent
view of the counters instead of a torn mix.
each pump (~50 Hz) was malloc'ing the OBS output buffer and each
decoded audio frame was malloc'ing the resample target. on lossy
IRL streams those bursts coincide with the worst allocator
pressure; the per-frame allocations were a real source of
microsecond-scale jitter.

add two ctx-owned scratch buffers, grown on demand:
- audio_pump_scratch (audio thread) covers normal pump output and
  the underrun silence path.
- audio_resample_scratch (receiver thread) covers swresample target
  and PTS-repair silence insertion. both are receiver-thread-owned
  and the silence is consumed (memcpy'd into the jitter buffer)
  before the next swresample, so the buffer is safe to reuse.

both are freed in irl_source_destroy. no lock needed: each scratch
is owned by exactly one thread.
the swscale fallback path was malloc'ing a frame-sized buffer per
video frame (30-60 Hz, hundreds of KB at 1080p). cache it on the
context, grown on demand, and free on destroy. only matters for
streams using pixel formats OBS does not accept directly, but it
removes a real allocator-pressure source on those streams.
audio_buffer_trim_to_keep_ms drops chunks under one lock until fill
falls below the target. the maybe_trim_hidden_audio_backlog path
used to relock for every dropped chunk plus an extra fill_ms read,
so trimming N chunks was 2N+1 lock acquires. now it's one.

maybe_log_audio_timing_diag now only takes the buffer lock to read
fill_ms after the throttle gate passes, instead of every pump.
audio_buffer_write previously dropped data into the ring without
recording a chunk. downstream audio_buffer_read_pts then returned
pts=0 for those bytes while pts_consume kept advancing the chunk
queue, gradually desyncing the pts metadata against the pcm.

derive the continuation pts from the previous chunk's end (last_pts
plus samples_in_chunk / sample_rate) and record a chunk so the
queue stays consistent. callers that don't track pts (legacy
helpers, future silence/pad paths) get sensible pts continuation
without having to compute it themselves.

current tree has no caller of this variant; the irl audio path
uses audio_buffer_write_pts. this commit defangs the API for any
future caller and removes the silent footgun that was waiting in
the queue invariants.
ts * 1000 * tb_num overflows int64 around ts ~= 9.2e15 / tb_num,
which is reachable on long-running streams with 90 kHz video
timebase or AAC at 48 kHz running for many days. av_rescale_q does
the same conversion safely with proper rounding.

ms_to_ts_ceil follows av_rescale_q_rnd with AV_ROUND_UP so the
relock step still rounds up to at least one tick.
the previous behaviour treated every backward jump as a reorder and
overwrote last_pts with the smaller value. on the next forward
frame, "expected" was lower than reality, the gap looked huge, and
we'd insert silence or trigger a reset against an artefact of our
own bookkeeping.

split the backward case:
- gap < small_gap_ms: real reorder / decoder ts wobble. pass the
  pts through but leave last_pts alone so the baseline keeps
  tracking the leading edge of the stream.
- gap >= small_gap_ms: timeline reset (sender pts wrap, segment
  remap, decoder reset). reanchor and return PTS_ACTION_RESET so
  the audio path flushes the buffer and reinits timing, same as
  for a large forward gap.

forward < 1 ms case is unchanged.
avframe linesizes can be negative when the frame is laid out
bottom-up. taking abs() before handing the buffer to OBS would
present the data as if it were top-down, silently flipping the
image vertically. detect any negative linesize and force the
swscale fallback path which produces a clean top-down nv12.

real ffmpeg decoders almost never emit this (it's mostly a
sws_scale flip artifact), but it's cheap to be safe.
bzalloc/bfree match the rest of the plugin's allocations and play
nicely with whatever debug allocator OBS may swap in. functionally
equivalent to calloc/free on current builds.
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 20b145c3-ed23-4774-b187-3241d822a568

📥 Commits

Reviewing files that changed from the base of the PR and between 7348f03 and 5bcdeff.

📒 Files selected for processing (11)
  • include/audio-buffer.h
  • include/irl-source.h
  • src/audio-buffer.c
  • src/irl-source.c
  • src/pts-repair.c
  • src/receiver-audio.c
  • src/receiver-decode.c
  • src/receiver-stream.c
  • src/receiver-video.c
  • src/receiver.c
  • src/video-handler.c

Walkthrough

This PR refactors the receiver pipeline to improve thread safety, memory efficiency, and timestamp handling. It introduces atomic operations for thread state management, replaces heap allocations with growable per-thread scratch buffers, upgrades PTS repair logic to use FFmpeg rescaling, and tightens lock coverage around shared audio/video state to prevent races.

Changes

Audio Receiver Pipeline Refactor

Layer / File(s) Summary
Data Contracts and Public API
include/audio-buffer.h, include/irl-source.h
New audio_buffer_trim_to_keep_ms() function for trimming buffers under a single lock. Source struct adds sws_nv12_buf, audio_pump_scratch, and audio_resample_scratch fields with capacities.
Audio Buffer Allocation and Trimming
src/audio-buffer.c
Ring buffer allocation switches to bzalloc/bfree; mutex initialized before buf->data visibility; trim-to-keep-ms function drops oldest PTS chunks until fill target and minimum chunk count are met.
Timestamp Conversion and PTS Repair
src/pts-repair.c
Timestamp helpers use av_rescale_q/av_rescale_q_rnd with clamping/rounding; backward-jump handling splits small (pass-through) from large (reset); tiny forward gaps handled separately.
Thread State Atomicity
src/irl-source.c, src/receiver.c, src/receiver-stream.c
reconnecting and thread_active fields converted to atomic boolean loads/stores; receiver startup gated with atomic checks; shutdown performs atomic already-stopped check before joining threads.
Audio Scratch Buffers and Timing
src/receiver-audio.c
ensure_scratch() helper replaces per-call heap allocations for silence, pump output, and resample buffers; timing diagnostics deferred until post-throttle; PTS action locking refined; resampler error handling added.
Video Timestamp and Rendering
src/receiver-video.c, src/video-handler.c
Video PTS converted via av_rescale_q under audio_state_lock; frame_timestamp() snapshots audio state for consistent OBS mapping; negative linesize triggers swscale path; NV12 conversion reuses cached sws_nv12_buf.
Stream Reconnection and Recovery
src/receiver-stream.c, src/receiver-decode.c
Atomic operations for reconnecting flag; demuxer option parsing validates allocation; interrupt callback reads atomically; recovery marking runs under lock before fade_in updates; redundant assignments removed.
Source Lifecycle and Stats
src/irl-source.c
Stats generation snapshots shared state under audio_state_lock for race-free reporting; reconnect_count exported; thread creation failure handling added; scratch buffers freed on destruction.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Atomic hops through mutex gates,
Scratch buffers grow where heap once waits,
PTS rescales with FFmpeg grace,
Timestamps snap in locked embrace,
Thread-safe streams now find their pace! 🎬

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@datagutt
datagutt merged commit 8927950 into master May 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant