cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

ci/pipeline-ergonomics
goq / portal / stream-metrics.mjs
stream-metrics.mjs 12.5K
1export const STREAM_RATE_WINDOW_MS = 1000;2export const MAX_STREAM_RATE_SAMPLES = 240;3export const CADENCE_HITCH_25_MS = 25;4export const CADENCE_HITCH_33_333_MS = 33.333;56export function mediaAuthenticationPresentation(networkDiagnostics) {7 const authentication = networkDiagnostics?.media_authentication;8 const modes = new Set(['ed25519-v1', 'transport-authenticated-v1']);9 const deliveryRoles = new Set(['direct_host', 'viewer_relay']);10 if (!authentication11 || !modes.has(authentication.mode)12 || !deliveryRoles.has(authentication.delivery_role)13 || typeof authentication.generation_certified !== 'boolean'14 || !Number.isSafeInteger(authentication.verification_failures)15 || authentication.verification_failures < 0) {16 return {17 mode: 'invalid',18 generationState: 'unverified',19 deliveryRole: 'unknown',20 verificationFailures: null,21 trusted: false,22 };23 }24 const authenticated = authentication.mode === 'ed25519-v1';25 return {26 mode: authentication.mode,27 generationState: authentication.generation_certified ? 'certified' : 'transport-bound',28 deliveryRole: authentication.delivery_role,29 verificationFailures: authentication.verification_failures,30 trusted: authenticated31 ? authentication.generation_certified && authentication.verification_failures === 032 : authentication.delivery_role === 'direct_host'33 && !authentication.generation_certified34 && authentication.verification_failures === 0,35 };36}3738/**39 * A bounded, monotonic rolling-rate estimator. Callers supply their own clock40 * so the same implementation is deterministic in tests and uses41 * performance.now() in the webview.42 */43export class RollingRateWindow {44 constructor(windowMs = STREAM_RATE_WINDOW_MS, maxSamples = MAX_STREAM_RATE_SAMPLES) {45 if (!Number.isFinite(windowMs) || windowMs <= 0) throw new RangeError('windowMs must be positive');46 if (!Number.isInteger(maxSamples) || maxSamples < 2) throw new RangeError('maxSamples must be at least two');47 this.windowMs = windowMs;48 this.maxSamples = maxSamples;49 this.samples = [];50 }5152 record(nowMs) {53 if (!Number.isFinite(nowMs)) throw new TypeError('sample time must be finite');54 const previous = this.samples.at(-1);55 if (previous !== undefined && nowMs < previous) throw new RangeError('sample time must be monotonic');56 this.samples.push(nowMs);57 if (this.samples.length > this.maxSamples) {58 this.samples.splice(0, this.samples.length - this.maxSamples);59 }60 this.#prune(nowMs);61 }6263 rate(nowMs) {64 if (!Number.isFinite(nowMs)) throw new TypeError('query time must be finite');65 this.#prune(nowMs);66 if (this.samples.length < 2) return 0;67 const first = this.samples[0];68 const last = this.samples.at(-1);69 const elapsedMs = last - first;70 return elapsedMs > 0 ? ((this.samples.length - 1) * 1000) / elapsedMs : 0;71 }7273 reset() {74 this.samples.length = 0;75 }7677 #prune(nowMs) {78 const cutoff = nowMs - this.windowMs;79 let remove = 0;80 while (remove < this.samples.length && this.samples[remove] < cutoff) remove++;81 if (remove > 0) this.samples.splice(0, remove);82 }83}8485export class BoundedLatencyWindow {86 constructor(windowMs = 2000, maxSamples = 512) {87 if (!Number.isFinite(windowMs) || windowMs <= 0) throw new RangeError('windowMs must be positive');88 if (!Number.isInteger(maxSamples) || maxSamples < 1) throw new RangeError('maxSamples must be positive');89 this.windowMs = windowMs;90 this.maxSamples = maxSamples;91 this.samples = [];92 }9394 record(valueMs, nowMs) {95 if (!Number.isFinite(valueMs) || valueMs < 0) throw new RangeError('latency must be finite and non-negative');96 if (!Number.isFinite(nowMs)) throw new TypeError('sample time must be finite');97 const previous = this.samples.at(-1);98 if (previous && nowMs < previous.time) throw new RangeError('sample time must be monotonic');99 this.samples.push({ time: nowMs, value: valueMs });100 if (this.samples.length > this.maxSamples) {101 this.samples.splice(0, this.samples.length - this.maxSamples);102 }103 this.#prune(nowMs);104 }105106 summary(nowMs) {107 if (!Number.isFinite(nowMs)) throw new TypeError('query time must be finite');108 this.#prune(nowMs);109 if (this.samples.length === 0) {110 return { p50: null, p95: null, p99: null, max: null, count: 0 };111 }112 const values = this.samples.map((sample) => sample.value).sort((a, b) => a - b);113 return {114 p50: values[Math.ceil(values.length * 0.50) - 1],115 p95: values[Math.ceil(values.length * 0.95) - 1],116 p99: values[Math.ceil(values.length * 0.99) - 1],117 max: values.at(-1),118 count: values.length,119 };120 }121122 reset() {123 this.samples.length = 0;124 }125126 #prune(nowMs) {127 const cutoff = nowMs - this.windowMs;128 let remove = 0;129 while (remove < this.samples.length && this.samples[remove].time < cutoff) remove++;130 if (remove > 0) this.samples.splice(0, remove);131 }132}133134export class BoundedValueWindow {135 constructor(windowMs = 5000, maxSamples = 512) {136 if (!Number.isFinite(windowMs) || windowMs <= 0) throw new RangeError('windowMs must be positive');137 if (!Number.isInteger(maxSamples) || maxSamples < 1) throw new RangeError('maxSamples must be positive');138 this.windowMs = windowMs;139 this.maxSamples = maxSamples;140 this.samples = [];141 }142143 record(value, nowMs) {144 if (!Number.isFinite(value)) throw new TypeError('value must be finite');145 if (!Number.isFinite(nowMs)) throw new TypeError('sample time must be finite');146 const previous = this.samples.at(-1);147 if (previous && nowMs < previous.time) throw new RangeError('sample time must be monotonic');148 this.samples.push({ time: nowMs, value });149 if (this.samples.length > this.maxSamples) {150 this.samples.splice(0, this.samples.length - this.maxSamples);151 }152 this.#prune(nowMs);153 }154155 summary(nowMs) {156 if (!Number.isFinite(nowMs)) throw new TypeError('query time must be finite');157 this.#prune(nowMs);158 if (this.samples.length === 0) {159 return { p50: null, p95: null, maxAbsolute: null, count: 0 };160 }161 const values = this.samples.map((sample) => sample.value).sort((a, b) => a - b);162 return {163 p50: values[Math.ceil(values.length * 0.50) - 1],164 p95: values[Math.ceil(values.length * 0.95) - 1],165 maxAbsolute: Math.max(...values.map(Math.abs)),166 count: values.length,167 };168 }169170 reset() {171 this.samples.length = 0;172 }173174 #prune(nowMs) {175 const cutoff = nowMs - this.windowMs;176 let remove = 0;177 while (remove < this.samples.length && this.samples[remove].time < cutoff) remove++;178 if (remove > 0) this.samples.splice(0, remove);179 }180}181182/**183 * A bounded rolling distribution of presentation intervals. The first184 * timestamp establishes the cadence anchor; each later timestamp contributes185 * one interval ending at that timestamp.186 */187export class BoundedCadenceWindow {188 constructor(windowMs = 5000, maxSamples = 512) {189 if (!Number.isFinite(windowMs) || windowMs <= 0) throw new RangeError('windowMs must be positive');190 if (!Number.isInteger(maxSamples) || maxSamples < 1) throw new RangeError('maxSamples must be positive');191 this.windowMs = windowMs;192 this.maxSamples = maxSamples;193 this.samples = [];194 this.lastSampleTimeMs = null;195 }196197 record(nowMs) {198 if (!Number.isFinite(nowMs)) throw new TypeError('sample time must be finite');199 if (this.lastSampleTimeMs !== null && nowMs < this.lastSampleTimeMs) {200 throw new RangeError('sample time must be monotonic');201 }202 if (this.lastSampleTimeMs !== null203 && nowMs - this.lastSampleTimeMs <= this.windowMs) {204 this.samples.push({ time: nowMs, value: nowMs - this.lastSampleTimeMs });205 if (this.samples.length > this.maxSamples) {206 this.samples.splice(0, this.samples.length - this.maxSamples);207 }208 }209 this.lastSampleTimeMs = nowMs;210 this.#prune(nowMs);211 }212213 summary(nowMs) {214 if (!Number.isFinite(nowMs)) throw new TypeError('query time must be finite');215 if (this.lastSampleTimeMs !== null && nowMs < this.lastSampleTimeMs) {216 throw new RangeError('query time must be monotonic');217 }218 this.#prune(nowMs);219 if (this.samples.length === 0) {220 return {221 p50: null,222 p95: null,223 p99: null,224 max: null,225 count: 0,226 over25Ms: 0,227 over33Ms: 0,228 };229 }230 const values = this.samples.map((sample) => sample.value).sort((a, b) => a - b);231 return {232 p50: values[Math.ceil(values.length * 0.50) - 1],233 p95: values[Math.ceil(values.length * 0.95) - 1],234 p99: values[Math.ceil(values.length * 0.99) - 1],235 max: values.at(-1),236 count: values.length,237 over25Ms: values.filter((value) => value > CADENCE_HITCH_25_MS).length,238 over33Ms: values.filter((value) => value > CADENCE_HITCH_33_333_MS).length,239 };240 }241242 reset() {243 this.samples.length = 0;244 this.lastSampleTimeMs = null;245 }246247 #prune(nowMs) {248 const cutoff = nowMs - this.windowMs;249 let remove = 0;250 while (remove < this.samples.length && this.samples[remove].time < cutoff) remove++;251 if (remove > 0) this.samples.splice(0, remove);252 }253}254255export class LatestFramePresenter {256 constructor({257 requestFrame,258 cancelFrame,259 setTimer,260 cancelTimer,261 now,262 fallbackDelayMs = 25,263 draw,264 onPresent = () => {},265 onDrop = () => {},266 }) {267 for (const [name, value] of Object.entries({268 requestFrame,269 cancelFrame,270 setTimer,271 cancelTimer,272 now,273 draw,274 })) {275 if (typeof value !== 'function') throw new TypeError(`${name} must be a function`);276 }277 if (!Number.isFinite(fallbackDelayMs) || fallbackDelayMs < 1 || fallbackDelayMs > 1000) {278 throw new RangeError('fallbackDelayMs must be within 1..1000 ms');279 }280 this.requestFrame = requestFrame;281 this.cancelFrame = cancelFrame;282 this.setTimer = setTimer;283 this.cancelTimer = cancelTimer;284 this.now = now;285 this.fallbackDelayMs = fallbackDelayMs;286 this.draw = draw;287 this.onPresent = onPresent;288 this.onDrop = onDrop;289 // Keep one frame for the next display refresh and one frame of jitter290 // tolerance. Decoder output commonly arrives in pairs even when its291 // average rate matches the display; a single latest-frame slot turns292 // those pairs into an avoidable 30–40 fps presentation cadence. The hard293 // two-frame ceiling still bounds latency, and overload evicts the oldest294 // stale frame first.295 this.pending = [];296 this.activeSchedule = null;297 }298299 enqueue(frame, metadata = null) {300 if (!frame || typeof frame.close !== 'function') throw new TypeError('frame must be closeable');301 if (this.pending.length >= 2) {302 const stale = this.pending.shift();303 stale.frame.close();304 this.onDrop(stale.metadata);305 }306 this.pending.push({ frame, metadata });307 this.#schedule();308 }309310 #schedule() {311 if (this.activeSchedule !== null || this.pending.length === 0) return;312 const schedule = {313 settled: false,314 frameHandle: null,315 timerHandle: null,316 };317 this.activeSchedule = schedule;318 const frameHandle = this.requestFrame((nowMs) => {319 this.#presentScheduled(schedule, 'frame', nowMs);320 });321 if (this.activeSchedule !== schedule || schedule.settled) return;322 schedule.frameHandle = frameHandle;323 const timerHandle = this.setTimer(() => {324 this.#presentScheduled(schedule, 'timer', this.now());325 }, this.fallbackDelayMs);326 if (this.activeSchedule === schedule && !schedule.settled) {327 schedule.timerHandle = timerHandle;328 } else {329 this.cancelTimer(timerHandle);330 }331 }332333 #presentScheduled(schedule, source, nowMs) {334 if (this.activeSchedule !== schedule || schedule.settled) return;335 schedule.settled = true;336 this.activeSchedule = null;337 if (source === 'frame') {338 if (schedule.timerHandle !== null) this.cancelTimer(schedule.timerHandle);339 } else if (schedule.frameHandle !== null) {340 this.cancelFrame(schedule.frameHandle);341 }342 const pending = this.pending.shift();343 if (!pending) return;344 try {345 this.draw(pending.frame);346 this.onPresent(pending.metadata, nowMs);347 } finally {348 pending.frame.close();349 this.#schedule();350 }351 }352353 clear() {354 const schedule = this.activeSchedule;355 this.activeSchedule = null;356 if (schedule !== null) {357 schedule.settled = true;358 if (schedule.frameHandle !== null) this.cancelFrame(schedule.frameHandle);359 if (schedule.timerHandle !== null) this.cancelTimer(schedule.timerHandle);360 }361 for (const pending of this.pending.splice(0)) pending.frame.close();362 }363364 get depth() {365 return this.pending.length;366 }367}