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
-
-
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
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
+ }
}