Merge pull request #17 from kixelated/more-lint

More linting.
This commit is contained in:
kixelated 2023-05-22 22:11:45 -07:00 committed by GitHub
commit dfe5cc1771
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1546 additions and 5636 deletions

View File

@ -1,8 +1,6 @@
name: server name: server
on: on:
push:
branches: [ "main" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
@ -13,11 +11,23 @@ jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: ./server
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: build
working-directory: server - name: toolchain
run: cargo build --verbose uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy, rustfmt
- name: test - name: test
working-directory: server
run: cargo test --verbose run: cargo test --verbose
- name: fmt
run: cargo fmt --check
- name: clippy
run: cargo clippy

View File

@ -1,8 +1,6 @@
name: web name: web
on: on:
push:
branches: [ "main" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
@ -10,17 +8,25 @@ jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: install - name: install
working-directory: web
run: yarn install run: yarn install
- name: cert - name: cert
working-directory: cert working-directory: cert
run: ./generate run: ./generate
- name: build - name: build
working-directory: web
run: yarn build run: yarn build
- name: fmt
run: yarn prettier --check .
- name: lint - name: lint
working-directory: web
run: yarn lint run: yarn lint

View File

@ -1,10 +1,14 @@
/* eslint-env node */ /* eslint-env node */
module.exports = { module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], extends: [
parser: '@typescript-eslint/parser', "eslint:recommended",
plugins: ['@typescript-eslint'], "plugin:@typescript-eslint/recommended",
"prettier",
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true, root: true,
ignorePatterns: [ 'dist', 'node_modules' ], ignorePatterns: ["dist", "node_modules"],
rules: { rules: {
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
@ -13,10 +17,10 @@ module.exports = {
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"warn", // or "error" "warn", // or "error"
{ {
"argsIgnorePattern": "^_", argsIgnorePattern: "^_",
"varsIgnorePattern": "^_", varsIgnorePattern: "^_",
"caughtErrorsIgnorePattern": "^_" caughtErrorsIgnorePattern: "^_",
} },
], ],
} },
}; }

2
web/.prettierignore Normal file
View File

@ -0,0 +1,2 @@
dist
node_modules

3
web/.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"semi": false
}

View File

@ -1,7 +1,7 @@
module.exports = function (app) { module.exports = function (app) {
app.use((req, res, next) => { app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); res.setHeader("Cross-Origin-Opener-Policy", "same-origin")
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); res.setHeader("Cross-Origin-Embedder-Policy", "require-corp")
next(); next()
}); })
}; }

