code wiki / (root) / nx_jitter_buffer.nx

nx_jitter_buffer.nx source

↩ module page · 127 lines · 5293 B

1// nx_jitter_buffer.nx -- Adaptive jitter buffer for smooth video playback by reordering packets and adjusting delay based on network jitter. 2const JB_MAGIC_3550: i64 = 3550 3// nx_jitter_buffer.nx -- SOVEREIGN adaptive jitter buffer (the smoothness rung). BENCHMARK: WebRTC's 4// jitter buffer / NetEQ -- reorder out-of-order packets, absorb network jitter, PACE playout so frames 5// leave at a steady cadence instead of the bursty arrival that makes video "clunky". Built from scratch, 6// pure integer, NO third party, transport-AGNOSTIC (works over the WebSocket last-mile today, over raw 7// sovereign datagrams in NishiOS -- the core never sees the transport). RFC-3550-style interarrival 8// jitter estimate drives an ADAPTIVE playout delay (more jitter -> deeper buffer -> fewer skips, at the 9// cost of latency -- the exact trade WebRTC makes). Lost/late head frames are SKIPPED (conceal), never 10// stall the pipeline. Caller owns the ring (N slots x 4 i64 = seq,ts_us,arrival_us,valid) + state (16 i64). 11// license_tier: ORIGINAL 12 13// state indices 14const JB_NEXT: i64 = 0 // next seq to release 15const JB_TARGET: i64 = 1 // adaptive playout delay (us) 16const JB_JITTER: i64 = 2 // EWMA interarrival jitter (us) 17const JB_PREVTS: i64 = 3 18const JB_PREVARR: i64 = 4 19const JB_BASE: i64 = 5 // base delay floor (us) 20const JB_ANCHTS: i64 = 6 // ts of the frame that anchored playout 21const JB_ANCHPLAY: i64 = 7 // playout-clock time the anchor is released 22const JB_STARTED: i64 = 8 23const JB_COUNT: i64 = 9 24const JB_HAVEPREV: i64 = 10 25 26func jb_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 27 28func jb_init(st: *i64, base_delay_us: i64) -> i64 { 29 st[JB_NEXT] = 0 - 1 30 st[JB_TARGET] = base_delay_us 31 st[JB_JITTER] = 0 32 st[JB_PREVTS] = 0 33 st[JB_PREVARR] = 0 34 st[JB_BASE] = base_delay_us 35 st[JB_ANCHTS] = 0 36 st[JB_ANCHPLAY] = 0 37 st[JB_STARTED] = 0 38 st[JB_COUNT] = 0 39 st[JB_HAVEPREV] = 0 40 return 0 41} 42 43// insert a frame (its transport-decoded seq + media ts + local arrival time). Updates the adaptive delay. 44func jb_push(st: *i64, slots: *i64, N: i64, seq: i64, ts_us: i64, arr_us: i64) -> i64 { 45 if st[JB_HAVEPREV] == 1 { 46 let dts: i64 = ts_us - st[JB_PREVTS] 47 let darr: i64 = arr_us - st[JB_PREVARR] 48 let d: i64 = jb_abs(darr - dts) 49 st[JB_JITTER] = st[JB_JITTER] + (d - st[JB_JITTER]) / 16 // RFC-JB_MAGIC_3550 EWMA (1/16) 50 } 51 st[JB_PREVTS] = ts_us 52 st[JB_PREVARR] = arr_us 53 st[JB_HAVEPREV] = 1 54 // playout delay = base + 4x jitter (WebRTC uses ~3-4x std-dev of jitter) 55 st[JB_TARGET] = st[JB_BASE] + 4 * st[JB_JITTER] 56 // Once playout has STARTED, drop frames already behind the play head (too late to matter). BEFORE 57 // playout starts we have not committed to a position, so a lower seq arriving late (reordered) must 58 // still be accepted -- and it pulls the play head down to the true stream start. 59 if st[JB_STARTED] == 1 { if st[JB_NEXT] >= 0 { if seq < st[JB_NEXT] { return 0 } } } 60 let idx: i64 = (seq % N) * 4 61 slots[idx] = seq 62 slots[idx + 1] = ts_us 63 slots[idx + 2] = arr_us 64 slots[idx + 3] = 1 65 if st[JB_NEXT] < 0 { st[JB_NEXT] = seq } else { if st[JB_STARTED] == 0 { if seq < st[JB_NEXT] { st[JB_NEXT] = seq } } } 66 st[JB_COUNT] = st[JB_COUNT] + 1 67 return 0 68} 69 70// at playout-clock `now_us`, decide what (if anything) to release. out[0]=seq out[1]=status 71// status: 1 = RELEASED (play out[0]) ; 0 = HOLD (nothing due yet) ; -1 = SKIP (head lost/late, concealed) 72func jb_pop(st: *i64, slots: *i64, N: i64, now_us: i64, out: *i64) -> i64 { 73 out[0] = 0 - 1 74 out[1] = 0 75 let ns: i64 = st[JB_NEXT] 76 if ns < 0 { return 0 } 77 let idx: i64 = (ns % N) * 4 78 var present: i64 = 0 79 if slots[idx + 3] == 1 { if slots[idx] == ns { present = 1 } } 80 // start the playout clock when the head is first present: anchor its ts to now+target_delay 81 if st[JB_STARTED] == 0 { 82 if present == 1 { 83 st[JB_ANCHTS] = slots[idx + 1] 84 st[JB_ANCHPLAY] = now_us + st[JB_TARGET] 85 st[JB_STARTED] = 1 86 } else { 87 return 0 88 } 89 } 90 if present == 1 { 91 let deadline: i64 = st[JB_ANCHPLAY] + (slots[idx + 1] - st[JB_ANCHTS]) 92 if now_us >= deadline { 93 out[0] = ns 94 out[1] = 1 95 slots[idx + 3] = 0 96 st[JB_NEXT] = ns + 1 97 st[JB_COUNT] = st[JB_COUNT] - 1 98 return 0 99 } 100 return 0 101 } 102 // head missing: if ANY later frame is already past its own playout deadline, the head is lost/late -> 103 // skip it (conceal) so the pipeline keeps flowing instead of freezing on a gap. 104 var later_due: i64 = 0 105 var j: i64 = 0 106 while j < N { 107 if slots[j * 4 + 3] == 1 { 108 if slots[j * 4] > ns { 109 let dl: i64 = st[JB_ANCHPLAY] + (slots[j * 4 + 1] - st[JB_ANCHTS]) 110 if now_us >= dl { later_due = 1 } 111 } 112 } 113 j = j + 1 114 } 115 if later_due == 1 { 116 out[0] = ns 117 out[1] = 0 - 1 118 st[JB_NEXT] = ns + 1 119 return 0 120 } 121 return 0 122} 123 124func jb_target_delay(st: *i64) -> i64 { return st[JB_TARGET] } 125func jb_jitter(st: *i64) -> i64 { return st[JB_JITTER] } 126 127func main() -> i64 { return 0 }