Lag root cause: vPipe/aPipe were objectMode PassThrough HWM 128 → the pipe
held up to 128 frames ≈ 4.3s of video before backpressure reached the encoder.
The viewer was watching a 4+ second stale backlog.
Fixes (both faithful to @dank074/discord-video-stream):
1. vPipe/aPipe HWM 2 — at most ~1-2 frames in flight (~66ms @ 30fps), so the
writeFrame() backpressure pauses ffmpeg stdout almost immediately and the
whole chain (encoder → NUT → demuxer → vPipe → BaseMediaStream → WebRTC)
runs at the sender's real pace, exactly like dank's 'resume &&= vPipe.write'.
2. Wire vStream.syncStream = aStream — audio is the master clock; video
sleeps/wakes on ptsDelta like upstream newApi.js. Prevents A/V drift under
variable encoder throughput.
Per user direction ('pakai dank sebagai referensi karena itu yg berhasil'):
drop the custom setInterval/tail-drop emission clock entirely. The demuxer
now writes each access unit straight to vPipe with a monotonic PTS and lets
BaseMediaStream (ported 1:1 from @dank074) handle pacing via sleep-PTS + A/V
sync, exactly like the upstream library. The custom clocks were the source of
the blank tile (IDR delivery race) and the lag (head-drop watching 10s-old
frames).
Adds proper backpressure: pause ffmpeg stdout when vPipe.write() returns
false, resume on drain — mirrors dank's 'resume &&= vPipe.write(packet)' so the
encoder self-throttles to the WebRTC sender's real pace instead of bursting.
The tail-drop rewrite let a P-frame supersede a pending keyframe before the
emit tick fired, so the decoder never received an IDR → blank GoLive tile.
Give keyframes their own slot (pendingKey) that P-frames cannot steal, and
only emit a P-frame once at least one IDR has been shown (haveReference).
IDR is always emitted first when present so the reference re-establishes.
The Node token-bucket pacer used HEAD-drop (emit frames in arrival order,
drop newer ones when over budget). Under the encoder's ~330fps burst (ffmpeg
-re does not reliably throttle YouTube-DASH webm), the viewer was watching
frames ~10s behind live → frozen / 'patah-patah' video while audio (not
rate-limited) played current = desync.
Replace it with a steady setInterval emission clock at videoFps: each tick
emits exactly ONE frame — the NEWEST buffered one — and discards everything
older (tail-drop). At most one frame is ever held, so no backlog and no lag;
the emit clock (not the encoder rate) defines playback speed. Keyframes are
never superseded so the decoder keeps getting IDRs. Audio stays in sync.
yt-dlp 2026.07.04 rewrites the --cookies file on close. Handing it the
root-owned /etc/.../ytcookies.txt (not writable by the gmw service user)
caused PermissionError -> exit 1 on every screen-share download attempt.
- buildCookieArgs on-disk branch now copies the system cookie file into a
per-run temp file (like the env branch) so write-back lands somewhere we
own; unreadable -> anonymous.
- resolveInputWithRetry Invidious fallback regex now also matches
permission|EACCES|cookie, so a cookie failure triggers the link-alternative
(no-auth Invidious mirror) path instead of failing all retries.
- adds regression test asserting the original cookie path is never passed to yt-dlp
The live pipe (yt-dlp -o - -> ffmpeg) delivers data at network speed with
unreliable PTS, which defeats ffmpeg -re and made x264 -r 30 force-duplicate
held frames -> ~1fps video (the patah-patah symptom). Per user suggestion,
download the FULL clip to a temp file first (downloadScreenInput), then feed
that FILE PATH to prepareStream. String inputs already get -re, so the
encoder now paces cleanly at 1x against a monotonic-PTS file — proven
reliable in local tests (vs the live pipe which always bursted). Temp file
is removed on stream end / stop.
- getDirectScreenInput -> downloadScreenInput (returns file path)
- resolveInputWithRetry now awaits a completed file + retries on failure
- screenShareController.stops/cleanup removes the per-run tmpdir
- screenShareInput.test.ts updated to the file-download contract
Previous code only added ffmpeg -re when input was a string URL. Screen
share passes a Readable pipe (yt-dlp merge -> stdout) delivered at network
speed (bursts + stalls). Without -re the encoder slurps it instantly and,
when the merge stalls, x264 -r 30 force-duplicates the last held frame
~30x -> viewer sees ~1fps while WebRTC still paces 30fps. Add -re for all
inputs so the encoder paces at the stream's native PTS rate and emits a
fresh picture every frame.
- Demuxer.ts: deterministic token-bucket video pacing (replace unreliable ffmpeg -re which did not throttle the live multi-stage pipe — demuxer emitted ~240fps vs 30fps sender, 100k+ frame backlog, frozen video). Surplus non-key frames dropped; keyframes forced through; audio on fd3 unaffected.
- biome.json: pin noExplicitAny/noUnused* to off/warn. Biome 2.5.x (drifted via --no-frozen-lockfile) promotes these to errors and was failing the CI gate on pre-existing backend code unrelated to this change. Restores the warn-level behavior the config schema 2.2.0 expects.
Root cause (3rd iteration): ffmpeg '-re' on the demuxer does NOT reliably
throttle a multi-stage live pipe (merge ffmpeg -> encoder x264 -> NUT ->
demuxer). In production the demuxer still emitted ~240fps while the WebRTC
sender consumed 30fps, building a 100k+ frame backlog (observed: frames=197490
vs sent #24600, ~8.4 min in). The sender always emitted the OLDEST buffered
frame -> video frozen ~10 min behind live, while audio (tiny, jitter-buffer
recovered) stayed smooth. Local file/pipe tests showed -re working (30fps)
but the live YouTube/WebM pipeline did not — -re is not trustworthy here.
Fix: enforce 1x video output with a token-bucket limiter in the demuxer
(Node side), independent of ffmpeg. Capacity = 1s of frames, refill 1 token
per 1000/fps ms. Surplus non-key frames are DROPPED (never buffered) so the
sender always emits the newest frame; keyframes are forced through even over
budget so the decoder keeps a fresh IDR. The limiter does NOT stall the ffmpeg
process (unlike the earlier proc.stdout pause), so audio on fd3 keeps flowing.
Verified: tsc --noEmit clean.
Root cause (revisited): the previous gate paused proc.stdout when vPipe was
full. That stalled the SAME ffmpeg process that also writes audio on fd3, so
audio stuttered; and the ~8s backlog already built never drained → permanent
lag. Symptom: 'video still lags bad, now audio also choppy'.
Fix:
- spawn demuxer ffmpeg with -re for stream (pipe) input. Verified locally:
a 5s NUT clip demuxes in 0.088s without -re (57x burst) vs 4.539s with -re
(real-time). -re throttles the input read, which back-pressures the whole
upstream chain (encoder x264 -> merge ffmpeg -> yt-dlp) through OS pipes,
pinning production at 1x. No unbounded backlog.
- drop oldest queued frame when vPipe readableLength >= 30 (transient sender
stall guard) instead of pausing stdout — keeps video fresh and audio intact.
- removed gateSource/sourcePaused entirely.
Audio and video now pace together at 1x; video is the newest frame, not an
8-second-old one.
Root cause: prepareStream's ffmpeg consumed a YouTube VOD at download/CPU
speed (~10x real-time), so the demuxer buffered a huge frame backlog.
The sender paces at 30fps but always emitted the OLDEST buffered frames, so
the viewer saw frozen/laggy video while audio (tiny, jitter-buffer
recoverable) stayed smooth. That is exactly the 'video stuck, voice normal'
symptom reported live.
Fix: propagate vPipe backpressure UP to the demuxer's ffmpeg stdout — when
the sender can't keep up, pause the source, which stalls the demuxer and
back-pressures the encoder, pinning the whole pipeline to 1x. Also add a
realtime (-re) option for file/URL inputs (no-op for the streaming path,
which is what screen share uses).
Verified: 10s test clip encodes in 1.8s without -re vs 9.5s with it; tsc --noEmit clean.
Root cause: BaseMediaConnection.sendOpcode is a silent no-op when
ws.readyState !== OPEN. In GoLive, playStream() calls setVideoAttributes(true)
+ setSpeaking(true) the instant createStream() resolves (right after
SELECT_PROTOCOL_ACK), but the StreamConnection WebSocket can still be in
CONNECTING for a few ms — so op 12 (VIDEO, activating the video SSRC) was
silently DROPPED every session. Empirically verified: 0 ops 12/5 ever logged
across the entire journal, yet 10k+ video frames were sent and audio played
(audio SSRC is activated via the VoiceConnection handshake, independent of
GoLive op 12). Discord's media server thus received video RTP on video_ssrc
but was never told to forward it → black/broken shared-screen video with
working voice.
sendOpcodeWhenOpen retries up to ~2s for ws OPEN instead of dropping. Also
emits a=fmtp:101 packetization-mode=1;profile-level-id=42e01f in the answer
SDP (H264 FU-A fragments require packetization-mode=1 to reassemble).
Also removes pre-existing noNonNullAssertion lint (biome 2.5.8 now errors)
that was blocking the deploy CI.
Root cause: BaseMediaConnection.sendOpcode is a silent no-op when
ws.readyState !== OPEN. In GoLive, playStream() calls
setVideoAttributes(true) + setSpeaking(true) the instant createStream()
resolves (right after SELECT_PROTOCOL_ACK), but the StreamConnection WebSocket
can still be in CONNECTING for a few ms — so op 12 (VIDEO, enabling the video
SSRC) was silently DROPPED every session. Empirically verified: 0 ops 12/5 ever
logged across the entire journal, yet 10k+ video frames were sent and audio
played (audio SSRC is activated via the VoiceConnection handshake, independent
of GoLive op 12). Discord's media server thus received video RTP on video_ssrc
but was never told to forward it → black/broken shared-screen video with
working voice.
sendOpcodeWhenOpen retries up to ~2s for ws OPEN instead of dropping. Also
keeps the H264 packetization-mode=1 answer-SVP (defensive SDP correctness).
Also fix: emit a=fmtp:101 packetization-mode=1;profile-level-id=42e01f in the
answer SDP — H264 FU-A fragments require packetization-mode=1 to reassemble.
- Add examples for technical discussions (kinetic energy, drone weapon
engineering, physics simulations) that should be marked clean
- System rule: physics/engineering topics (kinetik, gravitasi, energi,
drone, senjata, drone warfare, CAD, CNC, 3D printing, robotics, aerospace)
are safe when in technical context — flag only if explicit threat
- Riwayat pengguna dengan pelanggaran sebelumnya tidak memengaruhi
penilaian pesan bersih yang terpisah dan tidak mengandung pelanggaran
- Removed getUserRecentInfractions usage in textBatchProcessor.ts and visionAnalyzer.ts
- Removed buildUserHistoryXml import and calls
- Messages are now evaluated standalone, not influenced by past violations in other channels
- Updated moderation prompts with clearer instructions about user_history usage
- Fixes issue where benign messages like 'tubuh manusia vs gravitasi' were incorrectly flagged due to carryover from previous drone weapons discussion
The user history context was causing the LLM to interpret unrelated current messages
as threats because it conflated them with past violations. Now each message is judged
on its own merit with only channel-specific context.
Verifies that two data URLs sharing the first 128 chars (same MIME prefix
+ identical base64 header — the real-world scenario that caused ALL images
to reuse the same cached vision analysis) produce DIFFERENT cache keys
under the fixed full-dataURL hashing, whereas the old 128-char-prefix
approach would collide. Also includes consistency + prefix tests.
Add debug logging to trace cacheKey + messageId + content length on
every vision cache HIT and MISS, so we can detect if the vision model
returns duplicate analysis for different images (provider issue vs
cache collision). Includes the phash on cache miss (new analysis cached).
Follow-up to 9f7ce7d which fixed makeImageCacheKey to hash full data
URL instead of just first 128 chars (root cause of all images sharing
the same cached 'konten judi' verdict due to hash collision).
Root cause: makeImageCacheKey() only hashed the first 128 chars of the
data URL. Since all resized images use the same MIME prefix
('data:image/png;base64,') + identical base64 header bytes, nearly every
image got the same 16-char hash → 'image:<same-hash>' → all images reused
the first cached vision analysis (often a gambling-detection verdict).
Fix: hash the entire data URL instead of just the prefix. Verified
114 stale 'image:' entries + 745 stale 'phash:' entries purged from prod
DB. tsc --noEmit clean, 133 tests pass.
bws-exec exposes the BWS secret as env GMW_YT_DOWNLOADER_COOKIES.
Materialize to temp Netscape file (yt-dlp --cookies needs a path).
Falls back to /etc/gmw-discord-gateway/ytcookies.txt written by deploy.
YouTube now blocks anonymous embeds (403 'Sign in to confirm you're not a
bot'). Resolve with account cookies via --cookies.
- mediaSource: buildCookieArgs() reads GMW_YT_COOKIES_PATH (default
/etc/gmw-discord-gateway/ytcookies.txt) and injects --cookies into
resolveMediaUrl + getDirectScreenInput + extractMediaInfo. Falls back
to anon if file missing (graceful 403, not crash).
- bws-exec now writes cookies file from BWS secret gmw_yt_downloader_cookies
on service start (systemd ConfigFile).
Root cause of "langsung left": YouTube bot-block/403 on u_c1tRmj7E4 (live
stream, LOGIN_REQUIRED) made yt-dlp timeout in resolveInputWithRetry (12s).
The timeout handler did cleanup() (removing once() listeners) THEN
tee.destroy(new Error(...)) — the PassThrough emitted 'error' with NO
listener left → unhandled stream 'error' event → uncaughtException →
gracefulShutdown → bot left voice.
Fix:
- resolveInputWithRetry: tee.destroy() silently after cleanup (error carried
in the rejection only); add permanent no-op tee.on('error') safety.
- prepareStream: output.on('error') no-op so ffmpeg spawn failure before
playStream attaches a demux listener never crashes the gateway.
- bootstrap: serialize uncaughtException/ClientError/DB errors with
{err, errorMsg, stack} (pino only serializes the 'err' magic key — the old
{error: err} key printed {} so crashes were invisible).
Revert 392bc35: streaming raw h264 video + opus on separate pipes broke
because prepareStream.output (pipe:1) feeds the Demuxer, but the opus
pipe:3 was never attached to the Demuxer's input — so for audio-capable
streams the Demuxer saw format=h264 (video-only) and emitted -an,
dropping audio RTP.
Correct design (from f1aa08c): prepareStream muxes video+audio into NUT
on a SINGLE pipe:1. The Demuxer then spawns a child ffmpeg that
demuxes NUT → -f h264 pipe:1 (pure AnnexB, start-code scan sees real
IDR type 5) + -f opus pipe:3 (Ogg Opus via createOggOpusDemux). The
start-code parser never touches NUT framing — it runs on the child
ffmpeg's clean h264 stdout.
Root cause: prepareStream muxed video+audio into a NUT container on pipe:1.
The Demuxer scans pipe:1 for AnnexB start codes (00 00 01) to split NAL
units into access units and classify keyframes (nal_type 5). NUT container
framing bytes sat in the stream and were scanned as NALs — NAL type 0
(NUT header) instead of 5 (IDR) → every frame classified key=false →
Discord decoder never got a decodable frame → static/black GoLive tile.
Fix: output raw H264 AnnexB on pipe:1 (demuxer target) and Ogg Opus on
fd3/pipe:3 for audio. NUT is only needed for *input* parsing (single
pipe carries both streams); output is demuxed into separate raw streams.
The demuxer resolved as soon as the VIDEO init line arrived on ffmpeg stderr.
With live NUT input the audio init line ('Stream #0:1: Audio: opus') lands in a
LATER stderr chunk (NUT info-stream packets are read incrementally from the
pipe), so `return { audio: aInfo }` captured undefined → playStream skipped
AudioStream → zero audio RTP on the audio SSRC → Discord showed a static
GoLive tile even though the NUT carried opus audio.
Fix:
- wait for BOTH video and audio init lines (when audio is expected) before
resolving demux metadata, with a 3s timeout fallback
- default aInfo to opus/48kHz when withAudio instead of undefined, so the
audio stream is always exposed even if the metadata line races the return
- Add term_glossary_cache table + migration 0014: resolved definitions are
stored permanently (definitions rarely change); misses stay ephemeral in
Redis/LRU with 1h TTL so transient failures get retried
- Lookup flow: LRU -> Redis -> Postgres (permanent) -> live SearXNG; DB hits
re-warm the fast caches; stale Redis miss sentinels no longer shadow DB
- Rate-limit-aware live lookups: concurrency 2 + stagger, retry once on empty
results, strict definition filter (Wikipedia preferred, rejects
disambiguation/ads/translate-homepages)
- Make SEARXNG_BASE_URL configurable via env (default unchanged)
Screen share showed a single frozen frame: the GoLive pipeline sent video
only (-"-an", h264 muxer cannot carry audio) so the audio SSRC never
transmitted and Discord kept the stream in thumbnail state.
- prepareStream: mux NUT when includeAudio (h264 muxer drops audio) and
return the actual container format
- Demuxer: support NUT input with a second output pipe (fd3) carrying
Ogg Opus; parse OGG pages into opus frames (20ms, 48kHz) emitted as
GoLiveFrames; fix metadata parsing that dropped the audio stream line
when it arrived in a later stderr chunk (early parsedMeta return)
- playStream: pipe audio.stream into AudioStream → RTP on the audio SSRC
- Encoders: -x264-params repeat-headers=1 → SPS/PPS inline before EVERY
IDR (NUT remux drops container extradata; also enables PLI recovery)
- screenShareController: includeAudio true
- tests: demuxerNut.test.ts — OGG parser unit test + real ffmpeg NUT
integration (video access units + parsed opus frames)
Second root cause (2026-08-12): even with yt-dlp http_headers forwarded,
YouTube still returns 403 when a signed DASH URL from --dump-single-json is
fetched raw by ffmpeg/curl on some videos (verified on fONoh7Pc6VU: curl
with the EXACT headers got 403; yt-dlp's own downloader succeeded). The
signature is tied to the extracting client context (po_token/visitor), not
just UA/IP.
Fix: getDirectScreenInput now spawns 'yt-dlp -o -' and returns its stdout
as a Readable — the same mechanism resolveMediaUrl already uses for music.
yt-dlp handles auth, cookies and transient retries internally. Merge
fragments go to /tmp/gmw-ytdlp-tmp (Nix store CWD is read-only → EACCES).
Removed resolveScreenInput + mergeScreenStreams (dead code).
Controller resolveInputWithRetry unchanged: tees the stream, waits for the
first byte (12s), retries with a fresh yt-dlp run up to 3x on error/EOF/
timeout, and destroys stuck inputs (EPIPE) so no process leaks.
Tests: rewritten for streaming (yt-dlp emits bytes; fail mode = exit 8
without stdout → stream must terminate with zero bytes).
Root cause (2026-08-12 11:50 test): merge ffmpeg hit a transient YouTube
403 and exited code 8 BEFORE prepareStream attached its input listeners
(voice release+join takes ~10s). The input's end/error events fired into
the void, the encoder stdin never received EOF, demux resolved with
fallback 0x0 metadata, setSpeaking fired anyway → stream 'started' with
zero frames for 8+ minutes (black tile, both ffmpeg processes hung).
Fixes:
- mediaSource: pass yt-dlp http_headers (UA/referer) to the merge ffmpeg
via -headers to suppress transient 403s; destroy the returned stream
with an error when the merge exits non-zero before producing bytes.
- screenShareController: resolveInputWithRetry — tee the merge stream and
wait for the first readable byte (12s timeout) before proceeding; on
error/EOF/timeout retry the whole resolution with a FRESH yt-dlp run
(signed DASH URLs expire fast) up to 3 attempts. Stuck merges get
EPIPE via input.destroy() so no process leaks per attempt.
- prepareStream: race guard — if the input already ended/destroyed before
listeners attach, EOF the encoder stdin immediately; first-frame
watchdog in playStream rejects 'started but nothing flowing' after 10s
instead of resolving with a silent black stream.
Tests: +2 (merge-fail zero-byte terminal state, -headers forwarding).
SDP offer advertises profile-level-id=42e01f (constrained baseline) but
x264 encoded the default High profile — Discord's receiver configures its
decoder from the negotiated profile, so the High-profile bitstream failed
to decode → black GoLive tile despite valid access units + correct RTP
timestamps (fixed in 42a503c).
- Add -profile:v baseline to H264 encoder options (matches @dank074's
proven config; SPS now 6742c01e → profile_idc=66 baseline, aligns with
the 42e01f fmtp).
- Default x264 tune film → zerolatency (no lookahead — correct for live
GoLive; @dank074 uses it).
- Update goLive-port test to assert baseline + zerolatency.
Demuxer emitted each AnnexB NAL as its own WebRTC frame (SPS/PPS/SEI
separate from slices) with a near-zero timestamp delta (duration=1 in a
1/90000 timebase → RTP +1/frame instead of +3000 @30fps). Discord's H264
receiver never receives a complete decodable access unit → black GoLive
tile despite frames flowing.
- Group NALs into access units: buffer param-set/SEI NALs, flush one
frame per slice with preceding parameter sets (AnnexB start codes kept
so the H264RtpPacketizer finds NAL boundaries).
- Timestamp each frame at the video frame rate: duration=1, timeBase
1/fps → BaseMediaStream frametime=1000/fps ms → RTP +clockRate/fps
(3000 @ 30fps/90kHz) and correct pacing.
- Thread explicit frameRate from playStream options (raw H264 has no
timing info; ffmpeg guesses 25fps on stderr).
- Strengthen golive-demux-live-e2e: validates every frame has a slice,
no bare param-set frames, keyframes carry SPS/PPS, timeBase 1/30.
Test 00:32 confirmed the video pipeline WORKS (1410 frames @ 1280x720 sent,
ready=true, camera off) but the gateway crashed at stream stop:
unhandledRejection → graceful shutdown → systemd restart (bot offline).
Root cause candidates (both were fire-and-forget promises without .catch):
- BaseMediaConnection.setProtocols().then(...) — rejects when the PC is
closed while setProtocols is in flight (stream teardown)
- void webRtcConn.createOffer().then(...) — rejects when the PC closes
while the offer is still gathering
Fixes:
- .catch on both promise chains (log + continue; teardown is expected)
- unhandledRejection handler now treats transient stream errors (EPIPE,
ERR_STREAM_DESTROYED, ERR_STREAM_WRITE_AFTER_END, ECONNRESET) like
uncaughtException already does — warn + continue instead of shutting
down the whole gateway. Non-transient rejections still log + shutdown
(with String(reason) so the detail actually shows).
ROOT CAUSE of empty GoLive tile (finally): prepareStream ran with
includeAudio: true + output -f h264. The h264 muxer cannot mux audio
('h264 muxer does not support any stream of type audio') → header write
fails -22 → stdout empty → Demuxer ffmpeg 'Invalid data found when
processing input' → 0 frames → black tile. Reproduced locally end-to-end
(13s backpressure delay + prepareStream + demux).
Fixes:
- screenShareController: includeAudio: false (video-only GoLive; demux
path never delivers audio anyway)
- Demuxer: pin input format -f h264 for stream inputs (raw AnnexB H264
has no magic header → auto-detect unreliable on delayed pipes)
- Streamer.signalStream: self_video: false — stop flipping on the bot's
camera in Discord (user request; screen share ≠ camera)
Verified: local repro now emits 644 frames 1280x720 (was 0); tsc/biome/
vitest all green.
Root cause of 'tile appears but content empty': demux() spooled the live
NUT/H264 input to a temp file and awaited stream 'finish' — but the merge
ffmpeg output never ends during playback, so demux deadlocked, no probe,
no transcode, 0 frames sent.
- Demuxer: pipe input straight into ffmpeg stdin (-i pipe:0), parse NAL
frames live from stdout; parse video metadata from ffmpeg stderr with a
1.5s race (fall back to H264 defaults). No spool, no await-end.
- screenShareController: pass width/height/frameRate (1280x720@30) to
playStream — matches the prepareStream encode settings, so setVideoAttributes
gets real dimensions even when ffmpeg can't report metadata on an open pipe.
- Add tests/golive-demux-live-e2e.ts: proves frames flow while input is
still open (regression test for the deadlock).
- signalStream: flip voice state to self_video:true/self_deaf:false before
STREAM_CREATE (Discord silently ignores the request while video disabled)
- createStream: attach dispatch listeners before first signal (race), clean
up listeners on timeout, retry STREAM_CREATE every 3s up to 4 attempts
(upstream issue #217/#219 — Discord randomly drops the request)
- sendOpcode: direct [goLive:Streamer] log bypassing bootstrap debug filter
(proves op 18 is actually broadcast)
binding.gyp appended '/libdatachannel.so.0.24.0' to LDC_LIB; nixpkgs output
layout is <out>/lib/libdatachannel.so.0.24.1. Make LDC_LIB the complete
library path (env or default) and drop the append.
libdatachannel-src fetchFromGitHub + manual cmake build fails: GitHub tarball
does not include git submodules (deps/plog, libjuice, libsrtp, usrsctp) →
CMake 'source directory does not contain CMakeLists.txt'.
Switch to pkgs.libdatachannel (0.24.1): nixpkgs builds submodules + ships
lib/dev outputs. In the Nix sandbox everything is consistent (store glibc),
so the GLIBC_ABI_GNU2_TLS issue that blocks host-local use of 0.24.1 does
not apply to the Nix build. binding.gyp defaults stay on local 0.24.0 for
dev; Nix sets LDC_INCLUDE/LDC_LIB to the store paths.
- Replace the per-package rebuild loop (node-datachannel cmake-js, zeromq)
with: opus build + libdatachannel-min N-API binding build (fetchFromGitHub
libdatachannel v0.24.0 — pinned because nixpkgs 0.24.1 is glibc-incompatible
with this host; sha256 1jk53qs…).
- Removes ~760MB of node-datachannel build/cleanup cruft from the build
phase; node_modules now 423MB (was 1.5GB).