4253
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"serve": "parcel serve --https --cert ../cert/localhost.crt --key ../cert/localhost.key --port 4444 --open", "serve": "parcel serve --https --cert ../cert/localhost.crt --key ../cert/localhost.key --port 4444 --open",
"build": "parcel build", "build": "parcel build",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"lint": "eslint ." "lint": "eslint .",
"fmt": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@parcel/transformer-inline-string": "2.8.3", "@parcel/transformer-inline-string": "2.8.3",
@ -15,7 +16,9 @@
"@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7", "@typescript-eslint/parser": "^5.59.7",
"eslint": "^8.41.0", "eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"parcel": "^2.8.0", "parcel": "^2.8.0",
"prettier": "^2.8.8",
"typescript": "^5.0.4" "typescript": "^5.0.4"
}, },
"dependencies": { "dependencies": {

View File

@ -1,104 +1,104 @@
import * as MP4 from "../mp4" import * as MP4 from "../mp4"
export class Encoder { export class Encoder {
container: MP4.ISOFile container: MP4.ISOFile
audio: AudioEncoder audio: AudioEncoder
video: VideoEncoder video: VideoEncoder
constructor() { constructor() {
this.container = new MP4.ISOFile(); this.container = new MP4.ISOFile()
this.audio = new AudioEncoder({ this.audio = new AudioEncoder({
output: this.onAudio.bind(this), output: this.onAudio.bind(this),
error: console.warn, error: console.warn,
}); })
this.video = new VideoEncoder({ this.video = new VideoEncoder({
output: this.onVideo.bind(this), output: this.onVideo.bind(this),
error: console.warn, error: console.warn,
}); })
this.container.init(); this.container.init()
this.audio.configure({ this.audio.configure({
codec: "mp4a.40.2", codec: "mp4a.40.2",
numberOfChannels: 2, numberOfChannels: 2,
sampleRate: 44100, sampleRate: 44100,
// TODO bitrate // TODO bitrate
}) })
this.video.configure({ this.video.configure({
codec: "avc1.42002A", // TODO h.264 baseline codec: "avc1.42002A", // TODO h.264 baseline
avc: { format: "avc" }, // or annexb avc: { format: "avc" }, // or annexb
width: 1280, width: 1280,
height: 720, height: 720,
// TODO bitrate // TODO bitrate
// TODO bitrateMode // TODO bitrateMode
// TODO framerate // TODO framerate
// TODO latencyMode // TODO latencyMode
}) })
} }
onAudio(frame: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata) { onAudio(frame: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata) {
const config = metadata.decoderConfig! const config = metadata.decoderConfig!
const track_id = 1; const track_id = 1
if (!this.container.getTrackById(track_id)) { if (!this.container.getTrackById(track_id)) {
this.container.addTrack({ this.container.addTrack({
id: track_id, id: track_id,
type: "mp4a", // TODO wrong type: "mp4a", // TODO wrong
timescale: 1000, // TODO verify timescale: 1000, // TODO verify
channel_count: config.numberOfChannels, channel_count: config.numberOfChannels,
samplerate: config.sampleRate, samplerate: config.sampleRate,
description: config.description, // TODO verify description: config.description, // TODO verify
// TODO description_boxes?: Box[]; // TODO description_boxes?: Box[];
}); })
} }
const buffer = new Uint8Array(frame.byteLength); const buffer = new Uint8Array(frame.byteLength)
frame.copyTo(buffer); frame.copyTo(buffer)
// TODO cts? // TODO cts?
const sample = this.container.addSample(track_id, buffer, { const sample = this.container.addSample(track_id, buffer, {
is_sync: frame.type == "key", is_sync: frame.type == "key",
duration: frame.duration!, duration: frame.duration!,
dts: frame.timestamp, dts: frame.timestamp,
}); })
const _stream = this.container.createSingleSampleMoof(sample); const _stream = this.container.createSingleSampleMoof(sample)
} }
onVideo(frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) { onVideo(frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) {
const config = metadata!.decoderConfig! const config = metadata!.decoderConfig!
const track_id = 2; const track_id = 2
if (!this.container.getTrackById(track_id)) { if (!this.container.getTrackById(track_id)) {
this.container.addTrack({ this.container.addTrack({
id: 2, id: 2,
type: "avc1", type: "avc1",
width: config.codedWidth, width: config.codedWidth,
height: config.codedHeight, height: config.codedHeight,
timescale: 1000, // TODO verify timescale: 1000, // TODO verify
description: config.description, // TODO verify description: config.description, // TODO verify
// TODO description_boxes?: Box[]; // TODO description_boxes?: Box[];
}); })
} }
const buffer = new Uint8Array(frame.byteLength); const buffer = new Uint8Array(frame.byteLength)
frame.copyTo(buffer); frame.copyTo(buffer)
// TODO cts? // TODO cts?
const sample = this.container.addSample(track_id, buffer, { const sample = this.container.addSample(track_id, buffer, {
is_sync: frame.type == "key", is_sync: frame.type == "key",
duration: frame.duration!, duration: frame.duration!,
dts: frame.timestamp, dts: frame.timestamp,
}); })
const _stream = this.container.createSingleSampleMoof(sample); const _stream = this.container.createSingleSampleMoof(sample)
} }
} }

View File

@ -1,5 +1,5 @@
export default class Broadcaster { export default class Broadcaster {
constructor() { constructor() {
// TODO // TODO
} }
} }

View File

@ -1,73 +1,75 @@
html, body, #player { html,
width: 100%; body,
#player {
width: 100%;
} }
body { body {
background: #000000; background: #000000;
color: #ffffff; color: #ffffff;
padding: 0; padding: 0;
margin: 0; margin: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
font-family: sans-serif; font-family: sans-serif;
} }
#screen { #screen {
position: relative; position: relative;
} }
#screen #play { #screen #play {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1; z-index: 1;
} }
#controls { #controls {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: 8px 16px; padding: 8px 16px;
} }
#controls > * { #controls > * {
margin-right: 8px; margin-right: 8px;
} }
#controls label { #controls label {
margin-right: 8px; margin-right: 8px;
} }
#stats { #stats {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
} }
#stats label { #stats label {
padding: 0 1rem; padding: 0 1rem;
} }
.buffer { .buffer {
position: relative; position: relative;
width: 100%; width: 100%;
} }
.buffer .fill { .buffer .fill {
position: absolute; position: absolute;
transition-duration: 0.1s; transition-duration: 0.1s;
transition-property: left, right, background-color; transition-property: left, right, background-color;
background-color: RebeccaPurple; background-color: RebeccaPurple;
height: 100%; height: 100%;
text-align: right; text-align: right;
padding-right: 0.5rem; padding-right: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.buffer .fill.net { .buffer .fill.net {
background-color: Purple; background-color: Purple;
} }

View File

@ -1,35 +1,33 @@
<!doctype html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="UTF-8" />
<title>WARP</title>
<head> <link rel="stylesheet" href="index.css" />
<meta charset="UTF-8"> </head>
<title>WARP</title>
<link rel="stylesheet" href="index.css"> <body>
</head> <div id="player">
<div id="screen">
<div id="play"><span>click to play</span></div>
<canvas id="video" width="1280" height="720"></canvas>
</div>
<body> <div id="controls">
<div id="player"> <button type="button" id="live">Go Live</button>
<div id="screen"> <button type="button" id="throttle">Throttle: None</button>
<div id="play"><span>click to play</span></div> </div>
<canvas id="video" width="1280" height="720"></canvas>
</div>
<div id="controls"> <div id="stats">
<button type="button" id="live">Go Live</button> <label>Audio Buffer:</label>
<button type="button" id="throttle">Throttle: None</button> <div class="audio buffer"></div>
</div>
<div id="stats"> <label>Video Buffer:</label>
<label>Audio Buffer:</label> <div class="video buffer"></div>
<div class="audio buffer"></div> </div>
</div>
<label>Video Buffer:</label>
<div class="video buffer"></div>
</div>
</div>
<script src="index.ts" type="module"></script>
</body>
<script src="index.ts" type="module"></script>
</body>
</html> </html>

View File

@ -2,12 +2,12 @@ import Player from "./player"
import Transport from "./transport" import Transport from "./transport"
// @ts-ignore embed the certificate fingerprint using bundler // @ts-ignore embed the certificate fingerprint using bundler
import fingerprintHex from 'bundle-text:../fingerprint.hex'; import fingerprintHex from "bundle-text:../fingerprint.hex"
// Convert the hex to binary. // Convert the hex to binary.
const fingerprint = []; const fingerprint = []
for (let c = 0; c < fingerprintHex.length - 1; c += 2) { 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) const params = new URLSearchParams(window.location.search)
@ -16,26 +16,27 @@ const url = params.get("url") || "https://localhost:4443/watch"
const canvas = document.querySelector<HTMLCanvasElement>("canvas#video")! const canvas = document.querySelector<HTMLCanvasElement>("canvas#video")!
const transport = new Transport({ const transport = new Transport({
url: url, url: url,
fingerprint: { // TODO remove when Chrome accepts the system CA fingerprint: {
"algorithm": "sha-256", // TODO remove when Chrome accepts the system CA
"value": new Uint8Array(fingerprint), algorithm: "sha-256",
}, value: new Uint8Array(fingerprint),
},
}) })
const player = new Player({ const player = new Player({
transport, transport,
canvas: canvas.transferControlToOffscreen(), canvas: canvas.transferControlToOffscreen(),
}) })
const play = document.querySelector<HTMLElement>("#screen #play")! const play = document.querySelector<HTMLElement>("#screen #play")!
const playFunc = (e: Event) => { const playFunc = (e: Event) => {
player.play() player.play()
e.preventDefault() e.preventDefault()
play.removeEventListener('click', playFunc) play.removeEventListener("click", playFunc)
play.style.display = "none" play.style.display = "none"
} }
play.addEventListener('click', playFunc) play.addEventListener("click", playFunc)

View File

@ -1,16 +1,16 @@
// Rename some stuff so it's on brand. // Rename some stuff so it's on brand.
export { export {
createFile as New, createFile as New,
MP4File as File, MP4File as File,
MP4ArrayBuffer as ArrayBuffer, MP4ArrayBuffer as ArrayBuffer,
MP4Info as Info, MP4Info as Info,
MP4Track as Track, MP4Track as Track,
MP4AudioTrack as AudioTrack, MP4AudioTrack as AudioTrack,
MP4VideoTrack as VideoTrack, MP4VideoTrack as VideoTrack,
DataStream as Stream, DataStream as Stream,
Box, Box,
ISOFile, ISOFile,
Sample, Sample,
} from "mp4box" } from "mp4box"
export { Init, InitParser } from "./init" export { Init, InitParser } from "./init"

View File

@ -1,43 +1,43 @@
import * as MP4 from "./index" import * as MP4 from "./index"
export interface Init { export interface Init {
raw: MP4.ArrayBuffer; raw: MP4.ArrayBuffer
info: MP4.Info; info: MP4.Info
} }
export class InitParser { export class InitParser {
mp4box: MP4.File; mp4box: MP4.File
offset: number; offset: number
raw: MP4.ArrayBuffer[]; raw: MP4.ArrayBuffer[]
info: Promise<MP4.Info>; info: Promise<MP4.Info>
constructor() { constructor() {
this.mp4box = MP4.New() this.mp4box = MP4.New()
this.raw = [] this.raw = []
this.offset = 0 this.offset = 0
// Create a promise that gets resolved once the init segment has been parsed. // Create a promise that gets resolved once the init segment has been parsed.
this.info = new Promise((resolve, reject) => { this.info = new Promise((resolve, reject) => {
this.mp4box.onError = reject this.mp4box.onError = reject
this.mp4box.onReady = resolve this.mp4box.onReady = resolve
}) })
} }
push(data: Uint8Array) { push(data: Uint8Array) {
// Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately
const box = new Uint8Array(data.byteLength); const box = new Uint8Array(data.byteLength)
box.set(data) box.set(data)
// and for some reason we need to modify the underlying ArrayBuffer with fileStart // and for some reason we need to modify the underlying ArrayBuffer with fileStart
const buffer = box.buffer as MP4.ArrayBuffer const buffer = box.buffer as MP4.ArrayBuffer
buffer.fileStart = this.offset buffer.fileStart = this.offset
// Parse the data // Parse the data
this.offset = this.mp4box.appendBuffer(buffer) this.offset = this.mp4box.appendBuffer(buffer)
this.mp4box.flush() this.mp4box.flush()
// Add the box to our queue of chunks // Add the box to our queue of chunks
this.raw.push(buffer) this.raw.push(buffer)
} }
} }

View File

@ -1,225 +1,239 @@
// https://github.com/gpac/mp4box.js/issues/233 // https://github.com/gpac/mp4box.js/issues/233
declare module "mp4box" { declare module "mp4box" {
export interface MP4MediaTrack { export interface MP4MediaTrack {
id: number; id: number
created: Date; created: Date
modified: Date; modified: Date
movie_duration: number; movie_duration: number
layer: number; layer: number
alternate_group: number; alternate_group: number
volume: number; volume: number
track_width: number; track_width: number
track_height: number; track_height: number
timescale: number; timescale: number
duration: number; duration: number
bitrate: number; bitrate: number
codec: string; codec: string
language: string; language: string
nb_samples: number; nb_samples: number
} }
export interface MP4VideoData { export interface MP4VideoData {
width: number; width: number
height: number; height: number
} }
export interface MP4VideoTrack extends MP4MediaTrack { export interface MP4VideoTrack extends MP4MediaTrack {
video: MP4VideoData; video: MP4VideoData
} }
export interface MP4AudioData { export interface MP4AudioData {
sample_rate: number; sample_rate: number
channel_count: number; channel_count: number
sample_size: number; sample_size: number
} }
export interface MP4AudioTrack extends MP4MediaTrack { export interface MP4AudioTrack extends MP4MediaTrack {
audio: MP4AudioData; audio: MP4AudioData
} }
export type MP4Track = MP4VideoTrack | MP4AudioTrack; export type MP4Track = MP4VideoTrack | MP4AudioTrack
export interface MP4Info { export interface MP4Info {
duration: number; duration: number
timescale: number; timescale: number
fragment_duration: number; fragment_duration: number
isFragmented: boolean; isFragmented: boolean
isProgressive: boolean; isProgressive: boolean
hasIOD: boolean; hasIOD: boolean
brands: string[]; brands: string[]
created: Date; created: Date
modified: Date; modified: Date
tracks: MP4Track[]; tracks: MP4Track[]
mime: string; mime: string
audioTracks: MP4AudioTrack[]; audioTracks: MP4AudioTrack[]
videoTracks: MP4VideoTrack[]; videoTracks: MP4VideoTrack[]
} }
export type MP4ArrayBuffer = ArrayBuffer & {fileStart: number}; export type MP4ArrayBuffer = ArrayBuffer & { fileStart: number }
export interface MP4File { export interface MP4File {
onMoovStart?: () => void; onMoovStart?: () => void
onReady?: (info: MP4Info) => void; onReady?: (info: MP4Info) => void
onError?: (e: string) => void; onError?: (e: string) => void
onSamples?: (id: number, user: any, samples: Sample[]) => void; onSamples?: (id: number, user: any, samples: Sample[]) => void
appendBuffer(data: MP4ArrayBuffer): number; appendBuffer(data: MP4ArrayBuffer): number
start(): void; start(): void
stop(): void; stop(): void
flush(): 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 { export interface Sample {
number: number; number: number
track_id: number; track_id: number
timescale: number; timescale: number
description_index: number; description_index: number
description: any; description: any
data: ArrayBuffer; data: ArrayBuffer
size: number; size: number
alreadyRead?: number; alreadyRead?: number
duration: number; duration: number
cts: number; cts: number
dts: number; dts: number
is_sync: boolean; is_sync: boolean
is_leading: number; is_leading: number
depends_on: number; depends_on: number
is_depended_on: number; is_depended_on: number
has_redundancy: number; has_redundancy: number
degration_priority: number; degration_priority: number
offset: number; offset: number
subsamples: any; subsamples: any
} }
export interface ExtractionOptions { export interface ExtractionOptions {
nbSamples: number; nbSamples: number
} }
const BIG_ENDIAN: boolean; const BIG_ENDIAN: boolean
const LITTLE_ENDIAN: boolean; const LITTLE_ENDIAN: boolean
export class DataStream { export class DataStream {
constructor(buffer?: ArrayBuffer, byteOffset?: number, littleEndian?: boolean); constructor(
getPosition(): number; buffer?: ArrayBuffer,
byteOffset?: number,
littleEndian?: boolean
)
getPosition(): number
get byteLength(): number; get byteLength(): number
get buffer(): ArrayBuffer; get buffer(): ArrayBuffer
set buffer(v: ArrayBuffer); set buffer(v: ArrayBuffer)
get byteOffset(): number; get byteOffset(): number
set byteOffset(v: number); set byteOffset(v: number)
get dataView(): DataView; get dataView(): DataView
set dataView(v: DataView); set dataView(v: DataView)
seek(pos: number): void; seek(pos: number): void
isEof(): boolean; isEof(): boolean
mapUint8Array(length: number): Uint8Array; mapUint8Array(length: number): Uint8Array
readInt32Array(length: number, littleEndian: boolean): Int32Array; readInt32Array(length: number, littleEndian: boolean): Int32Array
readInt16Array(length: number, littleEndian: boolean): Int16Array; readInt16Array(length: number, littleEndian: boolean): Int16Array
readInt8Array(length: number): Int8Array; readInt8Array(length: number): Int8Array
readUint32Array(length: number, littleEndian: boolean): Uint32Array; readUint32Array(length: number, littleEndian: boolean): Uint32Array
readUint16Array(length: number, littleEndian: boolean): Uint16Array; readUint16Array(length: number, littleEndian: boolean): Uint16Array
readUint8Array(length: number): Uint8Array; readUint8Array(length: number): Uint8Array
readFloat64Array(length: number, littleEndian: boolean): Float64Array; readFloat64Array(length: number, littleEndian: boolean): Float64Array
readFloat32Array(length: number, littleEndian: boolean): Float32Array; readFloat32Array(length: number, littleEndian: boolean): Float32Array
readInt32(littleEndian: boolean): number; readInt32(littleEndian: boolean): number
readInt16(littleEndian: boolean): number; readInt16(littleEndian: boolean): number
readInt8(): number; readInt8(): number
readUint32(littleEndian: boolean): number; readUint32(littleEndian: boolean): number
readUint16(littleEndian: boolean): number; readUint16(littleEndian: boolean): number
readUint8(): number; readUint8(): number
readFloat32(littleEndian: boolean): number; readFloat32(littleEndian: boolean): number
readFloat64(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 { export class Box {
write(stream: DataStream): void; write(stream: DataStream): void
} }
export interface TrackOptions { export interface TrackOptions {
id?: number; id?: number
type?: string; type?: string
width?: number; width?: number
height?: number; height?: number
duration?: number; duration?: number
layer?: number; layer?: number
timescale?: number; timescale?: number
media_duration?: number; media_duration?: number
language?: string; language?: string
hdlr?: string; hdlr?: string
// video // video
avcDecoderConfigRecord?: any; avcDecoderConfigRecord?: any
// audio // audio
balance?: number; balance?: number
channel_count?: number; channel_count?: number
samplesize?: number; samplesize?: number
samplerate?: number; samplerate?: number
//captions //captions
namespace?: string; namespace?: string
schema_location?: string; schema_location?: string
auxiliary_mime_types?: string; auxiliary_mime_types?: string
description?: any; description?: any
description_boxes?: Box[]; description_boxes?: Box[]
default_sample_description_index_id?: number; default_sample_description_index_id?: number
default_sample_duration?: number; default_sample_duration?: number
default_sample_size?: number; default_sample_size?: number
default_sample_flags?: number; default_sample_flags?: number
} }
export interface FileOptions { export interface FileOptions {
brands?: string[]; brands?: string[]
timescale?: number; timescale?: number
rate?: number; rate?: number
duration?: number; duration?: number
width?: number; width?: number
} }
export interface SampleOptions { export interface SampleOptions {
sample_description_index?: number; sample_description_index?: number
duration?: number; duration?: number
cts?: number; cts?: number
dts?: number; dts?: number
is_sync?: boolean; is_sync?: boolean
is_leading?: number; is_leading?: number
depends_on?: number; depends_on?: number
is_depended_on?: number; is_depended_on?: number
has_redundancy?: number; has_redundancy?: number
degradation_priority?: number; degradation_priority?: number
subsamples?: any; subsamples?: any
} }
// TODO add the remaining functions // TODO add the remaining functions
// TODO move to another module // TODO move to another module
export class ISOFile { export class ISOFile {
constructor(stream?: DataStream); constructor(stream?: DataStream)
init(options?: FileOptions): ISOFile; init(options?: FileOptions): ISOFile
addTrack(options?: TrackOptions): number; addTrack(options?: TrackOptions): number
addSample(track: number, data: ArrayBuffer, options?: SampleOptions): Sample; addSample(track: number, data: ArrayBuffer, options?: SampleOptions): Sample
createSingleSampleMoof(sample: Sample): Box; createSingleSampleMoof(sample: Sample): Box
// helpers // helpers
getTrackById(id: number): Box | undefined; getTrackById(id: number): Box | undefined
getTrexById(id: number): Box | undefined; getTrexById(id: number): Box | undefined
} }
export { }; export {}
} }

View File

@ -1,79 +1,82 @@
import * as Message from "./message"; import * as Message from "./message"
import { Ring } from "./ring" import { Ring } from "./ring"
export default class Audio { export default class Audio {
ring?: Ring; ring?: Ring
queue: Array<AudioData>; queue: Array<AudioData>
render?: number; // non-zero if requestAnimationFrame has been called render?: number // non-zero if requestAnimationFrame has been called
last?: number; // the timestamp of the last rendered frame, in microseconds last?: number // the timestamp of the last rendered frame, in microseconds
constructor(_config: Message.Config) { constructor(_config: Message.Config) {
this.queue = [] this.queue = []
}
push(frame: AudioData) {
// Drop any old frames
if (this.last && frame.timestamp <= this.last) {
frame.close()
return
} }
push(frame: AudioData) { // Insert the frame into the queue sorted by timestamp.
// Drop any old frames if (
if (this.last && frame.timestamp <= this.last) { this.queue.length > 0 &&
frame.close() this.queue[this.queue.length - 1].timestamp <= frame.timestamp
return ) {
} // 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. while (low < high) {
if (this.queue.length > 0 && this.queue[this.queue.length - 1].timestamp <= frame.timestamp) { const mid = (low + high) >>> 1
// Fast path because we normally append to the end. if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1
this.queue.push(frame) else high = mid
} else { }
// Do a full binary search
let low = 0
let high = this.queue.length;
while (low < high) { this.queue.splice(low, 0, frame)
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.emit()
} }
emit() { this.emit()
const ring = this.ring }
if (!ring) {
return
}
while (this.queue.length) { emit() {
const frame = this.queue[0]; const ring = this.ring
if (ring.size() + frame.numberOfFrames > ring.capacity) { if (!ring) {
// Buffer is full return
break
}
const size = ring.write(frame)
if (size < frame.numberOfFrames) {
throw new Error("audio buffer is full")
}
this.last = frame.timestamp
frame.close()
this.queue.shift()
}
} }
play(play: Message.Play) { while (this.queue.length) {
this.ring = new Ring(play.buffer) const frame = this.queue[0]
if (ring.size() + frame.numberOfFrames > ring.capacity) {
// Buffer is full
break
}
if (!this.render) { const size = ring.write(frame)
const sampleRate = 44100 // TODO dynamic if (size < frame.numberOfFrames) {
throw new Error("audio buffer is full")
}
// Refresh every half buffer this.last = frame.timestamp
const refresh = play.buffer.capacity / sampleRate * 1000 / 2
this.render = setInterval(this.emit.bind(this), refresh) frame.close()
} this.queue.shift()
} }
}
play(play: Message.Play) {
this.ring = new Ring(play.buffer)
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)
}
}
} }

View File

@ -1,167 +1,179 @@
import * as Message from "./message"; import * as Message from "./message"
import * as MP4 from "../mp4" import * as MP4 from "../mp4"
import * as Stream from "../stream" import * as Stream from "../stream"
import Renderer from "./renderer" import Renderer from "./renderer"
export default class Decoder { export default class Decoder {
init: MP4.InitParser; init: MP4.InitParser
decoders: Map<number, AudioDecoder | VideoDecoder>; decoders: Map<number, AudioDecoder | VideoDecoder>
renderer: Renderer; renderer: Renderer
constructor(renderer: Renderer) { constructor(renderer: Renderer) {
this.init = new MP4.InitParser(); this.init = new MP4.InitParser()
this.decoders = new Map(); this.decoders = new Map()
this.renderer = renderer; 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
this.init.push(data)
} }
async receiveInit(msg: Message.Init) { // TODO make sure the init segment is fully received
const stream = new Stream.Reader(msg.reader, msg.buffer); }
for (;;) {
const data = await stream.read()
if (!data) break
this.init.push(data) 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()
// TODO make sure the init segment is fully received 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()
} }
async receiveSegment(msg: Message.Segment) { // MP4box requires us to reparse the init segment unfortunately
// Wait for the init segment to be fully received and parsed let offset = 0
const init = await this.init.info
const input = MP4.New();
input.onSamples = this.onSamples.bind(this); for (const raw of this.init.raw) {
input.onReady = (track: any) => { raw.fileStart = offset
// Extract all of the tracks, because we don't know if it's audio or video. offset = input.appendBuffer(raw)
for (const i of init.tracks) {
input.setExtractionOptions(track.id, i, { nbSamples: 1 });
}
input.start();
}
// 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)
}
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)
// 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)
// 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()
}
} }
onSamples(track_id: number, track: MP4.Track, samples: MP4.Sample[]) { const stream = new Stream.Reader(msg.reader, msg.buffer)
let decoder = this.decoders.get(track_id);
if (!decoder) { // For whatever reason, mp4box doesn't work until you read an atom at a time.
// We need a sample to initalize the video decoder, because of mp4box limitations. while (!(await stream.done())) {
const sample = samples[0]; const raw = await stream.peek(4)
if (isVideoTrack(track)) { // TODO this doesn't support when size = 0 (until EOF) or size = 1 (extended size)
// Configure the decoder using the AVC box for H.264 const size = new DataView(
// TODO it should be easy to support other codecs, just need to know the right boxes. raw.buffer,
const avcc = sample.description.avcC; raw.byteOffset,
if (!avcc) throw new Error("TODO only h264 is supported"); raw.byteLength
).getUint32(0)
const atom = await stream.bytes(size)
const description = new MP4.Stream(new Uint8Array(avcc.size), 0, false) // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately
avcc.write(description) const box = new Uint8Array(atom.byteLength)
box.set(atom)
const videoDecoder = new VideoDecoder({ // and for some reason we need to modify the underlying ArrayBuffer with offset
output: this.renderer.push.bind(this.renderer), const buffer = box.buffer as MP4.ArrayBuffer
error: console.warn, buffer.fileStart = offset
});
videoDecoder.configure({ // Parse the data
codec: track.codec, offset = input.appendBuffer(buffer)
codedHeight: track.video.height, input.flush()
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,
});
audioDecoder.configure({
codec: track.codec,
numberOfChannels: track.audio.channel_count,
sampleRate: track.audio.sample_rate,
})
decoder = audioDecoder
} else {
throw new Error("unknown track type")
}
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
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")
}
}
} }
}
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 (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 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
})
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,
})
decoder = audioDecoder
} else {
throw new Error("unknown track type")
}
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
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 is AudioDecoder { function isAudioDecoder(
return decoder instanceof AudioDecoder decoder: AudioDecoder | VideoDecoder
): decoder is AudioDecoder {
return decoder instanceof AudioDecoder
} }
function isVideoDecoder(decoder: AudioDecoder | VideoDecoder): decoder is VideoDecoder { function isVideoDecoder(
return decoder instanceof VideoDecoder decoder: AudioDecoder | VideoDecoder
): decoder is VideoDecoder {
return decoder instanceof VideoDecoder
} }
function isAudioTrack(track: MP4.Track): track is MP4.AudioTrack { 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 { function isVideoTrack(track: MP4.Track): track is MP4.VideoTrack {
return (track as MP4.VideoTrack).video !== undefined; return (track as MP4.VideoTrack).video !== undefined
} }

View File

@ -3,86 +3,89 @@ import * as Ring from "./ring"
import Transport from "../transport" import Transport from "../transport"
export interface Config { export interface Config {
transport: Transport transport: Transport
canvas: OffscreenCanvas; canvas: OffscreenCanvas
} }
// This class must be created on the main thread due to AudioContext. // This class must be created on the main thread due to AudioContext.
export default class Player { export default class Player {
context: AudioContext; context: AudioContext
worker: Worker; worker: Worker
worklet: Promise<AudioWorkletNode>; worklet: Promise<AudioWorkletNode>
transport: Transport transport: Transport
constructor(config: Config) { constructor(config: Config) {
this.transport = config.transport this.transport = config.transport
this.transport.callback = this; this.transport.callback = this
this.context = new AudioContext({ this.context = new AudioContext({
latencyHint: "interactive", latencyHint: "interactive",
sampleRate: 44100, sampleRate: 44100,
}) })
this.worker = this.setupWorker(config) this.worker = this.setupWorker(config)
this.worklet = this.setupWorklet(config) this.worklet = this.setupWorklet(config)
}
private setupWorker(config: Config): Worker {
const url = new URL("worker.ts", import.meta.url)
const worker = new Worker(url, {
type: "module",
name: "media",
})
const msg = {
canvas: config.canvas,
} }
private setupWorker(config: Config): Worker { worker.postMessage({ config: msg }, [msg.canvas])
const url = new URL('worker.ts', import.meta.url)
const worker = new Worker(url, { return worker
type: "module", }
name: "media",
})
const msg = { private async setupWorklet(_config: Config): Promise<AudioWorkletNode> {
canvas: config.canvas, // Load the worklet source code.
} const url = new URL("worklet.ts", import.meta.url)
await this.context.audioWorklet.addModule(url)
worker.postMessage({ config: msg }, [msg.canvas]) const volume = this.context.createGain()
volume.gain.value = 2.0
return worker // Create a worklet
const worklet = new AudioWorkletNode(this.context, "renderer")
worklet.onprocessorerror = (e: Event) => {
console.error("Audio worklet error:", e)
} }
private async setupWorklet(_config: Config): Promise<AudioWorkletNode> { // Connect the worklet to the volume node and then to the speakers
// Load the worklet source code. worklet.connect(volume)
const url = new URL('worklet.ts', import.meta.url) volume.connect(this.context.destination)
await this.context.audioWorklet.addModule(url)
const volume = this.context.createGain() return worklet
volume.gain.value = 2.0; }
// Create a worklet onInit(init: Message.Init) {
const worklet = new AudioWorkletNode(this.context, 'renderer'); this.worker.postMessage({ init }, [init.buffer.buffer, init.reader])
worklet.onprocessorerror = (e: Event) => { }
console.error("Audio worklet error:", e)
};
// Connect the worklet to the volume node and then to the speakers onSegment(segment: Message.Segment) {
worklet.connect(volume) this.worker.postMessage({ segment }, [
volume.connect(this.context.destination) segment.buffer.buffer,
segment.reader,
])
}
return worklet async play() {
this.context.resume()
const play = {
buffer: new Ring.Buffer(2, 44100 / 10), // 100ms of audio
} }
onInit(init: Message.Init) { const worklet = await this.worklet
this.worker.postMessage({ init }, [init.buffer.buffer, init.reader]) worklet.port.postMessage({ play })
} this.worker.postMessage({ play })
}
onSegment(segment: Message.Segment) {
this.worker.postMessage({ segment }, [segment.buffer.buffer, segment.reader])
}
async play() {
this.context.resume()
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 })
}
} }

