Go Live: Native Screen Watch And Self Publish
This document consolidates the Discord Go Live transport inside clankvox.
ClankVox is Clanky's main native package for voice and media transport. Go Live is the first video-heavy platform implementation living there, not the whole scope of the crate.
It covers:
- inbound native screen watch (
stream_watch) - outbound native self publish (
stream_publish) - how Clanky and the selfbot gateway feed stream credentials into the subprocess
Mental Model
Discord Go Live is not “extra fields on the normal voice socket.”
The main voice connection and the Go Live stream connection are separate legs:
- main voice leg: normal audio send/receive, speaking, voice session identity
- stream leg: video receive or send, stream-specific SSRCs, stream-server credentials
That is why the Discord transport models Go Live as extra transport roles
instead of trying to force everything through the primary voice slot.
Control Plane Vs Media Plane
Clanky and the selfbot gateway own the control plane:
- raw gateway dispatch handling
- stream discovery
- OP18
STREAM_CREATE - OP19
STREAM_DELETE - OP20
STREAM_WATCH - OP22
STREAM_SET_PAUSED - deciding which session should attach to which stream
clankvox owns the Discord media plane:
- stream-server WebSocket connection
- UDP media send/receive
- codec advertisement and selection
- DAVE and transport encryption
- inbound frame forwarding
- outbound H264 packetization
Shared Stream Facts
For both watch and publish, Clanky eventually supplies:
- stream endpoint
- stream token
rtc_server_id- main voice
session_id - self user id
- DAVE channel id
The DAVE channel derivation for stream connections is:
BigInt(rtc_server_id) - 1
That value is computed in Clanky and passed to clankvox over IPC.
stream_watch Flow
Inbound native watch works like this:
- Clanky discovers an active Go Live stream for a target user
- Clanky sends OP20
STREAM_WATCH - Discord returns
STREAM_CREATEandSTREAM_SERVER_UPDATE - Clanky calls
stream_watch_connect clankvoxopens the stream-server transport- Discord sends video state and media
clankvoxdecrypts/depacketizes frames and emits:user_video_stateuser_video_frameuser_video_end
- clankvox decodes H264 access units to JPEG in-process (
decoded_video_frame); Clanky decodes sampled VP8 keyframes to JPEG and feeds the higher-level screen-watch pipeline
The receiver path supports H264 and VP8 receive.
stream_publish Flow
Outbound self publish works like this:
- Clanky decides to publish a self-owned stream
- if needed, Clanky sends OP18
STREAM_CREATE - Clanky sends OP22
STREAM_SET_PAUSED { paused: false } - Discord returns self stream discovery and credentials
- Clanky calls:
stream_publish_connectstream_publish_playfor URL-backed publish, orstream_publish_browser_startfollowed by repeatedstream_publish_browser_frame
clankvoxopens the sender-side stream transportclankvoxadvertises H264 sender capability and announces active video stateclankvoxturns the active source into H264 access units:- URL-backed publish uses ffmpeg/yt-dlp
- browser-session publish feeds PNG frames into ffmpeg over stdin
- each access unit is DAVE-encrypted, RTP-packetized, and sent over UDP
Pause/resume/stop are split cleanly:
- pause: Clanky sends OP22 paused true and
stream_publish_pause - resume: Clanky reuses the existing stream when possible and sends OP22 paused false plus
stream_publish_resume - stop: Clanky sends OP19
STREAM_DELETEandstream_publish_stop/stream_publish_disconnect
Sender Boundary
The Discord sender path exists, but it is not yet a general-purpose arbitrary video publisher.
Rollout:
- publish lifecycle is tied to Clanky-owned source orchestration
- source support is intentionally narrow and centered on:
- YouTube-backed music/video URLs
- browser-session PNG frames captured on the Clanky side
(
../clanky-agentagent/lib/headless-browser.ts+agent/tools/web_capture_frames.ts)
- sender codec is H264
- transport is the native Discord stream server path, not the share-link fallback path
Why voice_conn.rs Is So Large
../src/voice_conn.rs owns the protocol-heavy work for the Discord implementation: both normal voice and Go Live.
- role-aware identify and select-protocol payloads
- READY parsing and stream SSRC extraction
- OP12/OP18/OP15 handling
- speaking and video-state announcements
- RTP packetization for outbound video
- inbound video depacketization handoff
- transport encryption mode handling
That file is effectively the protocol core of the crate.
Key Files
- ../src/voice_conn.rs: role-aware Discord voice/stream transport
- ../src/stream_publish.rs: sender pipeline and H264 frame feed
- ../src/video.rs: inbound depacketization helpers
- ../src/capture_supervisor.rs: watch-ready handling and subscriptions
- ../src/connection_supervisor.rs: role-specific connect/disconnect
- ../src/ipc.rs:
stream_watch_*andstream_publish_*IPC messages
Transport Crypto: rtpsize AAD Rules
Discord's aead_aes256_gcm_rtpsize and aead_xchacha20_poly1305_rtpsize modes
authenticate different slices of the packet depending on packet type:
- RTP media packets: AAD = RTP fixed header (12 bytes) + CSRC list (cc * 4
bytes) + 4-byte extension header prefix (profile + length). The extension
body is part of the ciphertext, not the AAD.
parse_rtp_headerreturns aheader_sizethat includes the full extension body — this value is correct for locating the payload start but must NOT be used as the AAD boundary.decrypt()recomputes the AAD from the raw packet bytes. - RTCP packets: AAD = the 4-byte RTCP fixed header.
decrypt_with_aad()is used directly withRTCP_HEADER_LEN.
Inbound RTCP packets (payload types 72-76 after masking, corresponding to RTCP types 200-204 per RFC 5761 mux) are filtered early in the UDP recv loop before any decrypt attempt. They are silently skipped because we do not process inbound RTCP feedback.
H264 Keyframe and SPS Strategy
Discord's raw UDP protocol path does not honour PLI or FIR RTCP feedback for
keyframe requests. PLI/FIR only works through the WebRTC protocol path used by
reference implementations like Discord-video-stream. Since clankvox uses
protocol: "udp", we cannot request keyframes on demand. PLI/FIR packets are
still sent as a best-effort hint in three scenarios: periodic reassertion
(every 2s), after decoder reset (50 consecutive errors), and after DAVE ready.
To compensate:
- Cached SPS+PPS are prepended to every emitted frame after DAVE decrypt.
prepend_cached_parameter_setsis a no-op when the frame already contains inline parameter sets. The prepend happens after DAVE decrypt (not during depacketization) so that DAVE trailer byte offsets remain correct. - Only IDR slices (NAL type 5) are treated as keyframes for rate-limiting and forwarding purposes.
- The persistent OpenH264 decoder processes all frames (IDR + P-frames) for reference state accumulation with error concealment enabled. The first decoded frame may have visual artifacts, but subsequent frames improve as P-slice prediction converges. After 50 consecutive decode errors the decoder auto-resets and requests PLI.
DAVE Video Decrypt
DAVE video decrypt works at near 100% on the main voice connection.
strip_rtp_padding() in rtp.rs strips RTP padding bytes before
depacketization, so AES-GCM tag verification succeeds on padded FU-A
mid-fragments.
Go Live streams are a separate, still-open problem. The davey crate
reports UnencryptedWhenPassthroughDisabled for every Go Live video frame,
but the frame bodies are actually encrypted — feeding them to a decoder
produces deblocking_filter_idc out of range and reference count overflow
errors characteristic of encrypted data being parsed as H264.
Behavior: video frames that fail DAVE decrypt with
UnencryptedWhenPassthroughDisabled are validated by looks_like_valid_h264()
and dropped when they appear to be encrypted. Audio passthrough is still
allowed.
The first few frames after DAVE session commit sometimes arrive genuinely
unencrypted (the sender hasn't started encrypting yet). When those frames
contain a real IDR (nal_types=[7, 8, 6, 5]), they decode successfully and
bootstrap the screen watch pipeline. After that initial window, DAVE-encrypted
frames are dropped and no new frames reach Clanky until the next unencrypted IDR
or a session reconnect.
Fixing Go Live DAVE decrypt requires either:
- updating the
daveycrate to correctly handle Go Live video DAVE framing - or connecting to Go Live streams using the WebRTC protocol path instead of raw UDP (which would also fix PLI/FIR)
ffmpeg Video Decode
H264 decode is handled entirely in-process by clankvox's persistent OpenH264
decoder (video_decoder.rs), running on a dedicated decode thread
(video_decode_worker.rs) behind a bounded drop-oldest frame lane so decode
and JPEG encode never stall the event loop. The fps gate runs between decode
and JPEG encode: every frame feeds the decoder's reference state, but
rate-limited frames skip the encode entirely. H264 frames do not use ffmpeg.
VP8 still uses per-frame ffmpeg decode on the Clanky side. The ffmpeg raw demuxer
hangs on single-frame input; Clanky works around this by piping through
cat | ffmpeg -fflags +genpts -f ivf -i pipe:0 which guarantees clean pipe
close and EOF delivery.
