flowchart LR
subgraph Phone["Phone (EZ-Cam)"]
V["/video (MJPEG, H.264, RTSP, HLS...)"]
A["/audio.wav (any rate / channels)"]
end
subgraph Core["streambridge core (OS-independent)"]
U["urls.py<br/>allow-list, masking, LAN TLS"]
VW["engine.VideoWorker<br/>slot (depth 1) -> NV12 -> camera"]
AW["engine.AudioWorker<br/>swresample -> jitter buffer"]
R["reconnect.py<br/>backoff (10s connect / 5s stall)"]
end
subgraph OS["src/streambridge/platform"]
W["windows.py<br/>OBS Virtual Camera + VB-CABLE"]
L["linux.py<br/>v4l2loopback + PipeWire/PulseAudio"]
M["macos.py<br/>OBS Virtual Camera + BlackHole"]
end
V --> U --> VW --> CAM["sinks.CameraSink (pyvirtualcam)"]
A --> U --> AW --> MIC["sinks: SdlSink / SoundDeviceSink / PacatSink"]
R -.-> VW & AW
CAM --> W & L & M
MIC --> W & L & M
GUI["gui.py (Tkinter)"] --> S["session.py"]
CLI["cli.py"] --> S
S --> VW & AW
| Module | Responsibility | Key Invariants |
|---|---|---|
session.py |
Coordinates lifecycle of video & audio workers from Options. |
Joins worker and sender threads against a shared ~2 s deadline (STOP_TIMEOUT_S) before tearing down microphone sinks. |
engine.py |
Decodes media streams via PyAV, formats frames, manages output. | Never blocks decoder threads on device output. Pushes black frames during reconnection so applications don't drop the camera. |
urls.py |
Sanitizes, normalizes, and validates user stream endpoints. | Scheme allow-list; strictly masks credentials in logs and UI; enforces public TLS while accepting LAN self-signed certs. |
logging_setup.py |
Centralizes logging configuration and log directory management. | Front-end agnostic (cli.py, gui.py, and settings.py share it without circular dependencies); always filters out credentials. |
settings.py |
Persists configuration and recent history. | Writes atomically via temporary file + fsync + os.replace to prevent corrupted state on sudden termination. |
All OS-specific logic is strictly isolated in platform/ under a common Platform interface (check(), mic_setup(), mic_teardown(), camera_backend, audio_outputs()).
| Platform | Virtual Camera Target | Virtual Microphone Target | Primary Audio Sink |
|---|---|---|---|
| Windows | OBS Virtual Camera (obs) |
VB-CABLE (CABLE Input / CABLE Output) |
sdl_audio.SdlSink (SDL2 WASAPI shared); fallback to SoundDeviceSink (MME) |
| Linux | StreamBridge Camera (v4l2loopback) |
StreamBridge Microphone (streambridge_mic) |
sinks.PacatSink (pacat into module-null-sink / module-remap-source) |
| macOS | OBS Virtual Camera (obs) |
BlackHole 2ch (BlackHole) |
sinks.SoundDeviceSink (PortAudio Core Audio) |
Note
StreamBridge never requires administrator / root privileges. On Linux, it temporarily loads two user-space PulseAudio/PipeWire modules (streambridge_out and streambridge_mic) and cleans them up symmetrically on exit.
StreamBridge enforces minimal end-to-end latency without unbounded buffering:
-
Strict Queue Caps (Zero Delay Accumulation):
- Video: Depth-1 latest-frame slot. Decoded frames are dropped if the virtual camera backend is temporarily slower, preventing video delay from accumulating.
-
Audio: A single ring jitter buffer (
jitter.py). Oldest audio is pruned when exceeding threshold; clock drift is dynamically smoothed by 1–2 samples per block ($\le 0.8%$ ).
-
Pull-Driven Output:
- The device clock pulls audio on demand:
- Windows: SDL2 WASAPI callback (primary) or PortAudio callback (fallback)
- macOS: PortAudio Core Audio callback
-
Linux: 5 ms pacer thread writing directly to
pacat
- Decoders never block on playback output.
- The device clock pulls audio on demand:
-
Optimized Demuxing:
- Input streams are opened with low-latency parameters (
nobuffer,low_delay, and minimal probe sizes) equivalent toffplay -fflags nobuffer -flags low_delay.
- Input streams are opened with low-latency parameters (
[Connect Request] ──> [Session.start()] ──> [Daemon Threads: Decode + Send]
│
Stream Interrupted?
├──> Camera: Send 0.5s black frames (keep device active)
└──> Backoff: Exponential retry (0.5s to 10s)
│
[Stop Request] ──> [Session.stop()]
├──> Set worker stop events
├──> Join threads (max 2s STOP_TIMEOUT_S)
└──> mic_teardown() (guaranteed clean exit)
Important
If a worker is blocked inside an external network call (such as av.open up to the 10 s connect timeout), Session.stop() logs a warning after 2 seconds and proceeds immediately with mic teardown. Because worker threads are daemons, the GUI never freezes.
- Single Source of Truth: Version is declared solely in
src/streambridge/__init__.pyas__version__. - Dynamic Derivation: Read dynamically by
pyproject.toml, PyInstaller specifications, CLI--version, and runtime GUI dialogs. - Automated Verification:
tests/test_project_sync.pycontinuously guards against version drift across the repository and verifies synchronization betweenpyproject.tomlandrequirements.txt. - Tagged Releases: Tagging
vX.Y.Ztriggers.github/workflows/release.yml, which verifies that$GITHUB_REF_NAMEmatches__version__before generating signed provenance attestations and platform packages.