View File

@ -1,21 +1,21 @@
import * as Ring from "./ring" import * as Ring from "./ring"
export interface Config { export interface Config {
// video stuff // video stuff
canvas: OffscreenCanvas; canvas: OffscreenCanvas
} }
export interface Init { export interface Init {
buffer: Uint8Array; // unread buffered data buffer: Uint8Array // unread buffered data
reader: ReadableStream; // unread unbuffered data reader: ReadableStream // unread unbuffered data
} }
export interface Segment { export interface Segment {
buffer: Uint8Array; // unread buffered data buffer: Uint8Array // unread buffered data
reader: ReadableStream; // unread unbuffered data reader: ReadableStream // unread unbuffered data
} }
export interface Play { export interface Play {
timestamp?: number; timestamp?: number
buffer: Ring.Buffer; buffer: Ring.Buffer
} }

View File

@ -1,36 +1,36 @@
import * as Message from "./message"; import * as Message from "./message"
import Audio from "./audio" import Audio from "./audio"
import Video from "./video" import Video from "./video"
export default class Renderer { export default class Renderer {
audio: Audio; audio: Audio
video: Video; video: Video
constructor(config: Message.Config) { constructor(config: Message.Config) {
this.audio = new Audio(config); this.audio = new Audio(config)
this.video = new Video(config); this.video = new Video(config)
} }
push(frame: AudioData | VideoFrame) { push(frame: AudioData | VideoFrame) {
if (isAudioData(frame)) { if (isAudioData(frame)) {
this.audio.push(frame); this.audio.push(frame)
} else if (isVideoFrame(frame)) { } else if (isVideoFrame(frame)) {
this.video.push(frame); this.video.push(frame)
} else { } else {
throw new Error("unknown frame type") throw new Error("unknown frame type")
}
} }
}
play(play: Message.Play) { play(play: Message.Play) {
this.audio.play(play); this.audio.play(play)
this.video.play(play); this.video.play(play)
} }
} }
function isAudioData(frame: AudioData | VideoFrame): frame is AudioData { function isAudioData(frame: AudioData | VideoFrame): frame is AudioData {
return frame instanceof AudioData return frame instanceof AudioData
} }
function isVideoFrame(frame: AudioData | VideoFrame): frame is VideoFrame { function isVideoFrame(frame: AudioData | VideoFrame): frame is VideoFrame {
return frame instanceof VideoFrame return frame instanceof VideoFrame
} }

