Call videoSrc.EnableKeyFrameCache() immediately after binding the video
UDP Source in allocAdjacentPair(). Audio sources are intentionally left
uncached — IDR detection on Opus packets would accumulate the entire
audio stream without ever resetting the burst.
- Add cache *keyFrameCache field to Source (nil by default).
- Add EnableKeyFrameCache() — call before Start() on video sources to
activate IDR burst caching.
- readLoop: call cache.push(pkt) after each successful unmarshal, before
the subscriber fanout. No lock held at push time — push acquires its
own mutex internally.
- Subscribe: snapshot the cache outside s.mu to avoid any cross-lock
complexity, then pre-fill the new channel with the burst before
registering it in the subscriber set. Uses a labeled break to stop
pre-filling if the channel is full (bufDepth too small for the burst;
the subscriber will wait for the next live keyframe instead).
Introduces keyFrameCache — a bounded ring buffer that retains all RTP
packets from the most recent H.264 IDR NAL unit until the packet just
before the next one. New WHEP subscribers receive this burst immediately
on Subscribe(), cutting first-frame latency from up to one IDR interval
(typically ~2 s at GOP=60/30fps) to nearly zero.
Design notes:
- Detection covers single-NAL (type 5) and FU-A start (type 28, start
bit set, inner type 5). STAP-A IDR leading is not handled — FFmpeg
never uses STAP-A for IDR slices in practice.
- Bounded at 512 packets / 2 MiB per source to cap memory per stream.
- push() is called only from the single-goroutine readLoop; the lock
it holds is tiny and brief.
- snapshot() returns a shallow copy; *rtp.Packet values are immutable
after being placed in the cache so sharing is safe.
The React bundle hash changes every time the UI is rebuilt. With the old
no-clobber approach, a redeployed container kept serving the old bundle
because the static/ directory already existed on the data volume.
Fix: always overwrite index.html, asset-manifest.json, and the entire
static/ subdirectory from /core/static on every container start. These
are build artifacts (not operator-edited content) so overwriting is safe.
All other top-level entries (channels/, config/, etc.) remain no-clobber.
Adds ProcessConfigWHIPIngest struct to http/api/process.go and wires it
into ProcessConfig.Marshal() and ProcessConfig.Unmarshal() so that the
WHIPIngest.Enabled flag can be set via API and correctly propagates to
app.Config.WHIPIngest, which is checked by onWHIPProcessStart in
app/webrtc/whip_lifecycle.go.
Without this change, app.Config.WHIPIngest.Enabled was always false and
WHIP ingest could never activate regardless of what the UI sent.
IngestPeer is the symmetric inverse of the WHEP Peer:
- Creates a recvonly PeerConnection (Pion receives tracks from the publisher)
- OnTrack -> reads RTP packets from the remote track and writes them to
loopback UDP ports (videoPort, audioPort) that FFmpeg is listening on
- Full lifecycle: Done(), Connected(), Close(), AddICECandidate()
PeerFactory.CreateIngestPeer() follows the same ctx/offer/ICE-gather
pattern as CreatePeerFromSources() so the app/webrtc handler layer can
use a uniform error matrix.
Adds the ConfigWHIPIngest struct alongside the existing ConfigWebRTC.
When Enabled=true the app/webrtc subsystem (next commit) will prepend
RTP UDP input legs to the FFmpeg command, binding on loopback ports
that WHIP publisher peers write received WebRTC tracks to.
promauto is not in the vendor tree. Replace promauto.With(reg).NewXxx()
with prometheus.NewXxx() + reg.MustRegister() — functionally identical
but uses only the already-vendored prometheus/client_golang/prometheus
package. Fixes the vendor-mode build error:
cannot find module providing package .../prometheus/promauto
- Rajdhani + JetBrains Mono typefaces via Google Fonts
- Deep dark palette: #09090d bg, #ff5c28 accent
- 22px dot-grid background texture
- Sticky frosted-glass header with WD monogram SVG
- Wild Dragon wordmark SVG on login panel
- Process cards with border-left state indicator (green/amber/red)
- Animated pulse dots on state badges
- Cleaner WHEP URL row with Copy + Open buttons
- Log panels hidden until first entry (no empty box on load)
- All JS functionality preserved (JWT auth, toggle+restart, WHEP copy)
Single-file HTML/JS admin page seeded into /core/data alongside
whep-player.html. Lets an operator log in with the API_AUTH_USERNAME
+ API_AUTH_PASSWORD creds, list every process, and toggle webrtc.enabled
per process with a single button. WHEP URL displayed for enabled
processes with a one-click "open in WHEP player" link.
Closes the v0.1 GUI gap: the upstream Restreamer UI we ship doesn't
know about Core's webrtc config block, so toggling WebRTC required
direct API calls. This page is the user-friendly path. Reachable at
/wilddragon-webrtc.html on any deploy.
No build step — drops in via the existing seed-data.sh flow.
Closes the v0.1 observability gap. Eleven new metrics in the
dragonfork_webrtc_* namespace (RED-method on the WHEP surface plus
state gauges from the WebRTC subsystem), Prom + Grafana containers
added to deploy/truenas/core/, four pre-loaded alert rules, one
pre-provisioned dashboard.
Hybrid instrumentation: direct client_golang in app/webrtc/ for
hot-path counters and histograms; snapshot collector in
prometheus/webrtc.go for slow-changing gauges. Rationale and
trade-offs against the upstream monitor/metric bus pattern documented
in the Approach section.
Targets v0.2.0-dragonfork.
The compose file's environment: block only forwarded the variables it
explicitly referenced — CORE_ADDRESS, CORE_API_AUTH_*, CORE_WEBRTC_*,
CORE_LOG_LEVEL. Everything else got the upstream Core defaults
regardless of what was in .env. So 'CORE_RTMP_ADDRESS=:1937' in .env
was silently ignored and Core kept binding 1935.
Hit on the live TrueNAS host where another datarhei/restreamer
container was already on 1935 with active stream state — couldn't
just stop it. Adding explicit env passthrough for the four common
collision points (RTMP, RTMPS, SRT, TLS) so an operator can remap
each individually without editing this file:
CORE_RTMP_ADDRESS=:1937
CORE_RTMP_ADDRESS_TLS=:1938
CORE_SRT_ADDRESS=:6002
CORE_TLS_ADDRESS=:8183
Defaults are unchanged — empty .env keeps :1935/:1936/:6000/:8181.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Layers Wild Dragon branding on top of upstream restreamer-ui v1.14.0
without forking the whole repo — keeps upstream UI updates flowing in
when we bump RESTREAMER_UI_REF.
Overlay (deploy/truenas/core/ui-overlay/):
public/index.html Wild Dragon title, theme color #0d0e12
public/manifest.json PWA name/short_name/colors
public/favicon.ico multi-res ICO (16/32/64) generated from
a 'WD' monogram in orange #ff6633 on dark
public/logo192.png Apple touch icon
public/logo512.png PWA install icon
src/misc/Logo/images/ rs-logo.svg (square mark, used in the
Header) and logo.svg (wordmark, used in
the Footer) — both Wild-Dragon-themed
src/misc/Logo/{index,rsLogo}.js
link the logos to forge.wilddragon.net
instead of datarhei.com
apply-overlay.sh runs in the Docker ui-builder stage just after the
upstream git clone and just before yarn install. Two phases:
1. rsync the overlay's public/ and src/ on top of the cloned
upstream tree
2. Targeted in-place patches for one-line UI strings (header
title, two welcome captions). Each patch is anchored to a
unique surrounding context and the script fails loudly if the
anchor isn't present — so a future upstream rename surfaces
immediately rather than silently shipping un-rebranded UI.
Image size: ~+50KB (the overlay assets), no measurable build-time
delta. PWA installs and OS bookmarks now show Wild Dragon. The
remaining 'Restreamer'/'datarhei' references in views/Welcome.js,
views/Login.js, views/Settings.js, etc. are deeper-page strings
that aren't worth a one-off overlay; they'll go away when we fork
the UI repo properly for the WebRTC tab milestone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>