From 7843f8b0e4e50b7171726f85d995593cc651b4b4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 22 May 2023 21:49:02 -0700 Subject: [PATCH] Using spaces is WAY better for your career. This is because in JavaScript, there is a HEAVY perception bias against tab users. Most ES6 developers look down on anyone who uses tabs. This could easily mean the difference between getting a job offer and being rejected. It could mean tens of thousands of dollars of salary potential. https://medium.com/mintbean-io/tabs-or-spaces-for-practical-javascript-developers-the-answer-is-clear-f66c0458aa1e --- web/.eslintrc.cjs | 46 ++-- web/.prettierrc.json | 4 +- web/.proxyrc.js | 10 +- web/package.json | 50 ++-- web/src/broadcaster/encoder.ts | 158 +++++------ web/src/broadcaster/index.ts | 6 +- web/src/index.css | 72 ++--- web/src/index.html | 48 ++-- web/src/index.ts | 26 +- web/src/mp4/index.ts | 22 +- web/src/mp4/init.ts | 58 ++-- web/src/mp4/mp4box.d.ts | 402 ++++++++++++++-------------- web/src/player/audio.ts | 124 ++++----- web/src/player/decoder.ts | 264 +++++++++--------- web/src/player/index.ts | 124 ++++----- web/src/player/message.ts | 16 +- web/src/player/renderer.ts | 42 +-- web/src/player/ring.ts | 236 ++++++++-------- web/src/player/video.ts | 152 +++++------ web/src/player/worker.ts | 28 +- web/src/player/worklet.ts | 72 ++--- web/src/stream/reader.ts | 347 ++++++++++++------------ web/src/stream/writer.ts | 160 +++++------ web/src/transport/index.ts | 144 +++++----- web/src/transport/interface.ts | 12 +- web/src/transport/message.ts | 2 +- web/src/transport/webtransport.d.ts | 82 +++--- web/src/util/deferred.ts | 32 +-- web/tsconfig.json | 14 +- 29 files changed, 1366 insertions(+), 1387 deletions(-) diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index b2c5a94..61796f9 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -1,26 +1,26 @@ /* eslint-env node */ module.exports = { - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - ], - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], - root: true, - ignorePatterns: ["dist", "node_modules"], - rules: { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-explicit-any": "off", - "no-unused-vars": "off", // note you must disable the base rule as it can report incorrect errors - "@typescript-eslint/no-unused-vars": [ - "warn", // or "error" - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + ], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + root: true, + ignorePatterns: ["dist", "node_modules"], + rules: { + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-explicit-any": "off", + "no-unused-vars": "off", // note you must disable the base rule as it can report incorrect errors + "@typescript-eslint/no-unused-vars": [ + "warn", // or "error" + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, } diff --git a/web/.prettierrc.json b/web/.prettierrc.json index b6defc1..cce9d3c 100644 --- a/web/.prettierrc.json +++ b/web/.prettierrc.json @@ -1,5 +1,3 @@ { - "useTabs": true, - "tabWidth": 4, - "semi": false + "semi": false } diff --git a/web/.proxyrc.js b/web/.proxyrc.js index 3554d52..1295b7d 100644 --- a/web/.proxyrc.js +++ b/web/.proxyrc.js @@ -1,7 +1,7 @@ module.exports = function (app) { - app.use((req, res, next) => { - res.setHeader("Cross-Origin-Opener-Policy", "same-origin") - res.setHeader("Cross-Origin-Embedder-Policy", "require-corp") - next() - }) + app.use((req, res, next) => { + res.setHeader("Cross-Origin-Opener-Policy", "same-origin") + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp") + next() + }) } diff --git a/web/package.json b/web/package.json index 4093bcc..0cdf4ab 100644 --- a/web/package.json +++ b/web/package.json @@ -1,27 +1,27 @@ { - "license": "Apache-2.0", - "source": "src/index.html", - "scripts": { - "serve": "parcel serve --https --cert ../cert/localhost.crt --key ../cert/localhost.key --port 4444 --open", - "build": "parcel build", - "check": "tsc --noEmit", - "lint": "eslint .", - "fmt": "prettier --write ." - }, - "devDependencies": { - "@parcel/transformer-inline-string": "2.8.3", - "@parcel/validator-typescript": "^2.6.0", - "@types/audioworklet": "^0.0.41", - "@types/dom-webcodecs": "^0.1.6", - "@typescript-eslint/eslint-plugin": "^5.59.7", - "@typescript-eslint/parser": "^5.59.7", - "eslint": "^8.41.0", - "eslint-config-prettier": "^8.8.0", - "parcel": "^2.8.0", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "mp4box": "^0.5.2" - } + "license": "Apache-2.0", + "source": "src/index.html", + "scripts": { + "serve": "parcel serve --https --cert ../cert/localhost.crt --key ../cert/localhost.key --port 4444 --open", + "build": "parcel build", + "check": "tsc --noEmit", + "lint": "eslint .", + "fmt": "prettier --write ." + }, + "devDependencies": { + "@parcel/transformer-inline-string": "2.8.3", + "@parcel/validator-typescript": "^2.6.0", + "@types/audioworklet": "^0.0.41", + "@types/dom-webcodecs": "^0.1.6", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", + "eslint": "^8.41.0", + "eslint-config-prettier": "^8.8.0", + "parcel": "^2.8.0", + "prettier": "^2.8.8", + "typescript": "^5.0.4" + }, + "dependencies": { + "mp4box": "^0.5.2" + } } diff --git a/web/src/broadcaster/encoder.ts b/web/src/broadcaster/encoder.ts index 4ba7ff7..76ee774 100644 --- a/web/src/broadcaster/encoder.ts +++ b/web/src/broadcaster/encoder.ts @@ -1,104 +1,104 @@ import * as MP4 from "../mp4" export class Encoder { - container: MP4.ISOFile - audio: AudioEncoder - video: VideoEncoder + container: MP4.ISOFile + audio: AudioEncoder + video: VideoEncoder - constructor() { - this.container = new MP4.ISOFile() + constructor() { + this.container = new MP4.ISOFile() - this.audio = new AudioEncoder({ - output: this.onAudio.bind(this), - error: console.warn, - }) + this.audio = new AudioEncoder({ + output: this.onAudio.bind(this), + error: console.warn, + }) - this.video = new VideoEncoder({ - output: this.onVideo.bind(this), - error: console.warn, - }) + this.video = new VideoEncoder({ + output: this.onVideo.bind(this), + error: console.warn, + }) - this.container.init() + this.container.init() - this.audio.configure({ - codec: "mp4a.40.2", - numberOfChannels: 2, - sampleRate: 44100, + this.audio.configure({ + codec: "mp4a.40.2", + numberOfChannels: 2, + sampleRate: 44100, - // TODO bitrate - }) + // TODO bitrate + }) - this.video.configure({ - codec: "avc1.42002A", // TODO h.264 baseline - avc: { format: "avc" }, // or annexb - width: 1280, - height: 720, + this.video.configure({ + codec: "avc1.42002A", // TODO h.264 baseline + avc: { format: "avc" }, // or annexb + width: 1280, + height: 720, - // TODO bitrate - // TODO bitrateMode - // TODO framerate - // TODO latencyMode - }) - } + // TODO bitrate + // TODO bitrateMode + // TODO framerate + // TODO latencyMode + }) + } - onAudio(frame: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata) { - const config = metadata.decoderConfig! - const track_id = 1 + onAudio(frame: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata) { + const config = metadata.decoderConfig! + const track_id = 1 - if (!this.container.getTrackById(track_id)) { - this.container.addTrack({ - id: track_id, - type: "mp4a", // TODO wrong - timescale: 1000, // TODO verify + if (!this.container.getTrackById(track_id)) { + this.container.addTrack({ + id: track_id, + type: "mp4a", // TODO wrong + timescale: 1000, // TODO verify - channel_count: config.numberOfChannels, - samplerate: config.sampleRate, + channel_count: config.numberOfChannels, + samplerate: config.sampleRate, - description: config.description, // TODO verify - // TODO description_boxes?: Box[]; - }) - } + description: config.description, // TODO verify + // TODO description_boxes?: Box[]; + }) + } - const buffer = new Uint8Array(frame.byteLength) - frame.copyTo(buffer) + const buffer = new Uint8Array(frame.byteLength) + frame.copyTo(buffer) - // TODO cts? - const sample = this.container.addSample(track_id, buffer, { - is_sync: frame.type == "key", - duration: frame.duration!, - dts: frame.timestamp, - }) + // TODO cts? + const sample = this.container.addSample(track_id, buffer, { + is_sync: frame.type == "key", + duration: frame.duration!, + dts: frame.timestamp, + }) - const _stream = this.container.createSingleSampleMoof(sample) - } + const _stream = this.container.createSingleSampleMoof(sample) + } - onVideo(frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) { - const config = metadata!.decoderConfig! - const track_id = 2 + onVideo(frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) { + const config = metadata!.decoderConfig! + const track_id = 2 - if (!this.container.getTrackById(track_id)) { - this.container.addTrack({ - id: 2, - type: "avc1", - width: config.codedWidth, - height: config.codedHeight, - timescale: 1000, // TODO verify + if (!this.container.getTrackById(track_id)) { + this.container.addTrack({ + id: 2, + type: "avc1", + width: config.codedWidth, + height: config.codedHeight, + timescale: 1000, // TODO verify - description: config.description, // TODO verify - // TODO description_boxes?: Box[]; - }) - } + description: config.description, // TODO verify + // TODO description_boxes?: Box[]; + }) + } - const buffer = new Uint8Array(frame.byteLength) - frame.copyTo(buffer) + const buffer = new Uint8Array(frame.byteLength) + frame.copyTo(buffer) - // TODO cts? - const sample = this.container.addSample(track_id, buffer, { - is_sync: frame.type == "key", - duration: frame.duration!, - dts: frame.timestamp, - }) + // TODO cts? + const sample = this.container.addSample(track_id, buffer, { + is_sync: frame.type == "key", + duration: frame.duration!, + dts: frame.timestamp, + }) - const _stream = this.container.createSingleSampleMoof(sample) - } + const _stream = this.container.createSingleSampleMoof(sample) + } } diff --git a/web/src/broadcaster/index.ts b/web/src/broadcaster/index.ts index fa40b01..f17e9af 100644 --- a/web/src/broadcaster/index.ts +++ b/web/src/broadcaster/index.ts @@ -1,5 +1,5 @@ export default class Broadcaster { - constructor() { - // TODO - } + constructor() { + // TODO + } } diff --git a/web/src/index.css b/web/src/index.css index c3d1639..871df82 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,75 +1,75 @@ html, body, #player { - width: 100%; + width: 100%; } body { - background: #000000; - color: #ffffff; - padding: 0; - margin: 0; - display: flex; - justify-content: center; - font-family: sans-serif; + background: #000000; + color: #ffffff; + padding: 0; + margin: 0; + display: flex; + justify-content: center; + font-family: sans-serif; } #screen { - position: relative; + position: relative; } #screen #play { - position: absolute; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); + position: absolute; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; + display: flex; + justify-content: center; + align-items: center; - z-index: 1; + z-index: 1; } #controls { - display: flex; - flex-wrap: wrap; - padding: 8px 16px; + display: flex; + flex-wrap: wrap; + padding: 8px 16px; } #controls > * { - margin-right: 8px; + margin-right: 8px; } #controls label { - margin-right: 8px; + margin-right: 8px; } #stats { - display: grid; - grid-template-columns: auto 1fr; + display: grid; + grid-template-columns: auto 1fr; } #stats label { - padding: 0 1rem; + padding: 0 1rem; } .buffer { - position: relative; - width: 100%; + position: relative; + width: 100%; } .buffer .fill { - position: absolute; - transition-duration: 0.1s; - transition-property: left, right, background-color; - background-color: RebeccaPurple; - height: 100%; - text-align: right; - padding-right: 0.5rem; - overflow: hidden; + position: absolute; + transition-duration: 0.1s; + transition-property: left, right, background-color; + background-color: RebeccaPurple; + height: 100%; + text-align: right; + padding-right: 0.5rem; + overflow: hidden; } .buffer .fill.net { - background-color: Purple; + background-color: Purple; } diff --git a/web/src/index.html b/web/src/index.html index 4171b09..67ddd41 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -1,33 +1,33 @@  - - - WARP + + + WARP - - + + - -
-
-
click to play
- -
+ +
+
+
click to play
+ +
-
- - -
+
+ + +
-
- -
+
+ +
- -
-
-
+ +
+
+
- - + + diff --git a/web/src/index.ts b/web/src/index.ts index 5c04336..f0f3b21 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -7,7 +7,7 @@ import fingerprintHex from "bundle-text:../fingerprint.hex" // Convert the hex to binary. const fingerprint = [] for (let c = 0; c < fingerprintHex.length - 1; c += 2) { - fingerprint.push(parseInt(fingerprintHex.substring(c, c + 2), 16)) + fingerprint.push(parseInt(fingerprintHex.substring(c, c + 2), 16)) } const params = new URLSearchParams(window.location.search) @@ -16,27 +16,27 @@ const url = params.get("url") || "https://localhost:4443/watch" const canvas = document.querySelector("canvas#video")! const transport = new Transport({ - url: url, - fingerprint: { - // TODO remove when Chrome accepts the system CA - algorithm: "sha-256", - value: new Uint8Array(fingerprint), - }, + url: url, + fingerprint: { + // TODO remove when Chrome accepts the system CA + algorithm: "sha-256", + value: new Uint8Array(fingerprint), + }, }) const player = new Player({ - transport, - canvas: canvas.transferControlToOffscreen(), + transport, + canvas: canvas.transferControlToOffscreen(), }) const play = document.querySelector("#screen #play")! const playFunc = (e: Event) => { - player.play() - e.preventDefault() + player.play() + e.preventDefault() - play.removeEventListener("click", playFunc) - play.style.display = "none" + play.removeEventListener("click", playFunc) + play.style.display = "none" } play.addEventListener("click", playFunc) diff --git a/web/src/mp4/index.ts b/web/src/mp4/index.ts index 8c4cd6f..0a6a50a 100644 --- a/web/src/mp4/index.ts +++ b/web/src/mp4/index.ts @@ -1,16 +1,16 @@ // Rename some stuff so it's on brand. export { - createFile as New, - MP4File as File, - MP4ArrayBuffer as ArrayBuffer, - MP4Info as Info, - MP4Track as Track, - MP4AudioTrack as AudioTrack, - MP4VideoTrack as VideoTrack, - DataStream as Stream, - Box, - ISOFile, - Sample, + createFile as New, + MP4File as File, + MP4ArrayBuffer as ArrayBuffer, + MP4Info as Info, + MP4Track as Track, + MP4AudioTrack as AudioTrack, + MP4VideoTrack as VideoTrack, + DataStream as Stream, + Box, + ISOFile, + Sample, } from "mp4box" export { Init, InitParser } from "./init" diff --git a/web/src/mp4/init.ts b/web/src/mp4/init.ts index 5ff2802..70b885b 100644 --- a/web/src/mp4/init.ts +++ b/web/src/mp4/init.ts @@ -1,43 +1,43 @@ import * as MP4 from "./index" export interface Init { - raw: MP4.ArrayBuffer - info: MP4.Info + raw: MP4.ArrayBuffer + info: MP4.Info } export class InitParser { - mp4box: MP4.File - offset: number + mp4box: MP4.File + offset: number - raw: MP4.ArrayBuffer[] - info: Promise + raw: MP4.ArrayBuffer[] + info: Promise - constructor() { - this.mp4box = MP4.New() - this.raw = [] - this.offset = 0 + constructor() { + this.mp4box = MP4.New() + this.raw = [] + this.offset = 0 - // Create a promise that gets resolved once the init segment has been parsed. - this.info = new Promise((resolve, reject) => { - this.mp4box.onError = reject - this.mp4box.onReady = resolve - }) - } + // Create a promise that gets resolved once the init segment has been parsed. + this.info = new Promise((resolve, reject) => { + this.mp4box.onError = reject + this.mp4box.onReady = resolve + }) + } - push(data: Uint8Array) { - // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately - const box = new Uint8Array(data.byteLength) - box.set(data) + push(data: Uint8Array) { + // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately + const box = new Uint8Array(data.byteLength) + box.set(data) - // and for some reason we need to modify the underlying ArrayBuffer with fileStart - const buffer = box.buffer as MP4.ArrayBuffer - buffer.fileStart = this.offset + // and for some reason we need to modify the underlying ArrayBuffer with fileStart + const buffer = box.buffer as MP4.ArrayBuffer + buffer.fileStart = this.offset - // Parse the data - this.offset = this.mp4box.appendBuffer(buffer) - this.mp4box.flush() + // Parse the data + this.offset = this.mp4box.appendBuffer(buffer) + this.mp4box.flush() - // Add the box to our queue of chunks - this.raw.push(buffer) - } + // Add the box to our queue of chunks + this.raw.push(buffer) + } } diff --git a/web/src/mp4/mp4box.d.ts b/web/src/mp4/mp4box.d.ts index 788d48e..4c07e19 100644 --- a/web/src/mp4/mp4box.d.ts +++ b/web/src/mp4/mp4box.d.ts @@ -1,243 +1,239 @@ // https://github.com/gpac/mp4box.js/issues/233 declare module "mp4box" { - export interface MP4MediaTrack { - id: number - created: Date - modified: Date - movie_duration: number - layer: number - alternate_group: number - volume: number - track_width: number - track_height: number - timescale: number - duration: number - bitrate: number - codec: string - language: string - nb_samples: number - } + export interface MP4MediaTrack { + id: number + created: Date + modified: Date + movie_duration: number + layer: number + alternate_group: number + volume: number + track_width: number + track_height: number + timescale: number + duration: number + bitrate: number + codec: string + language: string + nb_samples: number + } - export interface MP4VideoData { - width: number - height: number - } + export interface MP4VideoData { + width: number + height: number + } - export interface MP4VideoTrack extends MP4MediaTrack { - video: MP4VideoData - } + export interface MP4VideoTrack extends MP4MediaTrack { + video: MP4VideoData + } - export interface MP4AudioData { - sample_rate: number - channel_count: number - sample_size: number - } + export interface MP4AudioData { + sample_rate: number + channel_count: number + sample_size: number + } - export interface MP4AudioTrack extends MP4MediaTrack { - audio: MP4AudioData - } + export interface MP4AudioTrack extends MP4MediaTrack { + audio: MP4AudioData + } - export type MP4Track = MP4VideoTrack | MP4AudioTrack + export type MP4Track = MP4VideoTrack | MP4AudioTrack - export interface MP4Info { - duration: number - timescale: number - fragment_duration: number - isFragmented: boolean - isProgressive: boolean - hasIOD: boolean - brands: string[] - created: Date - modified: Date - tracks: MP4Track[] - mime: string - audioTracks: MP4AudioTrack[] - videoTracks: MP4VideoTrack[] - } + export interface MP4Info { + duration: number + timescale: number + fragment_duration: number + isFragmented: boolean + isProgressive: boolean + hasIOD: boolean + brands: string[] + created: Date + modified: Date + tracks: MP4Track[] + mime: string + audioTracks: MP4AudioTrack[] + videoTracks: MP4VideoTrack[] + } - export type MP4ArrayBuffer = ArrayBuffer & { fileStart: number } + export type MP4ArrayBuffer = ArrayBuffer & { fileStart: number } - export interface MP4File { - onMoovStart?: () => void - onReady?: (info: MP4Info) => void - onError?: (e: string) => void - onSamples?: (id: number, user: any, samples: Sample[]) => void + export interface MP4File { + onMoovStart?: () => void + onReady?: (info: MP4Info) => void + onError?: (e: string) => void + onSamples?: (id: number, user: any, samples: Sample[]) => void - appendBuffer(data: MP4ArrayBuffer): number - start(): void - stop(): void - flush(): void + appendBuffer(data: MP4ArrayBuffer): number + start(): void + stop(): void + flush(): void - setExtractionOptions( - id: number, - user: any, - options: ExtractionOptions - ): void - } + setExtractionOptions( + id: number, + user: any, + options: ExtractionOptions + ): void + } - export function createFile(): MP4File + export function createFile(): MP4File - export interface Sample { - number: number - track_id: number - timescale: number - description_index: number - description: any - data: ArrayBuffer - size: number - alreadyRead?: number - duration: number - cts: number - dts: number - is_sync: boolean - is_leading: number - depends_on: number - is_depended_on: number - has_redundancy: number - degration_priority: number - offset: number - subsamples: any - } + export interface Sample { + number: number + track_id: number + timescale: number + description_index: number + description: any + data: ArrayBuffer + size: number + alreadyRead?: number + duration: number + cts: number + dts: number + is_sync: boolean + is_leading: number + depends_on: number + is_depended_on: number + has_redundancy: number + degration_priority: number + offset: number + subsamples: any + } - export interface ExtractionOptions { - nbSamples: number - } + export interface ExtractionOptions { + nbSamples: number + } - const BIG_ENDIAN: boolean - const LITTLE_ENDIAN: boolean + const BIG_ENDIAN: boolean + const LITTLE_ENDIAN: boolean - export class DataStream { - constructor( - buffer?: ArrayBuffer, - byteOffset?: number, - littleEndian?: boolean - ) - getPosition(): number + export class DataStream { + constructor( + buffer?: ArrayBuffer, + byteOffset?: number, + littleEndian?: boolean + ) + getPosition(): number - get byteLength(): number - get buffer(): ArrayBuffer - set buffer(v: ArrayBuffer) - get byteOffset(): number - set byteOffset(v: number) - get dataView(): DataView - set dataView(v: DataView) + get byteLength(): number + get buffer(): ArrayBuffer + set buffer(v: ArrayBuffer) + get byteOffset(): number + set byteOffset(v: number) + get dataView(): DataView + set dataView(v: DataView) - seek(pos: number): void - isEof(): boolean + seek(pos: number): void + isEof(): boolean - mapUint8Array(length: number): Uint8Array - readInt32Array(length: number, littleEndian: boolean): Int32Array - readInt16Array(length: number, littleEndian: boolean): Int16Array - readInt8Array(length: number): Int8Array - readUint32Array(length: number, littleEndian: boolean): Uint32Array - readUint16Array(length: number, littleEndian: boolean): Uint16Array - readUint8Array(length: number): Uint8Array - readFloat64Array(length: number, littleEndian: boolean): Float64Array - readFloat32Array(length: number, littleEndian: boolean): Float32Array + mapUint8Array(length: number): Uint8Array + readInt32Array(length: number, littleEndian: boolean): Int32Array + readInt16Array(length: number, littleEndian: boolean): Int16Array + readInt8Array(length: number): Int8Array + readUint32Array(length: number, littleEndian: boolean): Uint32Array + readUint16Array(length: number, littleEndian: boolean): Uint16Array + readUint8Array(length: number): Uint8Array + readFloat64Array(length: number, littleEndian: boolean): Float64Array + readFloat32Array(length: number, littleEndian: boolean): Float32Array - readInt32(littleEndian: boolean): number - readInt16(littleEndian: boolean): number - readInt8(): number - readUint32(littleEndian: boolean): number - readUint16(littleEndian: boolean): number - readUint8(): number - readFloat32(littleEndian: boolean): number - readFloat64(littleEndian: boolean): number + readInt32(littleEndian: boolean): number + readInt16(littleEndian: boolean): number + readInt8(): number + readUint32(littleEndian: boolean): number + readUint16(littleEndian: boolean): number + readUint8(): number + readFloat32(littleEndian: boolean): number + readFloat64(littleEndian: boolean): number - endianness: boolean + endianness: boolean - memcpy( - dst: ArrayBufferLike, - dstOffset: number, - src: ArrayBufferLike, - srcOffset: number, - byteLength: number - ): void + memcpy( + dst: ArrayBufferLike, + dstOffset: number, + src: ArrayBufferLike, + srcOffset: number, + byteLength: number + ): void - // TODO I got bored porting the remaining functions - } + // TODO I got bored porting the remaining functions + } - export class Box { - write(stream: DataStream): void - } + export class Box { + write(stream: DataStream): void + } - export interface TrackOptions { - id?: number - type?: string - width?: number - height?: number - duration?: number - layer?: number - timescale?: number - media_duration?: number - language?: string - hdlr?: string + export interface TrackOptions { + id?: number + type?: string + width?: number + height?: number + duration?: number + layer?: number + timescale?: number + media_duration?: number + language?: string + hdlr?: string - // video - avcDecoderConfigRecord?: any + // video + avcDecoderConfigRecord?: any - // audio - balance?: number - channel_count?: number - samplesize?: number - samplerate?: number + // audio + balance?: number + channel_count?: number + samplesize?: number + samplerate?: number - //captions - namespace?: string - schema_location?: string - auxiliary_mime_types?: string + //captions + namespace?: string + schema_location?: string + auxiliary_mime_types?: string - description?: any - description_boxes?: Box[] + description?: any + description_boxes?: Box[] - default_sample_description_index_id?: number - default_sample_duration?: number - default_sample_size?: number - default_sample_flags?: number - } + default_sample_description_index_id?: number + default_sample_duration?: number + default_sample_size?: number + default_sample_flags?: number + } - export interface FileOptions { - brands?: string[] - timescale?: number - rate?: number - duration?: number - width?: number - } + export interface FileOptions { + brands?: string[] + timescale?: number + rate?: number + duration?: number + width?: number + } - export interface SampleOptions { - sample_description_index?: number - duration?: number - cts?: number - dts?: number - is_sync?: boolean - is_leading?: number - depends_on?: number - is_depended_on?: number - has_redundancy?: number - degradation_priority?: number - subsamples?: any - } + export interface SampleOptions { + sample_description_index?: number + duration?: number + cts?: number + dts?: number + is_sync?: boolean + is_leading?: number + depends_on?: number + is_depended_on?: number + has_redundancy?: number + degradation_priority?: number + subsamples?: any + } - // TODO add the remaining functions - // TODO move to another module - export class ISOFile { - constructor(stream?: DataStream) + // TODO add the remaining functions + // TODO move to another module + export class ISOFile { + constructor(stream?: DataStream) - init(options?: FileOptions): ISOFile - addTrack(options?: TrackOptions): number - addSample( - track: number, - data: ArrayBuffer, - options?: SampleOptions - ): Sample + init(options?: FileOptions): ISOFile + addTrack(options?: TrackOptions): number + addSample(track: number, data: ArrayBuffer, options?: SampleOptions): Sample - createSingleSampleMoof(sample: Sample): Box + createSingleSampleMoof(sample: Sample): Box - // helpers - getTrackById(id: number): Box | undefined - getTrexById(id: number): Box | undefined - } + // helpers + getTrackById(id: number): Box | undefined + getTrexById(id: number): Box | undefined + } - export {} + export {} } diff --git a/web/src/player/audio.ts b/web/src/player/audio.ts index 30e8a7e..50309f1 100644 --- a/web/src/player/audio.ts +++ b/web/src/player/audio.ts @@ -2,81 +2,81 @@ import * as Message from "./message" import { Ring } from "./ring" export default class Audio { - ring?: Ring - queue: Array + ring?: Ring + queue: Array - render?: number // non-zero if requestAnimationFrame has been called - last?: number // the timestamp of the last rendered frame, in microseconds + render?: number // non-zero if requestAnimationFrame has been called + last?: number // the timestamp of the last rendered frame, in microseconds - constructor(_config: Message.Config) { - this.queue = [] - } + constructor(_config: Message.Config) { + this.queue = [] + } - push(frame: AudioData) { - // Drop any old frames - if (this.last && frame.timestamp <= this.last) { - frame.close() - return - } + push(frame: AudioData) { + // Drop any old frames + if (this.last && frame.timestamp <= this.last) { + frame.close() + return + } - // Insert the frame into the queue sorted by timestamp. - if ( - this.queue.length > 0 && - this.queue[this.queue.length - 1].timestamp <= frame.timestamp - ) { - // Fast path because we normally append to the end. - this.queue.push(frame) - } else { - // Do a full binary search - let low = 0 - let high = this.queue.length + // Insert the frame into the queue sorted by timestamp. + if ( + this.queue.length > 0 && + this.queue[this.queue.length - 1].timestamp <= frame.timestamp + ) { + // Fast path because we normally append to the end. + this.queue.push(frame) + } else { + // Do a full binary search + let low = 0 + let high = this.queue.length - while (low < high) { - const mid = (low + high) >>> 1 - if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1 - else high = mid - } + while (low < high) { + const mid = (low + high) >>> 1 + if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1 + else high = mid + } - this.queue.splice(low, 0, frame) - } + this.queue.splice(low, 0, frame) + } - this.emit() - } + this.emit() + } - emit() { - const ring = this.ring - if (!ring) { - return - } + emit() { + const ring = this.ring + if (!ring) { + return + } - while (this.queue.length) { - const frame = this.queue[0] - if (ring.size() + frame.numberOfFrames > ring.capacity) { - // Buffer is full - break - } + while (this.queue.length) { + const frame = this.queue[0] + if (ring.size() + frame.numberOfFrames > ring.capacity) { + // Buffer is full + break + } - const size = ring.write(frame) - if (size < frame.numberOfFrames) { - throw new Error("audio buffer is full") - } + const size = ring.write(frame) + if (size < frame.numberOfFrames) { + throw new Error("audio buffer is full") + } - this.last = frame.timestamp + this.last = frame.timestamp - frame.close() - this.queue.shift() - } - } + frame.close() + this.queue.shift() + } + } - play(play: Message.Play) { - this.ring = new Ring(play.buffer) + play(play: Message.Play) { + this.ring = new Ring(play.buffer) - if (!this.render) { - const sampleRate = 44100 // TODO dynamic + if (!this.render) { + const sampleRate = 44100 // TODO dynamic - // Refresh every half buffer - const refresh = ((play.buffer.capacity / sampleRate) * 1000) / 2 - this.render = setInterval(this.emit.bind(this), refresh) - } - } + // Refresh every half buffer + const refresh = ((play.buffer.capacity / sampleRate) * 1000) / 2 + this.render = setInterval(this.emit.bind(this), refresh) + } + } } diff --git a/web/src/player/decoder.ts b/web/src/player/decoder.ts index 676b5f6..56bfba7 100644 --- a/web/src/player/decoder.ts +++ b/web/src/player/decoder.ts @@ -5,179 +5,175 @@ import * as Stream from "../stream" import Renderer from "./renderer" export default class Decoder { - init: MP4.InitParser - decoders: Map - renderer: Renderer + init: MP4.InitParser + decoders: Map + renderer: Renderer - constructor(renderer: Renderer) { - this.init = new MP4.InitParser() - this.decoders = new Map() - this.renderer = renderer - } + constructor(renderer: Renderer) { + this.init = new MP4.InitParser() + this.decoders = new Map() + this.renderer = renderer + } - async receiveInit(msg: Message.Init) { - const stream = new Stream.Reader(msg.reader, msg.buffer) - for (;;) { - const data = await stream.read() - if (!data) break + async receiveInit(msg: Message.Init) { + const stream = new Stream.Reader(msg.reader, msg.buffer) + for (;;) { + const data = await stream.read() + if (!data) break - this.init.push(data) - } + this.init.push(data) + } - // TODO make sure the init segment is fully received - } + // TODO make sure the init segment is fully received + } - async receiveSegment(msg: Message.Segment) { - // Wait for the init segment to be fully received and parsed - const init = await this.init.info - const input = MP4.New() + async receiveSegment(msg: Message.Segment) { + // Wait for the init segment to be fully received and parsed + const init = await this.init.info + const input = MP4.New() - input.onSamples = this.onSamples.bind(this) - input.onReady = (track: any) => { - // Extract all of the tracks, because we don't know if it's audio or video. - for (const i of init.tracks) { - input.setExtractionOptions(track.id, i, { nbSamples: 1 }) - } + input.onSamples = this.onSamples.bind(this) + input.onReady = (track: any) => { + // Extract all of the tracks, because we don't know if it's audio or video. + for (const i of init.tracks) { + input.setExtractionOptions(track.id, i, { nbSamples: 1 }) + } - input.start() - } + input.start() + } - // MP4box requires us to reparse the init segment unfortunately - let offset = 0 + // MP4box requires us to reparse the init segment unfortunately + let offset = 0 - for (const raw of this.init.raw) { - raw.fileStart = offset - offset = input.appendBuffer(raw) - } + for (const raw of this.init.raw) { + raw.fileStart = offset + offset = input.appendBuffer(raw) + } - const stream = new Stream.Reader(msg.reader, msg.buffer) + const stream = new Stream.Reader(msg.reader, msg.buffer) - // For whatever reason, mp4box doesn't work until you read an atom at a time. - while (!(await stream.done())) { - const raw = await stream.peek(4) + // For whatever reason, mp4box doesn't work until you read an atom at a time. + while (!(await stream.done())) { + const raw = await stream.peek(4) - // TODO this doesn't support when size = 0 (until EOF) or size = 1 (extended size) - const size = new DataView( - raw.buffer, - raw.byteOffset, - raw.byteLength - ).getUint32(0) - const atom = await stream.bytes(size) + // TODO this doesn't support when size = 0 (until EOF) or size = 1 (extended size) + const size = new DataView( + raw.buffer, + raw.byteOffset, + raw.byteLength + ).getUint32(0) + const atom = await stream.bytes(size) - // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately - const box = new Uint8Array(atom.byteLength) - box.set(atom) + // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately + const box = new Uint8Array(atom.byteLength) + box.set(atom) - // and for some reason we need to modify the underlying ArrayBuffer with offset - const buffer = box.buffer as MP4.ArrayBuffer - buffer.fileStart = offset + // and for some reason we need to modify the underlying ArrayBuffer with offset + const buffer = box.buffer as MP4.ArrayBuffer + buffer.fileStart = offset - // Parse the data - offset = input.appendBuffer(buffer) - input.flush() - } - } + // Parse the data + offset = input.appendBuffer(buffer) + input.flush() + } + } - onSamples(track_id: number, track: MP4.Track, samples: MP4.Sample[]) { - let decoder = this.decoders.get(track_id) + onSamples(track_id: number, track: MP4.Track, samples: MP4.Sample[]) { + let decoder = this.decoders.get(track_id) - if (!decoder) { - // We need a sample to initalize the video decoder, because of mp4box limitations. - const sample = samples[0] + if (!decoder) { + // We need a sample to initalize the video decoder, because of mp4box limitations. + const sample = samples[0] - if (isVideoTrack(track)) { - // Configure the decoder using the AVC box for H.264 - // TODO it should be easy to support other codecs, just need to know the right boxes. - const avcc = sample.description.avcC - if (!avcc) throw new Error("TODO only h264 is supported") + if (isVideoTrack(track)) { + // Configure the decoder using the AVC box for H.264 + // TODO it should be easy to support other codecs, just need to know the right boxes. + const avcc = sample.description.avcC + if (!avcc) throw new Error("TODO only h264 is supported") - const description = new MP4.Stream( - new Uint8Array(avcc.size), - 0, - false - ) - avcc.write(description) + const description = new MP4.Stream(new Uint8Array(avcc.size), 0, false) + avcc.write(description) - const videoDecoder = new VideoDecoder({ - output: this.renderer.push.bind(this.renderer), - error: console.warn, - }) + const videoDecoder = new VideoDecoder({ + output: this.renderer.push.bind(this.renderer), + error: console.warn, + }) - videoDecoder.configure({ - codec: track.codec, - codedHeight: track.video.height, - codedWidth: track.video.width, - description: description.buffer?.slice(8), - // optimizeForLatency: true - }) + videoDecoder.configure({ + codec: track.codec, + codedHeight: track.video.height, + codedWidth: track.video.width, + description: description.buffer?.slice(8), + // optimizeForLatency: true + }) - decoder = videoDecoder - } else if (isAudioTrack(track)) { - const audioDecoder = new AudioDecoder({ - output: this.renderer.push.bind(this.renderer), - error: console.warn, - }) + decoder = videoDecoder + } else if (isAudioTrack(track)) { + const audioDecoder = new AudioDecoder({ + output: this.renderer.push.bind(this.renderer), + error: console.warn, + }) - audioDecoder.configure({ - codec: track.codec, - numberOfChannels: track.audio.channel_count, - sampleRate: track.audio.sample_rate, - }) + audioDecoder.configure({ + codec: track.codec, + numberOfChannels: track.audio.channel_count, + sampleRate: track.audio.sample_rate, + }) - decoder = audioDecoder - } else { - throw new Error("unknown track type") - } + decoder = audioDecoder + } else { + throw new Error("unknown track type") + } - this.decoders.set(track_id, decoder) - } + this.decoders.set(track_id, decoder) + } - for (const sample of samples) { - // Convert to microseconds - const timestamp = (1000 * 1000 * sample.dts) / sample.timescale - const duration = (1000 * 1000 * sample.duration) / sample.timescale + for (const sample of samples) { + // Convert to microseconds + const timestamp = (1000 * 1000 * sample.dts) / sample.timescale + const duration = (1000 * 1000 * sample.duration) / sample.timescale - if (isAudioDecoder(decoder)) { - decoder.decode( - new EncodedAudioChunk({ - type: sample.is_sync ? "key" : "delta", - data: sample.data, - duration: duration, - timestamp: timestamp, - }) - ) - } else if (isVideoDecoder(decoder)) { - decoder.decode( - new EncodedVideoChunk({ - type: sample.is_sync ? "key" : "delta", - data: sample.data, - duration: duration, - timestamp: timestamp, - }) - ) - } else { - throw new Error("unknown decoder type") - } - } - } + if (isAudioDecoder(decoder)) { + decoder.decode( + new EncodedAudioChunk({ + type: sample.is_sync ? "key" : "delta", + data: sample.data, + duration: duration, + timestamp: timestamp, + }) + ) + } else if (isVideoDecoder(decoder)) { + decoder.decode( + new EncodedVideoChunk({ + type: sample.is_sync ? "key" : "delta", + data: sample.data, + duration: duration, + timestamp: timestamp, + }) + ) + } else { + throw new Error("unknown decoder type") + } + } + } } function isAudioDecoder( - decoder: AudioDecoder | VideoDecoder + decoder: AudioDecoder | VideoDecoder ): decoder is AudioDecoder { - return decoder instanceof AudioDecoder + return decoder instanceof AudioDecoder } function isVideoDecoder( - decoder: AudioDecoder | VideoDecoder + decoder: AudioDecoder | VideoDecoder ): decoder is VideoDecoder { - return decoder instanceof VideoDecoder + return decoder instanceof VideoDecoder } function isAudioTrack(track: MP4.Track): track is MP4.AudioTrack { - return (track as MP4.AudioTrack).audio !== undefined + return (track as MP4.AudioTrack).audio !== undefined } function isVideoTrack(track: MP4.Track): track is MP4.VideoTrack { - return (track as MP4.VideoTrack).video !== undefined + return (track as MP4.VideoTrack).video !== undefined } diff --git a/web/src/player/index.ts b/web/src/player/index.ts index 9b17c22..00c476f 100644 --- a/web/src/player/index.ts +++ b/web/src/player/index.ts @@ -3,89 +3,89 @@ import * as Ring from "./ring" import Transport from "../transport" export interface Config { - transport: Transport - canvas: OffscreenCanvas + transport: Transport + canvas: OffscreenCanvas } // This class must be created on the main thread due to AudioContext. export default class Player { - context: AudioContext - worker: Worker - worklet: Promise + context: AudioContext + worker: Worker + worklet: Promise - transport: Transport + transport: Transport - constructor(config: Config) { - this.transport = config.transport - this.transport.callback = this + constructor(config: Config) { + this.transport = config.transport + this.transport.callback = this - this.context = new AudioContext({ - latencyHint: "interactive", - sampleRate: 44100, - }) + this.context = new AudioContext({ + latencyHint: "interactive", + sampleRate: 44100, + }) - this.worker = this.setupWorker(config) - this.worklet = this.setupWorklet(config) - } + this.worker = this.setupWorker(config) + this.worklet = this.setupWorklet(config) + } - private setupWorker(config: Config): Worker { - const url = new URL("worker.ts", import.meta.url) + private setupWorker(config: Config): Worker { + const url = new URL("worker.ts", import.meta.url) - const worker = new Worker(url, { - type: "module", - name: "media", - }) + const worker = new Worker(url, { + type: "module", + name: "media", + }) - const msg = { - canvas: config.canvas, - } + const msg = { + canvas: config.canvas, + } - worker.postMessage({ config: msg }, [msg.canvas]) + worker.postMessage({ config: msg }, [msg.canvas]) - return worker - } + return worker + } - private async setupWorklet(_config: Config): Promise { - // Load the worklet source code. - const url = new URL("worklet.ts", import.meta.url) - await this.context.audioWorklet.addModule(url) + private async setupWorklet(_config: Config): Promise { + // Load the worklet source code. + const url = new URL("worklet.ts", import.meta.url) + await this.context.audioWorklet.addModule(url) - const volume = this.context.createGain() - volume.gain.value = 2.0 + const volume = this.context.createGain() + volume.gain.value = 2.0 - // Create a worklet - const worklet = new AudioWorkletNode(this.context, "renderer") - worklet.onprocessorerror = (e: Event) => { - console.error("Audio worklet error:", e) - } + // Create a worklet + const worklet = new AudioWorkletNode(this.context, "renderer") + worklet.onprocessorerror = (e: Event) => { + console.error("Audio worklet error:", e) + } - // Connect the worklet to the volume node and then to the speakers - worklet.connect(volume) - volume.connect(this.context.destination) + // Connect the worklet to the volume node and then to the speakers + worklet.connect(volume) + volume.connect(this.context.destination) - return worklet - } + return worklet + } - onInit(init: Message.Init) { - this.worker.postMessage({ init }, [init.buffer.buffer, init.reader]) - } + onInit(init: Message.Init) { + this.worker.postMessage({ init }, [init.buffer.buffer, init.reader]) + } - onSegment(segment: Message.Segment) { - this.worker.postMessage({ segment }, [ - segment.buffer.buffer, - segment.reader, - ]) - } + onSegment(segment: Message.Segment) { + this.worker.postMessage({ segment }, [ + segment.buffer.buffer, + segment.reader, + ]) + } - async play() { - this.context.resume() + async play() { + this.context.resume() - const play = { - buffer: new Ring.Buffer(2, 44100 / 10), // 100ms of audio - } + const play = { + buffer: new Ring.Buffer(2, 44100 / 10), // 100ms of audio + } - const worklet = await this.worklet - worklet.port.postMessage({ play }) - this.worker.postMessage({ play }) - } + const worklet = await this.worklet + worklet.port.postMessage({ play }) + this.worker.postMessage({ play }) + } } diff --git a/web/src/player/message.ts b/web/src/player/message.ts index 2a82acc..3ed3993 100644 --- a/web/src/player/message.ts +++ b/web/src/player/message.ts @@ -1,21 +1,21 @@ import * as Ring from "./ring" export interface Config { - // video stuff - canvas: OffscreenCanvas + // video stuff + canvas: OffscreenCanvas } export interface Init { - buffer: Uint8Array // unread buffered data - reader: ReadableStream // unread unbuffered data + buffer: Uint8Array // unread buffered data + reader: ReadableStream // unread unbuffered data } export interface Segment { - buffer: Uint8Array // unread buffered data - reader: ReadableStream // unread unbuffered data + buffer: Uint8Array // unread buffered data + reader: ReadableStream // unread unbuffered data } export interface Play { - timestamp?: number - buffer: Ring.Buffer + timestamp?: number + buffer: Ring.Buffer } diff --git a/web/src/player/renderer.ts b/web/src/player/renderer.ts index a74ac68..9b063a1 100644 --- a/web/src/player/renderer.ts +++ b/web/src/player/renderer.ts @@ -3,34 +3,34 @@ import Audio from "./audio" import Video from "./video" export default class Renderer { - audio: Audio - video: Video + audio: Audio + video: Video - constructor(config: Message.Config) { - this.audio = new Audio(config) - this.video = new Video(config) - } + constructor(config: Message.Config) { + this.audio = new Audio(config) + this.video = new Video(config) + } - push(frame: AudioData | VideoFrame) { - if (isAudioData(frame)) { - this.audio.push(frame) - } else if (isVideoFrame(frame)) { - this.video.push(frame) - } else { - throw new Error("unknown frame type") - } - } + push(frame: AudioData | VideoFrame) { + if (isAudioData(frame)) { + this.audio.push(frame) + } else if (isVideoFrame(frame)) { + this.video.push(frame) + } else { + throw new Error("unknown frame type") + } + } - play(play: Message.Play) { - this.audio.play(play) - this.video.play(play) - } + play(play: Message.Play) { + this.audio.play(play) + this.video.play(play) + } } function isAudioData(frame: AudioData | VideoFrame): frame is AudioData { - return frame instanceof AudioData + return frame instanceof AudioData } function isVideoFrame(frame: AudioData | VideoFrame): frame is VideoFrame { - return frame instanceof VideoFrame + return frame instanceof VideoFrame } diff --git a/web/src/player/ring.ts b/web/src/player/ring.ts index 434f9d3..bb28b0d 100644 --- a/web/src/player/ring.ts +++ b/web/src/player/ring.ts @@ -1,159 +1,159 @@ // Ring buffer with audio samples. enum STATE { - READ_POS = 0, // The current read position - WRITE_POS, // The current write position - LENGTH, // Clever way of saving the total number of enums values. + READ_POS = 0, // The current read position + WRITE_POS, // The current write position + LENGTH, // Clever way of saving the total number of enums values. } // No prototype to make this easier to send via postMessage export class Buffer { - state: SharedArrayBuffer + state: SharedArrayBuffer - channels: SharedArrayBuffer[] - capacity: number + channels: SharedArrayBuffer[] + capacity: number - constructor(channels: number, capacity: number) { - // Store the current state in a separate ring buffer. - this.state = new SharedArrayBuffer( - STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT - ) + constructor(channels: number, capacity: number) { + // Store the current state in a separate ring buffer. + this.state = new SharedArrayBuffer( + STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT + ) - // Create a buffer for each audio channel - this.channels = [] - for (let i = 0; i < channels; i += 1) { - const buffer = new SharedArrayBuffer( - capacity * Float32Array.BYTES_PER_ELEMENT - ) - this.channels.push(buffer) - } + // Create a buffer for each audio channel + this.channels = [] + for (let i = 0; i < channels; i += 1) { + const buffer = new SharedArrayBuffer( + capacity * Float32Array.BYTES_PER_ELEMENT + ) + this.channels.push(buffer) + } - this.capacity = capacity - } + this.capacity = capacity + } } export class Ring { - state: Int32Array - channels: Float32Array[] - capacity: number + state: Int32Array + channels: Float32Array[] + capacity: number - constructor(buffer: Buffer) { - this.state = new Int32Array(buffer.state) + constructor(buffer: Buffer) { + this.state = new Int32Array(buffer.state) - this.channels = [] - for (const channel of buffer.channels) { - this.channels.push(new Float32Array(channel)) - } + this.channels = [] + for (const channel of buffer.channels) { + this.channels.push(new Float32Array(channel)) + } - this.capacity = buffer.capacity - } + this.capacity = buffer.capacity + } - // Write samples for single audio frame, returning the total number written. - write(frame: AudioData): number { - const readPos = Atomics.load(this.state, STATE.READ_POS) - const writePos = Atomics.load(this.state, STATE.WRITE_POS) + // Write samples for single audio frame, returning the total number written. + write(frame: AudioData): number { + const readPos = Atomics.load(this.state, STATE.READ_POS) + const writePos = Atomics.load(this.state, STATE.WRITE_POS) - const startPos = writePos - let endPos = writePos + frame.numberOfFrames + const startPos = writePos + let endPos = writePos + frame.numberOfFrames - if (endPos > readPos + this.capacity) { - endPos = readPos + this.capacity - if (endPos <= startPos) { - // No space to write - return 0 - } - } + if (endPos > readPos + this.capacity) { + endPos = readPos + this.capacity + if (endPos <= startPos) { + // No space to write + return 0 + } + } - const startIndex = startPos % this.capacity - const endIndex = endPos % this.capacity + const startIndex = startPos % this.capacity + const endIndex = endPos % this.capacity - // Loop over each channel - for (let i = 0; i < this.channels.length; i += 1) { - const channel = this.channels[i] + // Loop over each channel + for (let i = 0; i < this.channels.length; i += 1) { + const channel = this.channels[i] - if (startIndex < endIndex) { - // One continuous range to copy. - const full = channel.subarray(startIndex, endIndex) + if (startIndex < endIndex) { + // One continuous range to copy. + const full = channel.subarray(startIndex, endIndex) - frame.copyTo(full, { - planeIndex: i, - frameCount: endIndex - startIndex, - }) - } else { - const first = channel.subarray(startIndex) - const second = channel.subarray(0, endIndex) + frame.copyTo(full, { + planeIndex: i, + frameCount: endIndex - startIndex, + }) + } else { + const first = channel.subarray(startIndex) + const second = channel.subarray(0, endIndex) - frame.copyTo(first, { - planeIndex: i, - frameCount: first.length, - }) + frame.copyTo(first, { + planeIndex: i, + frameCount: first.length, + }) - // We need this conditional when startIndex == 0 and endIndex == 0 - // When capacity=4410 and frameCount=1024, this was happening 52s into the audio. - if (second.length) { - frame.copyTo(second, { - planeIndex: i, - frameOffset: first.length, - frameCount: second.length, - }) - } - } - } + // We need this conditional when startIndex == 0 and endIndex == 0 + // When capacity=4410 and frameCount=1024, this was happening 52s into the audio. + if (second.length) { + frame.copyTo(second, { + planeIndex: i, + frameOffset: first.length, + frameCount: second.length, + }) + } + } + } - Atomics.store(this.state, STATE.WRITE_POS, endPos) + Atomics.store(this.state, STATE.WRITE_POS, endPos) - return endPos - startPos - } + return endPos - startPos + } - read(dst: Float32Array[]): number { - const readPos = Atomics.load(this.state, STATE.READ_POS) - const writePos = Atomics.load(this.state, STATE.WRITE_POS) + read(dst: Float32Array[]): number { + const readPos = Atomics.load(this.state, STATE.READ_POS) + const writePos = Atomics.load(this.state, STATE.WRITE_POS) - const startPos = readPos - let endPos = startPos + dst[0].length + const startPos = readPos + let endPos = startPos + dst[0].length - if (endPos > writePos) { - endPos = writePos - if (endPos <= startPos) { - // Nothing to read - return 0 - } - } + if (endPos > writePos) { + endPos = writePos + if (endPos <= startPos) { + // Nothing to read + return 0 + } + } - const startIndex = startPos % this.capacity - const endIndex = endPos % this.capacity + const startIndex = startPos % this.capacity + const endIndex = endPos % this.capacity - // Loop over each channel - for (let i = 0; i < dst.length; i += 1) { - if (i >= this.channels.length) { - // ignore excess channels - } + // Loop over each channel + for (let i = 0; i < dst.length; i += 1) { + if (i >= this.channels.length) { + // ignore excess channels + } - const input = this.channels[i] - const output = dst[i] + const input = this.channels[i] + const output = dst[i] - if (startIndex < endIndex) { - const full = input.subarray(startIndex, endIndex) - output.set(full) - } else { - const first = input.subarray(startIndex) - const second = input.subarray(0, endIndex) + if (startIndex < endIndex) { + const full = input.subarray(startIndex, endIndex) + output.set(full) + } else { + const first = input.subarray(startIndex) + const second = input.subarray(0, endIndex) - output.set(first) - output.set(second, first.length) - } - } + output.set(first) + output.set(second, first.length) + } + } - Atomics.store(this.state, STATE.READ_POS, endPos) + Atomics.store(this.state, STATE.READ_POS, endPos) - return endPos - startPos - } + return endPos - startPos + } - size() { - // TODO is this thread safe? - const readPos = Atomics.load(this.state, STATE.READ_POS) - const writePos = Atomics.load(this.state, STATE.WRITE_POS) + size() { + // TODO is this thread safe? + const readPos = Atomics.load(this.state, STATE.READ_POS) + const writePos = Atomics.load(this.state, STATE.WRITE_POS) - return writePos - readPos - } + return writePos - readPos + } } diff --git a/web/src/player/video.ts b/web/src/player/video.ts index 09219e8..b118b3b 100644 --- a/web/src/player/video.ts +++ b/web/src/player/video.ts @@ -1,101 +1,101 @@ import * as Message from "./message" export default class Video { - canvas: OffscreenCanvas - queue: Array + canvas: OffscreenCanvas + queue: Array - render: number // non-zero if requestAnimationFrame has been called - sync?: number // the wall clock value for timestamp 0, in microseconds - last?: number // the timestamp of the last rendered frame, in microseconds + render: number // non-zero if requestAnimationFrame has been called + sync?: number // the wall clock value for timestamp 0, in microseconds + last?: number // the timestamp of the last rendered frame, in microseconds - constructor(config: Message.Config) { - this.canvas = config.canvas - this.queue = [] + constructor(config: Message.Config) { + this.canvas = config.canvas + this.queue = [] - this.render = 0 - } + this.render = 0 + } - push(frame: VideoFrame) { - // Drop any old frames - if (this.last && frame.timestamp <= this.last) { - frame.close() - return - } + push(frame: VideoFrame) { + // Drop any old frames + if (this.last && frame.timestamp <= this.last) { + frame.close() + return + } - // Insert the frame into the queue sorted by timestamp. - if ( - this.queue.length > 0 && - this.queue[this.queue.length - 1].timestamp <= frame.timestamp - ) { - // Fast path because we normally append to the end. - this.queue.push(frame) - } else { - // Do a full binary search - let low = 0 - let high = this.queue.length + // Insert the frame into the queue sorted by timestamp. + if ( + this.queue.length > 0 && + this.queue[this.queue.length - 1].timestamp <= frame.timestamp + ) { + // Fast path because we normally append to the end. + this.queue.push(frame) + } else { + // Do a full binary search + let low = 0 + let high = this.queue.length - while (low < high) { - const mid = (low + high) >>> 1 - if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1 - else high = mid - } + while (low < high) { + const mid = (low + high) >>> 1 + if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1 + else high = mid + } - this.queue.splice(low, 0, frame) - } - } + this.queue.splice(low, 0, frame) + } + } - draw(now: number) { - // Draw and then queue up the next draw call. - this.drawOnce(now) + draw(now: number) { + // Draw and then queue up the next draw call. + this.drawOnce(now) - // Queue up the new draw frame. - this.render = self.requestAnimationFrame(this.draw.bind(this)) - } + // Queue up the new draw frame. + this.render = self.requestAnimationFrame(this.draw.bind(this)) + } - drawOnce(now: number) { - // Convert to microseconds - now *= 1000 + drawOnce(now: number) { + // Convert to microseconds + now *= 1000 - if (!this.queue.length) { - return - } + if (!this.queue.length) { + return + } - let frame = this.queue[0] + let frame = this.queue[0] - if (!this.sync) { - this.sync = now - frame.timestamp - } + if (!this.sync) { + this.sync = now - frame.timestamp + } - // Determine the target timestamp. - const target = now - this.sync + // Determine the target timestamp. + const target = now - this.sync - if (frame.timestamp >= target) { - // nothing to render yet, wait for the next animation frame - return - } + if (frame.timestamp >= target) { + // nothing to render yet, wait for the next animation frame + return + } - this.queue.shift() + this.queue.shift() - // Check if we should skip some frames - while (this.queue.length) { - const next = this.queue[0] - if (next.timestamp > target) break + // Check if we should skip some frames + while (this.queue.length) { + const next = this.queue[0] + if (next.timestamp > target) break - frame.close() - frame = this.queue.shift()! - } + frame.close() + frame = this.queue.shift()! + } - const ctx = this.canvas.getContext("2d") - ctx!.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height) // TODO aspect ratio + const ctx = this.canvas.getContext("2d") + ctx!.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height) // TODO aspect ratio - this.last = frame.timestamp - frame.close() - } + this.last = frame.timestamp + frame.close() + } - play(_play: Message.Play) { - // Queue up to render the next frame. - if (!this.render) { - this.render = self.requestAnimationFrame(this.draw.bind(this)) - } - } + play(_play: Message.Play) { + // Queue up to render the next frame. + if (!this.render) { + this.render = self.requestAnimationFrame(this.draw.bind(this)) + } + } } diff --git a/web/src/player/worker.ts b/web/src/player/worker.ts index f30dd22..064889d 100644 --- a/web/src/player/worker.ts +++ b/web/src/player/worker.ts @@ -6,19 +6,19 @@ let decoder: Decoder let renderer: Renderer self.addEventListener("message", async (e: MessageEvent) => { - if (e.data.config) { - const config = e.data.config as Message.Config + if (e.data.config) { + const config = e.data.config as Message.Config - renderer = new Renderer(config) - decoder = new Decoder(renderer) - } else if (e.data.init) { - const init = e.data.init as Message.Init - await decoder.receiveInit(init) - } else if (e.data.segment) { - const segment = e.data.segment as Message.Segment - await decoder.receiveSegment(segment) - } else if (e.data.play) { - const play = e.data.play as Message.Play - await renderer.play(play) - } + renderer = new Renderer(config) + decoder = new Decoder(renderer) + } else if (e.data.init) { + const init = e.data.init as Message.Init + await decoder.receiveInit(init) + } else if (e.data.segment) { + const segment = e.data.segment as Message.Segment + await decoder.receiveSegment(segment) + } else if (e.data.play) { + const play = e.data.play as Message.Play + await renderer.play(play) + } }) diff --git a/web/src/player/worklet.ts b/web/src/player/worklet.ts index 2adb6db..9f4f6d3 100644 --- a/web/src/player/worklet.ts +++ b/web/src/player/worklet.ts @@ -7,51 +7,51 @@ import * as Message from "./message" import { Ring } from "./ring" class Renderer extends AudioWorkletProcessor { - ring?: Ring - base: number + ring?: Ring + base: number - constructor(_params: AudioWorkletNodeOptions) { - // The super constructor call is required. - super() + constructor(_params: AudioWorkletNodeOptions) { + // The super constructor call is required. + super() - this.base = 0 - this.port.onmessage = this.onMessage.bind(this) - } + this.base = 0 + this.port.onmessage = this.onMessage.bind(this) + } - onMessage(e: MessageEvent) { - if (e.data.play) { - this.onPlay(e.data.play) - } - } + onMessage(e: MessageEvent) { + if (e.data.play) { + this.onPlay(e.data.play) + } + } - onPlay(play: Message.Play) { - this.ring = new Ring(play.buffer) - } + onPlay(play: Message.Play) { + this.ring = new Ring(play.buffer) + } - // Inputs and outputs in groups of 128 samples. - process( - inputs: Float32Array[][], - outputs: Float32Array[][], - _parameters: Record - ): boolean { - if (!this.ring) { - // Paused - return true - } + // Inputs and outputs in groups of 128 samples. + process( + inputs: Float32Array[][], + outputs: Float32Array[][], + _parameters: Record + ): boolean { + if (!this.ring) { + // Paused + return true + } - if (inputs.length != 1 && outputs.length != 1) { - throw new Error("only a single track is supported") - } + if (inputs.length != 1 && outputs.length != 1) { + throw new Error("only a single track is supported") + } - const output = outputs[0] + const output = outputs[0] - const size = this.ring.read(output) - if (size < output.length) { - // TODO trigger rebuffering event - } + const size = this.ring.read(output) + if (size < output.length) { + // TODO trigger rebuffering event + } - return true - } + return true + } } registerProcessor("renderer", Renderer) diff --git a/web/src/stream/reader.ts b/web/src/stream/reader.ts index 185b170..f2bc0e0 100644 --- a/web/src/stream/reader.ts +++ b/web/src/stream/reader.ts @@ -1,219 +1,210 @@ // Reader wraps a stream and provides convience methods for reading pieces from a stream export default class Reader { - reader: ReadableStream - buffer: Uint8Array + reader: ReadableStream + buffer: Uint8Array - constructor( - reader: ReadableStream, - buffer: Uint8Array = new Uint8Array(0) - ) { - this.reader = reader - this.buffer = buffer - } + constructor(reader: ReadableStream, buffer: Uint8Array = new Uint8Array(0)) { + this.reader = reader + this.buffer = buffer + } - // Returns any number of bytes - async read(): Promise { - if (this.buffer.byteLength) { - const buffer = this.buffer - this.buffer = new Uint8Array() - return buffer - } + // Returns any number of bytes + async read(): Promise { + if (this.buffer.byteLength) { + const buffer = this.buffer + this.buffer = new Uint8Array() + return buffer + } - const r = this.reader.getReader() - const result = await r.read() + const r = this.reader.getReader() + const result = await r.read() - r.releaseLock() + r.releaseLock() - return result.value - } + return result.value + } - async readAll(): Promise { - const r = this.reader.getReader() + async readAll(): Promise { + const r = this.reader.getReader() - for (;;) { - const result = await r.read() - if (result.done) { - break - } + for (;;) { + const result = await r.read() + if (result.done) { + break + } - const buffer = new Uint8Array(result.value) + const buffer = new Uint8Array(result.value) - if (this.buffer.byteLength == 0) { - this.buffer = buffer - } else { - const temp = new Uint8Array( - this.buffer.byteLength + buffer.byteLength - ) - temp.set(this.buffer) - temp.set(buffer, this.buffer.byteLength) - this.buffer = temp - } - } + if (this.buffer.byteLength == 0) { + this.buffer = buffer + } else { + const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) + temp.set(this.buffer) + temp.set(buffer, this.buffer.byteLength) + this.buffer = temp + } + } - const result = this.buffer - this.buffer = new Uint8Array() + const result = this.buffer + this.buffer = new Uint8Array() - r.releaseLock() + r.releaseLock() - return result - } + return result + } - async bytes(size: number): Promise { - const r = this.reader.getReader() + async bytes(size: number): Promise { + const r = this.reader.getReader() - while (this.buffer.byteLength < size) { - const result = await r.read() - if (result.done) { - throw "short buffer" - } + while (this.buffer.byteLength < size) { + const result = await r.read() + if (result.done) { + throw "short buffer" + } - const buffer = new Uint8Array(result.value) + const buffer = new Uint8Array(result.value) - if (this.buffer.byteLength == 0) { - this.buffer = buffer - } else { - const temp = new Uint8Array( - this.buffer.byteLength + buffer.byteLength - ) - temp.set(this.buffer) - temp.set(buffer, this.buffer.byteLength) - this.buffer = temp - } - } + if (this.buffer.byteLength == 0) { + this.buffer = buffer + } else { + const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) + temp.set(this.buffer) + temp.set(buffer, this.buffer.byteLength) + this.buffer = temp + } + } - const result = new Uint8Array( - this.buffer.buffer, - this.buffer.byteOffset, - size - ) - this.buffer = new Uint8Array( - this.buffer.buffer, - this.buffer.byteOffset + size - ) + const result = new Uint8Array( + this.buffer.buffer, + this.buffer.byteOffset, + size + ) + this.buffer = new Uint8Array( + this.buffer.buffer, + this.buffer.byteOffset + size + ) - r.releaseLock() + r.releaseLock() - return result - } + return result + } - async peek(size: number): Promise { - const r = this.reader.getReader() + async peek(size: number): Promise { + const r = this.reader.getReader() - while (this.buffer.byteLength < size) { - const result = await r.read() - if (result.done) { - throw "short buffer" - } + while (this.buffer.byteLength < size) { + const result = await r.read() + if (result.done) { + throw "short buffer" + } - const buffer = new Uint8Array(result.value) + const buffer = new Uint8Array(result.value) - if (this.buffer.byteLength == 0) { - this.buffer = buffer - } else { - const temp = new Uint8Array( - this.buffer.byteLength + buffer.byteLength - ) - temp.set(this.buffer) - temp.set(buffer, this.buffer.byteLength) - this.buffer = temp - } - } + if (this.buffer.byteLength == 0) { + this.buffer = buffer + } else { + const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) + temp.set(this.buffer) + temp.set(buffer, this.buffer.byteLength) + this.buffer = temp + } + } - const result = new Uint8Array( - this.buffer.buffer, - this.buffer.byteOffset, - size - ) + const result = new Uint8Array( + this.buffer.buffer, + this.buffer.byteOffset, + size + ) - r.releaseLock() + r.releaseLock() - return result - } + return result + } - async view(size: number): Promise { - const buf = await this.bytes(size) - return new DataView(buf.buffer, buf.byteOffset, buf.byteLength) - } + async view(size: number): Promise { + const buf = await this.bytes(size) + return new DataView(buf.buffer, buf.byteOffset, buf.byteLength) + } - async uint8(): Promise { - const view = await this.view(1) - return view.getUint8(0) - } + async uint8(): Promise { + const view = await this.view(1) + return view.getUint8(0) + } - async uint16(): Promise { - const view = await this.view(2) - return view.getUint16(0) - } + async uint16(): Promise { + const view = await this.view(2) + return view.getUint16(0) + } - async uint32(): Promise { - const view = await this.view(4) - return view.getUint32(0) - } + async uint32(): Promise { + const view = await this.view(4) + return view.getUint32(0) + } - // Returns a Number using 52-bits, the max Javascript can use for integer math - async uint52(): Promise { - const v = await this.uint64() - if (v > Number.MAX_SAFE_INTEGER) { - throw "overflow" - } + // Returns a Number using 52-bits, the max Javascript can use for integer math + async uint52(): Promise { + const v = await this.uint64() + if (v > Number.MAX_SAFE_INTEGER) { + throw "overflow" + } - return Number(v) - } + return Number(v) + } - // Returns a Number using 52-bits, the max Javascript can use for integer math - async vint52(): Promise { - const v = await this.vint64() - if (v > Number.MAX_SAFE_INTEGER) { - throw "overflow" - } + // Returns a Number using 52-bits, the max Javascript can use for integer math + async vint52(): Promise { + const v = await this.vint64() + if (v > Number.MAX_SAFE_INTEGER) { + throw "overflow" + } - return Number(v) - } + return Number(v) + } - // NOTE: Returns a BigInt instead of a Number - async uint64(): Promise { - const view = await this.view(8) - return view.getBigUint64(0) - } + // NOTE: Returns a BigInt instead of a Number + async uint64(): Promise { + const view = await this.view(8) + return view.getBigUint64(0) + } - // NOTE: Returns a BigInt instead of a Number - async vint64(): Promise { - const peek = await this.peek(1) - const first = new DataView( - peek.buffer, - peek.byteOffset, - peek.byteLength - ).getUint8(0) - const size = (first & 0xc0) >> 6 + // NOTE: Returns a BigInt instead of a Number + async vint64(): Promise { + const peek = await this.peek(1) + const first = new DataView( + peek.buffer, + peek.byteOffset, + peek.byteLength + ).getUint8(0) + const size = (first & 0xc0) >> 6 - switch (size) { - case 0: { - const v = await this.uint8() - return BigInt(v) & 0x3fn - } - case 1: { - const v = await this.uint16() - return BigInt(v) & 0x3fffn - } - case 2: { - const v = await this.uint32() - return BigInt(v) & 0x3fffffffn - } - case 3: { - const v = await this.uint64() - return v & 0x3fffffffffffffffn - } - default: - throw "impossible" - } - } + switch (size) { + case 0: { + const v = await this.uint8() + return BigInt(v) & 0x3fn + } + case 1: { + const v = await this.uint16() + return BigInt(v) & 0x3fffn + } + case 2: { + const v = await this.uint32() + return BigInt(v) & 0x3fffffffn + } + case 3: { + const v = await this.uint64() + return v & 0x3fffffffffffffffn + } + default: + throw "impossible" + } + } - async done(): Promise { - try { - await this.peek(1) - return false - } catch (err) { - return true // Assume EOF - } - } + async done(): Promise { + try { + await this.peek(1) + return false + } catch (err) { + return true // Assume EOF + } + } } diff --git a/web/src/stream/writer.ts b/web/src/stream/writer.ts index 0210051..21b3054 100644 --- a/web/src/stream/writer.ts +++ b/web/src/stream/writer.ts @@ -1,100 +1,100 @@ // Writer wraps a stream and writes chunks of data export default class Writer { - buffer: ArrayBuffer - writer: WritableStreamDefaultWriter + buffer: ArrayBuffer + writer: WritableStreamDefaultWriter - constructor(stream: WritableStream) { - this.buffer = new ArrayBuffer(8) - this.writer = stream.getWriter() - } + constructor(stream: WritableStream) { + this.buffer = new ArrayBuffer(8) + this.writer = stream.getWriter() + } - release() { - this.writer.releaseLock() - } + release() { + this.writer.releaseLock() + } - async close() { - return this.writer.close() - } + async close() { + return this.writer.close() + } - async uint8(v: number) { - const view = new DataView(this.buffer, 0, 1) - view.setUint8(0, v) - return this.writer.write(view) - } + async uint8(v: number) { + const view = new DataView(this.buffer, 0, 1) + view.setUint8(0, v) + return this.writer.write(view) + } - async uint16(v: number) { - const view = new DataView(this.buffer, 0, 2) - view.setUint16(0, v) - return this.writer.write(view) - } + async uint16(v: number) { + const view = new DataView(this.buffer, 0, 2) + view.setUint16(0, v) + return this.writer.write(view) + } - async uint24(v: number) { - const v1 = (v >> 16) & 0xff - const v2 = (v >> 8) & 0xff - const v3 = v & 0xff + async uint24(v: number) { + const v1 = (v >> 16) & 0xff + const v2 = (v >> 8) & 0xff + const v3 = v & 0xff - const view = new DataView(this.buffer, 0, 3) - view.setUint8(0, v1) - view.setUint8(1, v2) - view.setUint8(2, v3) + const view = new DataView(this.buffer, 0, 3) + view.setUint8(0, v1) + view.setUint8(1, v2) + view.setUint8(2, v3) - return this.writer.write(view) - } + return this.writer.write(view) + } - async uint32(v: number) { - const view = new DataView(this.buffer, 0, 4) - view.setUint32(0, v) - return this.writer.write(view) - } + async uint32(v: number) { + const view = new DataView(this.buffer, 0, 4) + view.setUint32(0, v) + return this.writer.write(view) + } - async uint52(v: number) { - if (v > Number.MAX_SAFE_INTEGER) { - throw "value too large" - } + async uint52(v: number) { + if (v > Number.MAX_SAFE_INTEGER) { + throw "value too large" + } - this.uint64(BigInt(v)) - } + this.uint64(BigInt(v)) + } - async vint52(v: number) { - if (v > Number.MAX_SAFE_INTEGER) { - throw "value too large" - } + async vint52(v: number) { + if (v > Number.MAX_SAFE_INTEGER) { + throw "value too large" + } - if (v < 1 << 6) { - return this.uint8(v) - } else if (v < 1 << 14) { - return this.uint16(v | 0x4000) - } else if (v < 1 << 30) { - return this.uint32(v | 0x80000000) - } else { - return this.uint64(BigInt(v) | 0xc000000000000000n) - } - } + if (v < 1 << 6) { + return this.uint8(v) + } else if (v < 1 << 14) { + return this.uint16(v | 0x4000) + } else if (v < 1 << 30) { + return this.uint32(v | 0x80000000) + } else { + return this.uint64(BigInt(v) | 0xc000000000000000n) + } + } - async uint64(v: bigint) { - const view = new DataView(this.buffer, 0, 8) - view.setBigUint64(0, v) - return this.writer.write(view) - } + async uint64(v: bigint) { + const view = new DataView(this.buffer, 0, 8) + view.setBigUint64(0, v) + return this.writer.write(view) + } - async vint64(v: bigint) { - if (v < 1 << 6) { - return this.uint8(Number(v)) - } else if (v < 1 << 14) { - return this.uint16(Number(v) | 0x4000) - } else if (v < 1 << 30) { - return this.uint32(Number(v) | 0x80000000) - } else { - return this.uint64(v | 0xc000000000000000n) - } - } + async vint64(v: bigint) { + if (v < 1 << 6) { + return this.uint8(Number(v)) + } else if (v < 1 << 14) { + return this.uint16(Number(v) | 0x4000) + } else if (v < 1 << 30) { + return this.uint32(Number(v) | 0x80000000) + } else { + return this.uint64(v | 0xc000000000000000n) + } + } - async bytes(buffer: ArrayBuffer) { - return this.writer.write(buffer) - } + async bytes(buffer: ArrayBuffer) { + return this.writer.write(buffer) + } - async string(str: string) { - const data = new TextEncoder().encode(str) - return this.writer.write(data) - } + async string(str: string) { + const data = new TextEncoder().encode(str) + return this.writer.write(data) + } } diff --git a/web/src/transport/index.ts b/web/src/transport/index.ts index f0630b1..13d16de 100644 --- a/web/src/transport/index.ts +++ b/web/src/transport/index.ts @@ -2,97 +2,95 @@ import * as Stream from "../stream" import * as Interface from "./interface" export interface Config { - url: string - fingerprint?: WebTransportHash // the certificate fingerprint, temporarily needed for local development + url: string + fingerprint?: WebTransportHash // the certificate fingerprint, temporarily needed for local development } export default class Transport { - quic: Promise - api: Promise - callback?: Interface.Callback + quic: Promise + api: Promise + callback?: Interface.Callback - constructor(config: Config) { - this.quic = this.connect(config) + constructor(config: Config) { + this.quic = this.connect(config) - // Create a unidirectional stream for all of our messages - this.api = this.quic.then((q) => { - return q.createUnidirectionalStream() - }) + // Create a unidirectional stream for all of our messages + this.api = this.quic.then((q) => { + return q.createUnidirectionalStream() + }) - // async functions - this.receiveStreams() - } + // async functions + this.receiveStreams() + } - async close() { - ;(await this.quic).close() - } + async close() { + ;(await this.quic).close() + } - // Helper function to make creating a promise easier - private async connect(config: Config): Promise { - const options: WebTransportOptions = {} - if (config.fingerprint) { - options.serverCertificateHashes = [config.fingerprint] - } + // Helper function to make creating a promise easier + private async connect(config: Config): Promise { + const options: WebTransportOptions = {} + if (config.fingerprint) { + options.serverCertificateHashes = [config.fingerprint] + } - const quic = new WebTransport(config.url, options) - await quic.ready - return quic - } + const quic = new WebTransport(config.url, options) + await quic.ready + return quic + } - async sendMessage(msg: any) { - const payload = JSON.stringify(msg) - const size = payload.length + 8 + async sendMessage(msg: any) { + const payload = JSON.stringify(msg) + const size = payload.length + 8 - const stream = await this.api + const stream = await this.api - const writer = new Stream.Writer(stream) - await writer.uint32(size) - await writer.string("warp") - await writer.string(payload) - writer.release() - } + const writer = new Stream.Writer(stream) + await writer.uint32(size) + await writer.string("warp") + await writer.string(payload) + writer.release() + } - async receiveStreams() { - const q = await this.quic - const streams = q.incomingUnidirectionalStreams.getReader() + async receiveStreams() { + const q = await this.quic + const streams = q.incomingUnidirectionalStreams.getReader() - for (;;) { - const result = await streams.read() - if (result.done) break + for (;;) { + const result = await streams.read() + if (result.done) break - const stream = result.value - this.handleStream(stream) // don't await - } - } + const stream = result.value + this.handleStream(stream) // don't await + } + } - async handleStream(stream: ReadableStream) { - const r = new Stream.Reader(stream) + async handleStream(stream: ReadableStream) { + const r = new Stream.Reader(stream) - while (!(await r.done())) { - const size = await r.uint32() - const typ = new TextDecoder("utf-8").decode(await r.bytes(4)) + while (!(await r.done())) { + const size = await r.uint32() + const typ = new TextDecoder("utf-8").decode(await r.bytes(4)) - if (typ != "warp") throw "expected warp atom" - if (size < 8) throw "atom too small" + if (typ != "warp") throw "expected warp atom" + if (size < 8) throw "atom too small" - const payload = new TextDecoder("utf-8").decode( - await r.bytes(size - 8) - ) - const msg = JSON.parse(payload) + const payload = new TextDecoder("utf-8").decode(await r.bytes(size - 8)) + const msg = JSON.parse(payload) - if (msg.init) { - return this.callback?.onInit({ - buffer: r.buffer, - reader: r.reader, - }) - } else if (msg.segment) { - return this.callback?.onSegment({ - buffer: r.buffer, - reader: r.reader, - }) - } else { - console.warn("unknown message", msg) - } - } - } + if (msg.init) { + return this.callback?.onInit({ + buffer: r.buffer, + reader: r.reader, + }) + } else if (msg.segment) { + return this.callback?.onSegment({ + buffer: r.buffer, + reader: r.reader, + }) + } else { + console.warn("unknown message", msg) + } + } + } } diff --git a/web/src/transport/interface.ts b/web/src/transport/interface.ts index 84a4276..5626c3c 100644 --- a/web/src/transport/interface.ts +++ b/web/src/transport/interface.ts @@ -1,14 +1,14 @@ export interface Callback { - onInit(init: Init): any - onSegment(segment: Segment): any + onInit(init: Init): any + onSegment(segment: Segment): any } export interface Init { - buffer: Uint8Array // unread buffered data - reader: ReadableStream // unread unbuffered data + buffer: Uint8Array // unread buffered data + reader: ReadableStream // unread unbuffered data } export interface Segment { - buffer: Uint8Array // unread buffered data - reader: ReadableStream // unread unbuffered data + buffer: Uint8Array // unread buffered data + reader: ReadableStream // unread unbuffered data } diff --git a/web/src/transport/message.ts b/web/src/transport/message.ts index 6dee40c..c120058 100644 --- a/web/src/transport/message.ts +++ b/web/src/transport/message.ts @@ -3,5 +3,5 @@ export type Init = any export type Segment = any export interface Debug { - max_bitrate: number + max_bitrate: number } diff --git a/web/src/transport/webtransport.d.ts b/web/src/transport/webtransport.d.ts index 7ac6b8d..34394ca 100644 --- a/web/src/transport/webtransport.d.ts +++ b/web/src/transport/webtransport.d.ts @@ -8,77 +8,77 @@ declare module "webtransport" */ interface WebTransportDatagramDuplexStream { - readonly readable: ReadableStream - readonly writable: WritableStream - readonly maxDatagramSize: number - incomingMaxAge: number - outgoingMaxAge: number - incomingHighWaterMark: number - outgoingHighWaterMark: number + readonly readable: ReadableStream + readonly writable: WritableStream + readonly maxDatagramSize: number + incomingMaxAge: number + outgoingMaxAge: number + incomingHighWaterMark: number + outgoingHighWaterMark: number } interface WebTransport { - getStats(): Promise - readonly ready: Promise - readonly closed: Promise - close(closeInfo?: WebTransportCloseInfo): undefined - readonly datagrams: WebTransportDatagramDuplexStream - createBidirectionalStream(): Promise - readonly incomingBidirectionalStreams: ReadableStream - createUnidirectionalStream(): Promise - readonly incomingUnidirectionalStreams: ReadableStream + getStats(): Promise + readonly ready: Promise + readonly closed: Promise + close(closeInfo?: WebTransportCloseInfo): undefined + readonly datagrams: WebTransportDatagramDuplexStream + createBidirectionalStream(): Promise + readonly incomingBidirectionalStreams: ReadableStream + createUnidirectionalStream(): Promise + readonly incomingUnidirectionalStreams: ReadableStream } declare const WebTransport: { - prototype: WebTransport - new (url: string, options?: WebTransportOptions): WebTransport + prototype: WebTransport + new (url: string, options?: WebTransportOptions): WebTransport } interface WebTransportHash { - algorithm?: string - value?: BufferSource + algorithm?: string + value?: BufferSource } interface WebTransportOptions { - allowPooling?: boolean - serverCertificateHashes?: Array + allowPooling?: boolean + serverCertificateHashes?: Array } interface WebTransportCloseInfo { - closeCode?: number - reason?: string + closeCode?: number + reason?: string } interface WebTransportStats { - timestamp?: DOMHighResTimeStamp - bytesSent?: number - packetsSent?: number - numOutgoingStreamsCreated?: number - numIncomingStreamsCreated?: number - bytesReceived?: number - packetsReceived?: number - minRtt?: DOMHighResTimeStamp - numReceivedDatagramsDropped?: number + timestamp?: DOMHighResTimeStamp + bytesSent?: number + packetsSent?: number + numOutgoingStreamsCreated?: number + numIncomingStreamsCreated?: number + bytesReceived?: number + packetsReceived?: number + minRtt?: DOMHighResTimeStamp + numReceivedDatagramsDropped?: number } interface WebTransportBidirectionalStream { - readonly readable: ReadableStream - readonly writable: WritableStream + readonly readable: ReadableStream + readonly writable: WritableStream } interface WebTransportError extends DOMException { - readonly source: WebTransportErrorSource - readonly streamErrorCode: number + readonly source: WebTransportErrorSource + readonly streamErrorCode: number } declare const WebTransportError: { - prototype: WebTransportError - new (init?: WebTransportErrorInit): WebTransportError + prototype: WebTransportError + new (init?: WebTransportErrorInit): WebTransportError } interface WebTransportErrorInit { - streamErrorCode?: number - message?: string + streamErrorCode?: number + message?: string } type WebTransportErrorSource = "stream" | "session" diff --git a/web/src/util/deferred.ts b/web/src/util/deferred.ts index 4ef9ee1..68de7e9 100644 --- a/web/src/util/deferred.ts +++ b/web/src/util/deferred.ts @@ -1,20 +1,20 @@ export default class Deferred { - promise: Promise - resolve: (value: T | PromiseLike) => void - reject: (value: T | PromiseLike) => void + promise: Promise + resolve: (value: T | PromiseLike) => void + reject: (value: T | PromiseLike) => void - constructor() { - // Set initial values so TS stops being annoying. - this.resolve = (_value: T | PromiseLike) => { - /* noop */ - } - this.reject = (_value: T | PromiseLike) => { - /* noop */ - } + constructor() { + // Set initial values so TS stops being annoying. + this.resolve = (_value: T | PromiseLike) => { + /* noop */ + } + this.reject = (_value: T | PromiseLike) => { + /* noop */ + } - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve - this.reject = reject - }) - } + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve + this.reject = reject + }) + } } diff --git a/web/tsconfig.json b/web/tsconfig.json index d1a39a8..7966004 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,9 +1,9 @@ { - "include": ["src/**/*"], - "compilerOptions": { - "target": "es2022", - "module": "es2022", - "moduleResolution": "node", - "strict": true - } + "include": ["src/**/*"], + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "node", + "strict": true + } }