View File

@ -1,155 +1,159 @@
// Ring buffer with audio samples. // Ring buffer with audio samples.
enum STATE { enum STATE {
READ_POS = 0, // The current read position READ_POS = 0, // The current read position
WRITE_POS, // The current write position WRITE_POS, // The current write position
LENGTH // Clever way of saving the total number of enums values. LENGTH, // Clever way of saving the total number of enums values.
} }
// No prototype to make this easier to send via postMessage // No prototype to make this easier to send via postMessage
export class Buffer { export class Buffer {
state: SharedArrayBuffer; state: SharedArrayBuffer
channels: SharedArrayBuffer[]; channels: SharedArrayBuffer[]
capacity: number; capacity: number
constructor(channels: number, capacity: number) { constructor(channels: number, capacity: number) {
// Store the current state in a separate ring buffer. // Store the current state in a separate ring buffer.
this.state = new SharedArrayBuffer(STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT) this.state = new SharedArrayBuffer(
STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT
)
// Create a buffer for each audio channel // Create a buffer for each audio channel
this.channels = [] this.channels = []
for (let i = 0; i < channels; i += 1) { for (let i = 0; i < channels; i += 1) {
const buffer = new SharedArrayBuffer(capacity * Float32Array.BYTES_PER_ELEMENT) const buffer = new SharedArrayBuffer(
this.channels.push(buffer) capacity * Float32Array.BYTES_PER_ELEMENT
} )
this.channels.push(buffer)
this.capacity = capacity
} }
this.capacity = capacity
}
} }
export class Ring { export class Ring {
state: Int32Array; state: Int32Array
channels: Float32Array[]; channels: Float32Array[]
capacity: number; capacity: number
constructor(buffer: Buffer) { constructor(buffer: Buffer) {
this.state = new Int32Array(buffer.state) this.state = new Int32Array(buffer.state)
this.channels = [] this.channels = []
for (const channel of buffer.channels) { for (const channel of buffer.channels) {
this.channels.push(new Float32Array(channel)) this.channels.push(new Float32Array(channel))
}
this.capacity = buffer.capacity
} }
// Write samples for single audio frame, returning the total number written. this.capacity = buffer.capacity
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 // Write samples for single audio frame, returning the total number written.
let endPos = writePos + frame.numberOfFrames; write(frame: AudioData): number {
const readPos = Atomics.load(this.state, STATE.READ_POS)
const writePos = Atomics.load(this.state, STATE.WRITE_POS)
if (endPos > readPos + this.capacity) { const startPos = writePos
endPos = readPos + this.capacity let endPos = writePos + frame.numberOfFrames
if (endPos <= startPos) {
// No space to write
return 0
}
}
const startIndex = startPos % this.capacity; if (endPos > readPos + this.capacity) {
const endIndex = endPos % this.capacity; endPos = readPos + this.capacity
if (endPos <= startPos) {
// Loop over each channel // No space to write
for (let i = 0; i < this.channels.length; i += 1) { return 0
const channel = this.channels[i] }
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(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,
})
}
}
}
Atomics.store(this.state, STATE.WRITE_POS, endPos)
return endPos - startPos
} }
read(dst: Float32Array[]): number { const startIndex = startPos % this.capacity
const readPos = Atomics.load(this.state, STATE.READ_POS) const endIndex = endPos % this.capacity
const writePos = Atomics.load(this.state, STATE.WRITE_POS)
const startPos = readPos; // Loop over each channel
let endPos = startPos + dst[0].length; for (let i = 0; i < this.channels.length; i += 1) {
const channel = this.channels[i]
if (endPos > writePos) { if (startIndex < endIndex) {
endPos = writePos // One continuous range to copy.
if (endPos <= startPos) { const full = channel.subarray(startIndex, endIndex)
// Nothing to read
return 0 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,
})
// 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,
})
} }
}
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
}
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)
output.set(first)
output.set(second, first.length)
}
}
Atomics.store(this.state, STATE.READ_POS, endPos)
return endPos - startPos
} }
size() { Atomics.store(this.state, STATE.WRITE_POS, endPos)
// 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 endPos - startPos
}
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
if (endPos > writePos) {
endPos = writePos
if (endPos <= startPos) {
// Nothing to read
return 0
}
} }
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
}
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)
output.set(first)
output.set(second, first.length)
}
}
Atomics.store(this.state, STATE.READ_POS, endPos)
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)
return writePos - readPos
}
} }

