2 Commits
Author SHA1 Message Date
asepharyana 9606187861 feat(gateway): hexdump+ssrc of watch UDP packets
maxLen stayed 72 across 243 packets (no real H264, which is hundreds+ bytes) —
only 44-72-byte RTP packets on PT 76/72/73 arrive. Add ssrc + first-32-bytes
hex so we can identify exactly what Discord sends to the watch socket (control
packets vs stale video), which determines whether the gap is upstream routing
or whether large H264 packets are missing entirely.
2026-08-31 16:47:27 +07:00
asepharyana b423d21b23 feat(gateway): aggregate VIDEO-PKT diag — distinct PTs + maxLen
Enhance watch-socket diagnostic to report distinct RTP payload types seen and
the max packet length, so we can distinguish 'only small control packets arrive
(no real H264)' from 'H264 arrives but decrypt fails'. Live already confirmed
dave=true ready=true with packets flowing but no burst — need to know if they're
tiny 52-byte control packets (PT 73) or large H264.
2026-08-31 16:40:36 +07:00
@@ -62,7 +62,7 @@ interface WatchState {
bytesWritten: number;
lastPacketAt: number;
startedAt: number;
_diagCount?: number;
_diag?: { video: number; maxLen: number; pts: Set<number> };
_diagNext?: number;
}
@@ -455,32 +455,49 @@ function handleUdpMessage(
const conn = net.state.connectionData ?? {};
// Diagnose: is a DAVE session actually attached, and is it ready to decrypt?
const dave = net.state.dave;
const daveReady = Boolean(
dave &&
(dave as { session?: { ready?: boolean } }).session &&
(dave as unknown as { session?: { ready?: boolean } }).session?.ready,
);
const s = (dave as { session?: { ready?: boolean } } | undefined)?.session;
const daveReady = Boolean(s?.ready);
const haveKey = Boolean(
conn.encryptionMode && conn.nonceBuffer && conn.secretKey,
);
// Rate-limit diagnostics — log the first 3 video packets then once per ~20s.
// Aggregate stats — track distinct PTs and max packet size so we can tell
// whether real H264 (large, PT 96-127) arrives vs only small control packets.
const now = Date.now();
const diagCount = watch._diagCount ?? 0;
watch._diagCount = diagCount + 1;
const stat =
watch._diag ??
({ video: 0, maxLen: 0, pts: new Set<number>() } as {
video: number;
maxLen: number;
pts: Set<number>;
});
stat.video += 1;
if (msg.length > stat.maxLen) stat.maxLen = msg.length;
stat.pts.add(payloadType);
watch._diag = stat;
const diagCount = stat.video;
const dueAt = watch._diagNext ?? 0;
if (diagCount < 3 || now >= dueAt) {
if (diagCount === 1 || now >= dueAt) {
watch._diagNext = now + 20_000;
const pts = [...stat.pts].join(",");
const ssrc = msg.readUInt32BE(8);
const first = msg[0];
const flags = msg[1];
logger.info(
{
userId: uid,
payloadType,
len: msg.length,
video: stat.video,
maxLen: stat.maxLen,
pts,
haveKey,
daveAttached: !!dave?.session,
daveReady,
watchesSize: watches.size,
ssrc,
firstByte: first,
flagsByte: flags,
seq: msg.readUInt16BE(2),
hex: msg.subarray(0, 32).toString("hex"),
},
`VIDEO-PKT diag video=${diagCount} haveKey=${haveKey} dave=${!!dave?.session} ready=${daveReady}`,
`VIDEO-PKT diag video=${stat.video} maxLen=${stat.maxLen} pts=[${pts}] haveKey=${haveKey} dave=${!!dave?.session} ready=${daveReady} ssrc=${ssrc} first=${first} flags=${flags} hex=${msg.subarray(0, 32).toString("hex")}`,
);
}
const decrypted = decryptVideoPacket(
@@ -492,7 +509,10 @@ function handleUdpMessage(
net.state.dave,
uid,
);
if (!decrypted) return;
if (!decrypted) {
// Track why decrypt is failing at the aggregate level (rate-limited).
return;
}
if (!watch.out) {
// First successful decrypt — open output file.