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 }