View File

@ -1,98 +1,101 @@
import * as Message from "./message"; import * as Message from "./message"
export default class Video { export default class Video {
canvas: OffscreenCanvas; canvas: OffscreenCanvas
queue: Array<VideoFrame>; queue: Array<VideoFrame>
render: number; // non-zero if requestAnimationFrame has been called render: number // non-zero if requestAnimationFrame has been called
sync?: number; // the wall clock value for timestamp 0, in microseconds sync?: number // the wall clock value for timestamp 0, in microseconds
last?: number; // the timestamp of the last rendered frame, in microseconds last?: number // the timestamp of the last rendered frame, in microseconds
constructor(config: Message.Config) { constructor(config: Message.Config) {
this.canvas = config.canvas; this.canvas = config.canvas
this.queue = []; 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) { // Insert the frame into the queue sorted by timestamp.
// Drop any old frames if (
if (this.last && frame.timestamp <= this.last) { this.queue.length > 0 &&
frame.close() this.queue[this.queue.length - 1].timestamp <= frame.timestamp
return ) {
} // 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. while (low < high) {
if (this.queue.length > 0 && this.queue[this.queue.length - 1].timestamp <= frame.timestamp) { const mid = (low + high) >>> 1
// Fast path because we normally append to the end. if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1
this.queue.push(frame) else high = mid
} else { }
// Do a full binary search
let low = 0
let high = this.queue.length;
while (low < high) { this.queue.splice(low, 0, frame)
const mid = (low + high) >>> 1; }
if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1; }
else high = mid;
}
this.queue.splice(low, 0, frame) 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))
}
drawOnce(now: number) {
// Convert to microseconds
now *= 1000
if (!this.queue.length) {
return
} }
draw(now: number) { let frame = this.queue[0]
// Draw and then queue up the next draw call.
this.drawOnce(now);
// Queue up the new draw frame. if (!this.sync) {
this.render = self.requestAnimationFrame(this.draw.bind(this)) this.sync = now - frame.timestamp
} }
drawOnce(now: number) { // Determine the target timestamp.
// Convert to microseconds const target = now - this.sync
now *= 1000;
if (!this.queue.length) { if (frame.timestamp >= target) {
return // nothing to render yet, wait for the next animation frame
} return
let frame = this.queue[0];
if (!this.sync) {
this.sync = now - frame.timestamp;
}
// Determine the target timestamp.
const target = now - this.sync
if (frame.timestamp >= target) {
// nothing to render yet, wait for the next animation frame
return
}
this.queue.shift();
// 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()!;
}
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()
} }
play(_play: Message.Play) { this.queue.shift()
// Queue up to render the next frame.
if (!this.render) { // Check if we should skip some frames
this.render = self.requestAnimationFrame(this.draw.bind(this)) while (this.queue.length) {
} const next = this.queue[0]
if (next.timestamp > target) break
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
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))
}
}
} }

View File

@ -2,24 +2,23 @@ import Renderer from "./renderer"
import Decoder from "./decoder" import Decoder from "./decoder"
import * as Message from "./message" import * as Message from "./message"
let decoder: Decoder; let decoder: Decoder
let renderer: Renderer; let renderer: Renderer
self.addEventListener('message', async (e: MessageEvent) => { self.addEventListener("message", async (e: MessageEvent) => {
if (e.data.config) { if (e.data.config) {
const config = e.data.config as Message.Config const config = e.data.config as Message.Config
renderer = new Renderer(config) renderer = new Renderer(config)
decoder = new Decoder(renderer) decoder = new Decoder(renderer)
} else if (e.data.init) { } else if (e.data.init) {
const init = e.data.init as Message.Init const init = e.data.init as Message.Init
await decoder.receiveInit(init) await decoder.receiveInit(init)
} else if (e.data.segment) { } else if (e.data.segment) {
const segment = e.data.segment as Message.Segment const segment = e.data.segment as Message.Segment
await decoder.receiveSegment(segment) await decoder.receiveSegment(segment)
} else if (e.data.play) { } else if (e.data.play) {
const play = e.data.play as Message.Play const play = e.data.play as Message.Play
await renderer.play(play) await renderer.play(play)
} }
}) })

View File

@ -7,47 +7,51 @@ import * as Message from "./message"
import { Ring } from "./ring" import { Ring } from "./ring"
class Renderer extends AudioWorkletProcessor { class Renderer extends AudioWorkletProcessor {
ring?: Ring; ring?: Ring
base: number; base: number
constructor(_params: AudioWorkletNodeOptions) { constructor(_params: AudioWorkletNodeOptions) {
// The super constructor call is required. // The super constructor call is required.
super(); super()
this.base = 0 this.base = 0
this.port.onmessage = this.onMessage.bind(this) this.port.onmessage = this.onMessage.bind(this)
}
onMessage(e: MessageEvent) {
if (e.data.play) {
this.onPlay(e.data.play)
}
}
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<string, Float32Array>
): boolean {
if (!this.ring) {
// Paused
return true
} }
onMessage(e: MessageEvent) { if (inputs.length != 1 && outputs.length != 1) {
if (e.data.play) { throw new Error("only a single track is supported")
this.onPlay(e.data.play)
}
} }
onPlay(play: Message.Play) { const output = outputs[0]
this.ring = new Ring(play.buffer)
const size = this.ring.read(output)
if (size < output.length) {
// TODO trigger rebuffering event
} }
// Inputs and outputs in groups of 128 samples. return true
process(inputs: Float32Array[][], outputs: Float32Array[][], _parameters: Record<string, Float32Array>): boolean { }
if (!this.ring) {
// Paused
return true
}
if (inputs.length != 1 && outputs.length != 1) {
throw new Error("only a single track is supported")
}
const output = outputs[0]
const size = this.ring.read(output)
if (size < output.length) {
// TODO trigger rebuffering event
}
return true;
}
} }
registerProcessor("renderer", Renderer); registerProcessor("renderer", Renderer)

View File

@ -1,196 +1,210 @@
// Reader wraps a stream and provides convience methods for reading pieces from a stream // Reader wraps a stream and provides convience methods for reading pieces from a stream
export default class Reader { export default class Reader {
reader: ReadableStream; reader: ReadableStream
buffer: Uint8Array; buffer: Uint8Array
constructor(reader: ReadableStream, buffer: Uint8Array = new Uint8Array(0)) { constructor(reader: ReadableStream, buffer: Uint8Array = new Uint8Array(0)) {
this.reader = reader this.reader = reader
this.buffer = buffer this.buffer = buffer
} }
// Returns any number of bytes // Returns any number of bytes
async read(): Promise<Uint8Array | undefined> { async read(): Promise<Uint8Array | undefined> {
if (this.buffer.byteLength) {
const buffer = this.buffer
this.buffer = new Uint8Array()
return buffer
}
if (this.buffer.byteLength) { const r = this.reader.getReader()
const buffer = this.buffer; const result = await r.read()
this.buffer = new Uint8Array()
return buffer
}
const r = this.reader.getReader() r.releaseLock()
const result = await r.read()
r.releaseLock() return result.value
}
return result.value async readAll(): Promise<Uint8Array> {
} const r = this.reader.getReader()
async readAll(): Promise<Uint8Array> { for (;;) {
const r = this.reader.getReader() const result = await r.read()
if (result.done) {
break
}
for (;;) { const buffer = new Uint8Array(result.value)
const result = await r.read()
if (result.done) {
break
}
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) { const result = this.buffer
this.buffer = buffer this.buffer = new Uint8Array()
} 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 r.releaseLock()
this.buffer = new Uint8Array()
r.releaseLock() return result
}
return result async bytes(size: number): Promise<Uint8Array> {
} const r = this.reader.getReader()
async bytes(size: number): Promise<Uint8Array> { while (this.buffer.byteLength < size) {
const r = this.reader.getReader() const result = await r.read()
if (result.done) {
throw "short buffer"
}
while (this.buffer.byteLength < size) { const buffer = new Uint8Array(result.value)
const result = await r.read()
if (result.done) {
throw "short buffer"
}
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) { const result = new Uint8Array(
this.buffer = buffer this.buffer.buffer,
} else { this.buffer.byteOffset,
const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) size
temp.set(this.buffer) )
temp.set(buffer, this.buffer.byteLength) this.buffer = new Uint8Array(
this.buffer = temp this.buffer.buffer,
} this.buffer.byteOffset + size
} )
const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) r.releaseLock()
this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size)
r.releaseLock() return result
}
return result async peek(size: number): Promise<Uint8Array> {
} const r = this.reader.getReader()
async peek(size: number): Promise<Uint8Array> { while (this.buffer.byteLength < size) {
const r = this.reader.getReader() const result = await r.read()
if (result.done) {
throw "short buffer"
}
while (this.buffer.byteLength < size) { const buffer = new Uint8Array(result.value)
const result = await r.read()
if (result.done) {
throw "short buffer"
}
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) { const result = new Uint8Array(
this.buffer = buffer this.buffer.buffer,
} else { this.buffer.byteOffset,
const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) size
temp.set(this.buffer) )
temp.set(buffer, this.buffer.byteLength)
this.buffer = temp
}
}
const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) r.releaseLock()
r.releaseLock() return result
}
return result async view(size: number): Promise<DataView> {
} const buf = await this.bytes(size)
return new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
}
async view(size: number): Promise<DataView> { async uint8(): Promise<number> {
const buf = await this.bytes(size) const view = await this.view(1)
return new DataView(buf.buffer, buf.byteOffset, buf.byteLength) return view.getUint8(0)
} }
async uint8(): Promise<number> { async uint16(): Promise<number> {
const view = await this.view(1) const view = await this.view(2)
return view.getUint8(0) return view.getUint16(0)
} }
async uint16(): Promise<number> { async uint32(): Promise<number> {
const view = await this.view(2) const view = await this.view(4)
return view.getUint16(0) return view.getUint32(0)
} }
async uint32(): Promise<number> { // Returns a Number using 52-bits, the max Javascript can use for integer math
const view = await this.view(4) async uint52(): Promise<number> {
return view.getUint32(0) 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 return Number(v)
async uint52(): Promise<number> { }
const v = await this.uint64()
if (v > Number.MAX_SAFE_INTEGER) {
throw "overflow"
}
return Number(v) // Returns a Number using 52-bits, the max Javascript can use for integer math
} async vint52(): Promise<number> {
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 return Number(v)
async vint52(): Promise<number> { }
const v = await this.vint64()
if (v > Number.MAX_SAFE_INTEGER) {
throw "overflow"
}
return Number(v) // NOTE: Returns a BigInt instead of a Number
} async uint64(): Promise<bigint> {
const view = await this.view(8)
return view.getBigUint64(0)
}
// NOTE: Returns a BigInt instead of a Number // NOTE: Returns a BigInt instead of a Number
async uint64(): Promise<bigint> { async vint64(): Promise<bigint> {
const view = await this.view(8) const peek = await this.peek(1)
return view.getBigUint64(0) 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 switch (size) {
async vint64(): Promise<bigint> { case 0: {
const peek = await this.peek(1) const v = await this.uint8()
const first = new DataView(peek.buffer, peek.byteOffset, peek.byteLength).getUint8(0) return BigInt(v) & 0x3fn
const size = (first & 0xc0) >> 6 }
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) { async done(): Promise<boolean> {
case 0: { try {
const v = await this.uint8() await this.peek(1)
return BigInt(v) & 0x3fn return false
} } catch (err) {
case 1: { return true // Assume EOF
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<boolean> {
try {
await this.peek(1)
return false
} catch (err) {
return true // Assume EOF
}
}
} }

View File

@ -1,100 +1,100 @@
// Writer wraps a stream and writes chunks of data // Writer wraps a stream and writes chunks of data
export default class Writer { export default class Writer {
buffer: ArrayBuffer; buffer: ArrayBuffer
writer: WritableStreamDefaultWriter; writer: WritableStreamDefaultWriter
constructor(stream: WritableStream) { constructor(stream: WritableStream) {
this.buffer = new ArrayBuffer(8) this.buffer = new ArrayBuffer(8)
this.writer = stream.getWriter() this.writer = stream.getWriter()
} }
release() { release() {
this.writer.releaseLock() this.writer.releaseLock()
} }
async close() { async close() {
return this.writer.close() return this.writer.close()
} }
async uint8(v: number) { async uint8(v: number) {
const view = new DataView(this.buffer, 0, 1) const view = new DataView(this.buffer, 0, 1)
view.setUint8(0, v) view.setUint8(0, v)
return this.writer.write(view) return this.writer.write(view)
} }
async uint16(v: number) { async uint16(v: number) {
const view = new DataView(this.buffer, 0, 2) const view = new DataView(this.buffer, 0, 2)
view.setUint16(0, v) view.setUint16(0, v)
return this.writer.write(view) return this.writer.write(view)
} }
async uint24(v: number) { async uint24(v: number) {
const v1 = (v >> 16) & 0xff const v1 = (v >> 16) & 0xff
const v2 = (v >> 8) & 0xff const v2 = (v >> 8) & 0xff
const v3 = (v) & 0xff const v3 = v & 0xff
const view = new DataView(this.buffer, 0, 3) const view = new DataView(this.buffer, 0, 3)
view.setUint8(0, v1) view.setUint8(0, v1)
view.setUint8(1, v2) view.setUint8(1, v2)
view.setUint8(2, v3) view.setUint8(2, v3)
return this.writer.write(view) return this.writer.write(view)
} }
async uint32(v: number) { async uint32(v: number) {
const view = new DataView(this.buffer, 0, 4) const view = new DataView(this.buffer, 0, 4)
view.setUint32(0, v) view.setUint32(0, v)
return this.writer.write(view) return this.writer.write(view)
} }
async uint52(v: number) { async uint52(v: number) {
if (v > Number.MAX_SAFE_INTEGER) { if (v > Number.MAX_SAFE_INTEGER) {
throw "value too large" throw "value too large"
} }
this.uint64(BigInt(v)) this.uint64(BigInt(v))
} }
async vint52(v: number) { async vint52(v: number) {
if (v > Number.MAX_SAFE_INTEGER) { if (v > Number.MAX_SAFE_INTEGER) {
throw "value too large" throw "value too large"
} }
if (v < (1 << 6)) { if (v < 1 << 6) {
return this.uint8(v) return this.uint8(v)
} else if (v < (1 << 14)) { } else if (v < 1 << 14) {
return this.uint16(v|0x4000) return this.uint16(v | 0x4000)
} else if (v < (1 << 30)) { } else if (v < 1 << 30) {
return this.uint32(v|0x80000000) return this.uint32(v | 0x80000000)
} else { } else {
return this.uint64(BigInt(v) | 0xc000000000000000n) return this.uint64(BigInt(v) | 0xc000000000000000n)
} }
} }
async uint64(v: bigint) { async uint64(v: bigint) {
const view = new DataView(this.buffer, 0, 8) const view = new DataView(this.buffer, 0, 8)
view.setBigUint64(0, v) view.setBigUint64(0, v)
return this.writer.write(view) return this.writer.write(view)
} }
async vint64(v: bigint) { async vint64(v: bigint) {
if (v < (1 << 6)) { if (v < 1 << 6) {
return this.uint8(Number(v)) return this.uint8(Number(v))
} else if (v < (1 << 14)) { } else if (v < 1 << 14) {
return this.uint16(Number(v)|0x4000) return this.uint16(Number(v) | 0x4000)
} else if (v < (1 << 30)) { } else if (v < 1 << 30) {
return this.uint32(Number(v)|0x80000000) return this.uint32(Number(v) | 0x80000000)
} else { } else {
return this.uint64(v | 0xc000000000000000n) return this.uint64(v | 0xc000000000000000n)
} }
} }
async bytes(buffer: ArrayBuffer) { async bytes(buffer: ArrayBuffer) {
return this.writer.write(buffer) return this.writer.write(buffer)
} }
async string(str: string) { async string(str: string) {
const data = new TextEncoder().encode(str) const data = new TextEncoder().encode(str)
return this.writer.write(data) return this.writer.write(data)
} }
} }

View File

@ -2,95 +2,95 @@ import * as Stream from "../stream"
import * as Interface from "./interface" import * as Interface from "./interface"
export interface Config { export interface Config {
url: string; url: string
fingerprint?: WebTransportHash; // the certificate fingerprint, temporarily needed for local development fingerprint?: WebTransportHash // the certificate fingerprint, temporarily needed for local development
} }
export default class Transport { export default class Transport {
quic: Promise<WebTransport>; quic: Promise<WebTransport>
api: Promise<WritableStream>; api: Promise<WritableStream>
callback?: Interface.Callback; callback?: Interface.Callback
constructor(config: Config) { constructor(config: Config) {
this.quic = this.connect(config) this.quic = this.connect(config)
// Create a unidirectional stream for all of our messages // Create a unidirectional stream for all of our messages
this.api = this.quic.then((q) => { this.api = this.quic.then((q) => {
return q.createUnidirectionalStream() return q.createUnidirectionalStream()
}) })
// async functions // async functions
this.receiveStreams() this.receiveStreams()
} }
async close() { async close() {
(await this.quic).close() ;(await this.quic).close()
} }
// Helper function to make creating a promise easier // Helper function to make creating a promise easier
private async connect(config: Config): Promise<WebTransport> { private async connect(config: Config): Promise<WebTransport> {
const options: WebTransportOptions = {}; const options: WebTransportOptions = {}
if (config.fingerprint) { if (config.fingerprint) {
options.serverCertificateHashes = [ config.fingerprint ] options.serverCertificateHashes = [config.fingerprint]
} }
const quic = new WebTransport(config.url, options) const quic = new WebTransport(config.url, options)
await quic.ready await quic.ready
return quic return quic
} }
async sendMessage(msg: any) { async sendMessage(msg: any) {
const payload = JSON.stringify(msg) const payload = JSON.stringify(msg)
const size = payload.length + 8 const size = payload.length + 8
const stream = await this.api const stream = await this.api
const writer = new Stream.Writer(stream) const writer = new Stream.Writer(stream)
await writer.uint32(size) await writer.uint32(size)
await writer.string("warp") await writer.string("warp")
await writer.string(payload) await writer.string(payload)
writer.release() writer.release()
} }
async receiveStreams() { async receiveStreams() {
const q = await this.quic const q = await this.quic
const streams = q.incomingUnidirectionalStreams.getReader() const streams = q.incomingUnidirectionalStreams.getReader()
for (;;) { for (;;) {
const result = await streams.read() const result = await streams.read()
if (result.done) break if (result.done) break
const stream = result.value const stream = result.value
this.handleStream(stream) // don't await this.handleStream(stream) // don't await
} }
} }
async handleStream(stream: ReadableStream) { async handleStream(stream: ReadableStream) {
const r = new Stream.Reader(stream) const r = new Stream.Reader(stream)
while (!await r.done()) { while (!(await r.done())) {
const size = await r.uint32(); const size = await r.uint32()
const typ = new TextDecoder('utf-8').decode(await r.bytes(4)); const typ = new TextDecoder("utf-8").decode(await r.bytes(4))
if (typ != "warp") throw "expected warp atom" if (typ != "warp") throw "expected warp atom"
if (size < 8) throw "atom too small" if (size < 8) throw "atom too small"
const payload = new TextDecoder('utf-8').decode(await r.bytes(size - 8)); const payload = new TextDecoder("utf-8").decode(await r.bytes(size - 8))
const msg = JSON.parse(payload) const msg = JSON.parse(payload)
if (msg.init) { if (msg.init) {
return this.callback?.onInit({ return this.callback?.onInit({
buffer: r.buffer, buffer: r.buffer,
reader: r.reader, reader: r.reader,
}) })
} else if (msg.segment) { } else if (msg.segment) {
return this.callback?.onSegment({ return this.callback?.onSegment({
buffer: r.buffer, buffer: r.buffer,
reader: r.reader, reader: r.reader,
}) })
} else { } else {
console.warn("unknown message", msg); console.warn("unknown message", msg)
} }
} }
} }
} }

View File

@ -1,14 +1,14 @@
export interface Callback { export interface Callback {
onInit(init: Init): any onInit(init: Init): any
onSegment(segment: Segment): any onSegment(segment: Segment): any
} }
export interface Init { export interface Init {
buffer: Uint8Array; // unread buffered data buffer: Uint8Array // unread buffered data
reader: ReadableStream; // unread unbuffered data reader: ReadableStream // unread unbuffered data
} }
export interface Segment { export interface Segment {
buffer: Uint8Array; // unread buffered data buffer: Uint8Array // unread buffered data
reader: ReadableStream; // unread unbuffered data reader: ReadableStream // unread unbuffered data
} }

View File

@ -3,5 +3,5 @@ export type Init = any
export type Segment = any export type Segment = any
export interface Debug { export interface Debug {
max_bitrate: number max_bitrate: number
} }

View File

@ -8,77 +8,77 @@ declare module "webtransport"
*/ */
interface WebTransportDatagramDuplexStream { interface WebTransportDatagramDuplexStream {
readonly readable: ReadableStream; readonly readable: ReadableStream
readonly writable: WritableStream; readonly writable: WritableStream
readonly maxDatagramSize: number; readonly maxDatagramSize: number
incomingMaxAge: number; incomingMaxAge: number
outgoingMaxAge: number; outgoingMaxAge: number
incomingHighWaterMark: number; incomingHighWaterMark: number
outgoingHighWaterMark: number; outgoingHighWaterMark: number
} }
interface WebTransport { interface WebTransport {
getStats(): Promise<WebTransportStats>; getStats(): Promise<WebTransportStats>
readonly ready: Promise<undefined>; readonly ready: Promise<undefined>
readonly closed: Promise<WebTransportCloseInfo>; readonly closed: Promise<WebTransportCloseInfo>
close(closeInfo?: WebTransportCloseInfo): undefined; close(closeInfo?: WebTransportCloseInfo): undefined
readonly datagrams: WebTransportDatagramDuplexStream; readonly datagrams: WebTransportDatagramDuplexStream
createBidirectionalStream(): Promise<WebTransportBidirectionalStream>; createBidirectionalStream(): Promise<WebTransportBidirectionalStream>
readonly incomingBidirectionalStreams: ReadableStream; readonly incomingBidirectionalStreams: ReadableStream
createUnidirectionalStream(): Promise<WritableStream>; createUnidirectionalStream(): Promise<WritableStream>
readonly incomingUnidirectionalStreams: ReadableStream; readonly incomingUnidirectionalStreams: ReadableStream
} }
declare const WebTransport: { declare const WebTransport: {
prototype: WebTransport; prototype: WebTransport
new(url: string, options?: WebTransportOptions): WebTransport; new (url: string, options?: WebTransportOptions): WebTransport
}; }
interface WebTransportHash { interface WebTransportHash {
algorithm?: string; algorithm?: string
value?: BufferSource; value?: BufferSource
} }
interface WebTransportOptions { interface WebTransportOptions {
allowPooling?: boolean; allowPooling?: boolean
serverCertificateHashes?: Array<WebTransportHash>; serverCertificateHashes?: Array<WebTransportHash>
} }
interface WebTransportCloseInfo { interface WebTransportCloseInfo {
closeCode?: number; closeCode?: number
reason?: string; reason?: string
} }
interface WebTransportStats { interface WebTransportStats {
timestamp?: DOMHighResTimeStamp; timestamp?: DOMHighResTimeStamp
bytesSent?: number; bytesSent?: number
packetsSent?: number; packetsSent?: number
numOutgoingStreamsCreated?: number; numOutgoingStreamsCreated?: number
numIncomingStreamsCreated?: number; numIncomingStreamsCreated?: number
bytesReceived?: number; bytesReceived?: number
packetsReceived?: number; packetsReceived?: number
minRtt?: DOMHighResTimeStamp; minRtt?: DOMHighResTimeStamp
numReceivedDatagramsDropped?: number; numReceivedDatagramsDropped?: number
} }
interface WebTransportBidirectionalStream { interface WebTransportBidirectionalStream {
readonly readable: ReadableStream; readonly readable: ReadableStream
readonly writable: WritableStream; readonly writable: WritableStream
} }
interface WebTransportError extends DOMException { interface WebTransportError extends DOMException {
readonly source: WebTransportErrorSource; readonly source: WebTransportErrorSource
readonly streamErrorCode: number; readonly streamErrorCode: number
} }
declare const WebTransportError: { declare const WebTransportError: {
prototype: WebTransportError; prototype: WebTransportError
new(init?: WebTransportErrorInit): WebTransportError; new (init?: WebTransportErrorInit): WebTransportError
};
interface WebTransportErrorInit {
streamErrorCode?: number;
message?: string;
} }
type WebTransportErrorSource = "stream" | "session"; interface WebTransportErrorInit {
streamErrorCode?: number
message?: string
}
type WebTransportErrorSource = "stream" | "session"

View File

@ -1,16 +1,20 @@
export default class Deferred<T> { export default class Deferred<T> {
promise: Promise<T> promise: Promise<T>
resolve: (value: T | PromiseLike<T>) => void resolve: (value: T | PromiseLike<T>) => void
reject: (value: T | PromiseLike<T>) => void reject: (value: T | PromiseLike<T>) => void
constructor() { constructor() {
// Set initial values so TS stops being annoying. // Set initial values so TS stops being annoying.
this.resolve = (_value: T | PromiseLike<T>) => { /* noop */ }; this.resolve = (_value: T | PromiseLike<T>) => {
this.reject = (_value: T | PromiseLike<T>) => { /* noop */ }; /* noop */
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
} }
this.reject = (_value: T | PromiseLike<T>) => {
/* noop */
}
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
} }
}

View File

@ -1,11 +1,9 @@
{ {
"include": [ "include": ["src/**/*"],
"src/**/*"
],
"compilerOptions": { "compilerOptions": {
"target": "es2022", "target": "es2022",
"module": "es2022", "module": "es2022",
"moduleResolution": "node", "moduleResolution": "node",
"strict": true, "strict": true
} }
} }

View File

@ -101,7 +101,7 @@
"@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@1.4.14": "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.14" version "1.4.14"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
@ -131,6 +131,31 @@
resolved "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.5.2.tgz" resolved "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.5.2.tgz"
integrity sha512-+F8ioQIUN68B4UFiIBYu0QQvgb9FmlKw2ctQMSBfW2QBrZIxz9vD9jCGqTCPqZBRbPHAS/vG1zSXnKqnS2ch/A== integrity sha512-+F8ioQIUN68B4UFiIBYu0QQvgb9FmlKw2ctQMSBfW2QBrZIxz9vD9jCGqTCPqZBRbPHAS/vG1zSXnKqnS2ch/A==
"@lmdb/lmdb-darwin-x64@2.5.2":
version "2.5.2"
resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.5.2.tgz#89d8390041bce6bab24a82a20392be22faf54ffc"
integrity sha512-KvPH56KRLLx4KSfKBx0m1r7GGGUMXm0jrKmNE7plbHlesZMuPJICtn07HYgQhj1LNsK7Yqwuvnqh1QxhJnF1EA==
"@lmdb/lmdb-linux-arm64@2.5.2":
version "2.5.2"
resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.5.2.tgz#14fe4c96c2bb1285f93797f45915fa35ee047268"
integrity sha512-aLl89VHL/wjhievEOlPocoefUyWdvzVrcQ/MHQYZm2JfV1jUsrbr/ZfkPPUFvZBf+VSE+Q0clWs9l29PCX1hTQ==
"@lmdb/lmdb-linux-arm@2.5.2":
version "2.5.2"
resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.5.2.tgz#05bde4573ab10cf21827339fe687148f2590cfa1"
integrity sha512-5kQAP21hAkfW5Bl+e0P57dV4dGYnkNIpR7f/GAh6QHlgXx+vp/teVj4PGRZaKAvt0GX6++N6hF8NnGElLDuIDw==
"@lmdb/lmdb-linux-x64@2.5.2":
version "2.5.2"
resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.5.2.tgz#d2f85afd857d2c33d2caa5b057944574edafcfee"
integrity sha512-xUdUfwDJLGjOUPH3BuPBt0NlIrR7f/QHKgu3GZIXswMMIihAekj2i97oI0iWG5Bok/b+OBjHPfa8IU9velnP/Q==
"@lmdb/lmdb-win32-x64@2.5.2":
version "2.5.2"
resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.5.2.tgz#28f643fbc0bec30b07fbe95b137879b6b4d1c9c5"
integrity sha512-zrBczSbXKxEyK2ijtbRdICDygRqWSRPpZMN5dD1T8VMEW5RIhIbwFWw2phDRXuBQdVDpSjalCIUMWMV2h3JaZA==
"@mischnic/json-sourcemap@^0.1.0": "@mischnic/json-sourcemap@^0.1.0":
version "0.1.0" version "0.1.0"
resolved "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz" resolved "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz"
@ -145,6 +170,31 @@
resolved "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz" resolved "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz"
integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ== integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==
"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3"
integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==
"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367"
integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==
"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399"
integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==
"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz#091b1218b66c341f532611477ef89e83f25fae4f"
integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==
"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407"
integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@ -153,7 +203,7 @@
"@nodelib/fs.stat" "2.0.5" "@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9" run-parallel "^1.1.9"
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5" version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@ -238,7 +288,7 @@
"@parcel/transformer-react-refresh-wrap" "2.8.3" "@parcel/transformer-react-refresh-wrap" "2.8.3"
"@parcel/transformer-svg" "2.8.3" "@parcel/transformer-svg" "2.8.3"
"@parcel/core@^2.8.3", "@parcel/core@2.8.3": "@parcel/core@2.8.3":
version "2.8.3" version "2.8.3"
resolved "https://registry.npmjs.org/@parcel/core/-/core-2.8.3.tgz" resolved "https://registry.npmjs.org/@parcel/core/-/core-2.8.3.tgz"
integrity sha512-Euf/un4ZAiClnlUXqPB9phQlKbveU+2CotZv7m7i+qkgvFn5nAGnrV4h1OzQU42j9dpgOxWi7AttUDMrvkbhCQ== integrity sha512-Euf/un4ZAiClnlUXqPB9phQlKbveU+2CotZv7m7i+qkgvFn5nAGnrV4h1OzQU42j9dpgOxWi7AttUDMrvkbhCQ==
@ -806,7 +856,7 @@
semver "^7.3.7" semver "^7.3.7"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.59.7": "@typescript-eslint/parser@^5.59.7":
version "5.59.7" version "5.59.7"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.7.tgz" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.7.tgz"
integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ== integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ==
@ -884,7 +934,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.5.0, acorn@^8.8.0: acorn@^8.5.0, acorn@^8.8.0:
version "8.8.2" version "8.8.2"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
@ -960,7 +1010,7 @@ braces@^3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
browserslist@^4.6.6, "browserslist@>= 4.21.0": browserslist@^4.6.6:
version "4.21.5" version "4.21.5"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz"
integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==
@ -1026,16 +1076,16 @@ color-convert@^2.0.1:
dependencies: dependencies:
color-name "~1.1.4" color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-name@1.1.3: color-name@1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
commander@^2.20.0: commander@^2.20.0:
version "2.20.3" version "2.20.3"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
@ -1102,21 +1152,7 @@ csso@^4.2.0:
dependencies: dependencies:
css-tree "^1.1.2" css-tree "^1.1.2"
debug@^4.1.1: debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
debug@^4.3.2:
version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
debug@^4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -1224,6 +1260,11 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-prettier@^8.8.0:
version "8.8.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
eslint-scope@^5.1.1: eslint-scope@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
@ -1245,7 +1286,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint@*, "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.41.0: eslint@^8.41.0:
version "8.41.0" version "8.41.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz" resolved "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz"
integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q== integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
@ -1318,12 +1359,7 @@ estraverse@^4.1.1:
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0: estraverse@^5.1.0, estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estraverse@^5.2.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
@ -1608,6 +1644,41 @@ lightningcss-darwin-arm64@1.19.0:
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.19.0.tgz" resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.19.0.tgz"
integrity sha512-wIJmFtYX0rXHsXHSr4+sC5clwblEMji7HHQ4Ub1/CznVRxtCFha6JIt5JZaNf8vQrfdZnBxLLC6R8pC818jXqg== integrity sha512-wIJmFtYX0rXHsXHSr4+sC5clwblEMji7HHQ4Ub1/CznVRxtCFha6JIt5JZaNf8vQrfdZnBxLLC6R8pC818jXqg==
lightningcss-darwin-x64@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.19.0.tgz#c867308b88859ba61a2c46c82b1ca52ff73a1bd0"
integrity sha512-Lif1wD6P4poaw9c/4Uh2z+gmrWhw/HtXFoeZ3bEsv6Ia4tt8rOJBdkfVaUJ6VXmpKHALve+iTyP2+50xY1wKPw==
lightningcss-linux-arm-gnueabihf@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.19.0.tgz#0f921dc45f2e5c3aea70fab98844ac0e5f2f81be"
integrity sha512-P15VXY5682mTXaiDtbnLYQflc8BYb774j2R84FgDLJTN6Qp0ZjWEFyN1SPqyfTj2B2TFjRHRUvQSSZ7qN4Weig==
lightningcss-linux-arm64-gnu@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.19.0.tgz#027f9df9c7f4ffa127c37a71726245a5794d7ba2"
integrity sha512-zwXRjWqpev8wqO0sv0M1aM1PpjHz6RVIsBcxKszIG83Befuh4yNysjgHVplF9RTU7eozGe3Ts7r6we1+Qkqsww==
lightningcss-linux-arm64-musl@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.19.0.tgz#85ea987da868524eac6db94f8e1eaa23d0b688a3"
integrity sha512-vSCKO7SDnZaFN9zEloKSZM5/kC5gbzUjoJQ43BvUpyTFUX7ACs/mDfl2Eq6fdz2+uWhUh7vf92c4EaaP4udEtA==
lightningcss-linux-x64-gnu@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.19.0.tgz#02bec89579ab4153dccc0def755d1fd9e3ee7f3c"
integrity sha512-0AFQKvVzXf9byrXUq9z0anMGLdZJS+XSDqidyijI5njIwj6MdbvX2UZK/c4FfNmeRa2N/8ngTffoIuOUit5eIQ==
lightningcss-linux-x64-musl@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.19.0.tgz#e36a5df8193ae961d22974635e4c100a1823bb8c"
integrity sha512-SJoM8CLPt6ECCgSuWe+g0qo8dqQYVcPiW2s19dxkmSI5+Uu1GIRzyKA0b7QqmEXolA+oSJhQqCmJpzjY4CuZAg==
lightningcss-win32-x64-msvc@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.19.0.tgz#0854dbd153035eca1396e2227c708ad43655a61c"
integrity sha512-C+VuUTeSUOAaBZZOPT7Etn/agx/MatzJzGRkeV+zEABmPuntv1zihncsi+AyGmjkkzq3wVedEy7h0/4S84mUtg==
lightningcss@^1.16.1: lightningcss@^1.16.1:
version "1.19.0" version "1.19.0"
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.19.0.tgz" resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.19.0.tgz"
@ -1918,6 +1989,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@^2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
punycode@^2.1.0: punycode@^2.1.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz"
@ -2014,7 +2090,7 @@ source-map@^0.6.0, source-map@^0.6.1:
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
srcset@4, srcset@4.0.0: srcset@4:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz" resolved "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz"
integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==
@ -2050,7 +2126,7 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
svgo@^2.4.0, svgo@^2.8.0: svgo@^2.4.0:
version "2.8.0" version "2.8.0"
resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz" resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz"
integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
@ -2068,7 +2144,7 @@ term-size@^2.2.1:
resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz" resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz"
integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
terser@^5.10.0, terser@^5.2.0: terser@^5.2.0:
version "5.16.8" version "5.16.8"
resolved "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz" resolved "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz"
integrity sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA== integrity sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==
@ -2124,7 +2200,7 @@ type-fest@^0.20.2:
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
typescript@^5.0.4, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=3.0.0: typescript@^5.0.4:
version "5.0.4" version "5.0.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz" resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz"
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==