More linting.

This commit is contained in:
Luke Curley 2023-05-22 21:43:48 -07:00
parent f05bd5a0ac
commit cc8792da09
35 changed files with 1179 additions and 5246 deletions

View File

@ -1,8 +1,6 @@
name: server
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
@ -15,9 +13,23 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: build
working-directory: server
run: cargo build --verbose
- name: toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy, rustfmt
- name: test
working-directory: server
run: cargo test --verbose
- name: fmt
uses: mbrobbel/rustfmt-check@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

View File

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

View File

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

2
web/.prettierignore Normal file
View File

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

5
web/.prettierrc.json Normal file
View File

@ -0,0 +1,5 @@
{
"useTabs": true,
"tabWidth": 4,
"semi": false
}

View File

@ -1,7 +1,7 @@
module.exports = function (app) {
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});
};
res.setHeader("Cross-Origin-Opener-Policy", "same-origin")
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp")
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",
"build": "parcel build",
"check": "tsc --noEmit",
"lint": "eslint ."
"lint": "eslint .",
"fmt": "prettier --write ."
},
"devDependencies": {
"@parcel/transformer-inline-string": "2.8.3",
@ -15,7 +16,9 @@
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"parcel": "^2.8.0",
"prettier": "^2.8.8",
"typescript": "^5.0.4"
},
"dependencies": {

View File

@ -6,19 +6,19 @@ export class Encoder {
video: VideoEncoder
constructor() {
this.container = new MP4.ISOFile();
this.container = new MP4.ISOFile()
this.audio = new AudioEncoder({
output: this.onAudio.bind(this),
error: console.warn,
});
})
this.video = new VideoEncoder({
output: this.onVideo.bind(this),
error: console.warn,
});
})
this.container.init();
this.container.init()
this.audio.configure({
codec: "mp4a.40.2",
@ -43,7 +43,7 @@ export class Encoder {
onAudio(frame: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata) {
const config = metadata.decoderConfig!
const track_id = 1;
const track_id = 1
if (!this.container.getTrackById(track_id)) {
this.container.addTrack({
@ -56,25 +56,25 @@ export class Encoder {
description: config.description, // TODO verify
// TODO description_boxes?: Box[];
});
})
}
const buffer = new Uint8Array(frame.byteLength);
frame.copyTo(buffer);
const buffer = new Uint8Array(frame.byteLength)
frame.copyTo(buffer)
// TODO cts?
const sample = this.container.addSample(track_id, buffer, {
is_sync: frame.type == "key",
duration: frame.duration!,
dts: frame.timestamp,
});
})
const _stream = this.container.createSingleSampleMoof(sample);
const _stream = this.container.createSingleSampleMoof(sample)
}
onVideo(frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) {
const config = metadata!.decoderConfig!
const track_id = 2;
const track_id = 2
if (!this.container.getTrackById(track_id)) {
this.container.addTrack({
@ -86,19 +86,19 @@ export class Encoder {
description: config.description, // TODO verify
// TODO description_boxes?: Box[];
});
})
}
const buffer = new Uint8Array(frame.byteLength);
frame.copyTo(buffer);
const buffer = new Uint8Array(frame.byteLength)
frame.copyTo(buffer)
// TODO cts?
const sample = this.container.addSample(track_id, buffer, {
is_sync: frame.type == "key",
duration: frame.duration!,
dts: frame.timestamp,
});
})
const _stream = this.container.createSingleSampleMoof(sample);
const _stream = this.container.createSingleSampleMoof(sample)
}
}

View File

@ -1,4 +1,6 @@
html, body, #player {
html,
body,
#player {
width: 100%;
}

View File

@ -1,14 +1,13 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<title>WARP</title>
<link rel="stylesheet" href="index.css">
</head>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<body>
<div id="player">
<div id="screen">
<div id="play"><span>click to play</span></div>
@ -30,6 +29,5 @@
</div>
<script src="index.ts" type="module"></script>
</body>
</body>
</html>

View File

@ -2,12 +2,12 @@ import Player from "./player"
import Transport from "./transport"
// @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.
const fingerprint = [];
const fingerprint = []
for (let c = 0; c < fingerprintHex.length - 1; c += 2) {
fingerprint.push(parseInt(fingerprintHex.substring(c, c + 2), 16));
fingerprint.push(parseInt(fingerprintHex.substring(c, c + 2), 16))
}
const params = new URLSearchParams(window.location.search)
@ -17,9 +17,10 @@ const canvas = document.querySelector<HTMLCanvasElement>("canvas#video")!
const transport = new Transport({
url: url,
fingerprint: { // TODO remove when Chrome accepts the system CA
"algorithm": "sha-256",
"value": new Uint8Array(fingerprint),
fingerprint: {
// TODO remove when Chrome accepts the system CA
algorithm: "sha-256",
value: new Uint8Array(fingerprint),
},
})
@ -34,8 +35,8 @@ const playFunc = (e: Event) => {
player.play()
e.preventDefault()
play.removeEventListener('click', playFunc)
play.removeEventListener("click", playFunc)
play.style.display = "none"
}
play.addEventListener('click', playFunc)
play.addEventListener("click", playFunc)

View File

@ -1,16 +1,16 @@
import * as MP4 from "./index"
export interface Init {
raw: MP4.ArrayBuffer;
info: MP4.Info;
raw: MP4.ArrayBuffer
info: MP4.Info
}
export class InitParser {
mp4box: MP4.File;
offset: number;
mp4box: MP4.File
offset: number
raw: MP4.ArrayBuffer[];
info: Promise<MP4.Info>;
raw: MP4.ArrayBuffer[]
info: Promise<MP4.Info>
constructor() {
this.mp4box = MP4.New()
@ -26,7 +26,7 @@ export class InitParser {
push(data: Uint8Array) {
// 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)
// and for some reason we need to modify the underlying ArrayBuffer with fileStart

View File

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

View File

@ -1,12 +1,12 @@
import * as Message from "./message";
import * as Message from "./message"
import { Ring } from "./ring"
export default class Audio {
ring?: Ring;
queue: Array<AudioData>;
ring?: Ring
queue: Array<AudioData>
render?: number; // non-zero if requestAnimationFrame has been called
last?: number; // the timestamp of the last rendered frame, in microseconds
render?: number // non-zero if requestAnimationFrame has been called
last?: number // the timestamp of the last rendered frame, in microseconds
constructor(_config: Message.Config) {
this.queue = []
@ -20,18 +20,21 @@ export default class Audio {
}
// Insert the frame into the queue sorted by timestamp.
if (this.queue.length > 0 && this.queue[this.queue.length - 1].timestamp <= frame.timestamp) {
if (
this.queue.length > 0 &&
this.queue[this.queue.length - 1].timestamp <= frame.timestamp
) {
// Fast path because we normally append to the end.
this.queue.push(frame)
} else {
// Do a full binary search
let low = 0
let high = this.queue.length;
let high = this.queue.length
while (low < high) {
const mid = (low + high) >>> 1;
if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1;
else high = mid;
const mid = (low + high) >>> 1
if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1
else high = mid
}
this.queue.splice(low, 0, frame)
@ -47,7 +50,7 @@ export default class Audio {
}
while (this.queue.length) {
const frame = this.queue[0];
const frame = this.queue[0]
if (ring.size() + frame.numberOfFrames > ring.capacity) {
// Buffer is full
break
@ -72,7 +75,7 @@ export default class Audio {
const sampleRate = 44100 // TODO dynamic
// Refresh every half buffer
const refresh = play.buffer.capacity / sampleRate * 1000 / 2
const refresh = ((play.buffer.capacity / sampleRate) * 1000) / 2
this.render = setInterval(this.emit.bind(this), refresh)
}
}

View File

@ -1,22 +1,22 @@
import * as Message from "./message";
import * as Message from "./message"
import * as MP4 from "../mp4"
import * as Stream from "../stream"
import Renderer from "./renderer"
export default class Decoder {
init: MP4.InitParser;
decoders: Map<number, AudioDecoder | VideoDecoder>;
renderer: Renderer;
init: MP4.InitParser
decoders: Map<number, AudioDecoder | VideoDecoder>
renderer: Renderer
constructor(renderer: Renderer) {
this.init = new MP4.InitParser();
this.decoders = new Map();
this.renderer = renderer;
this.init = new MP4.InitParser()
this.decoders = new Map()
this.renderer = renderer
}
async receiveInit(msg: Message.Init) {
const stream = new Stream.Reader(msg.reader, msg.buffer);
const stream = new Stream.Reader(msg.reader, msg.buffer)
for (;;) {
const data = await stream.read()
if (!data) break
@ -30,20 +30,20 @@ export default class Decoder {
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();
const input = MP4.New()
input.onSamples = this.onSamples.bind(this);
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.setExtractionOptions(track.id, i, { nbSamples: 1 })
}
input.start();
input.start()
}
// MP4box requires us to reparse the init segment unfortunately
let offset = 0;
let offset = 0
for (const raw of this.init.raw) {
raw.fileStart = offset
@ -53,15 +53,19 @@ export default class Decoder {
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()) {
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 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);
const box = new Uint8Array(atom.byteLength)
box.set(atom)
// and for some reason we need to modify the underlying ArrayBuffer with offset
@ -75,25 +79,29 @@ export default class Decoder {
}
onSamples(track_id: number, track: MP4.Track, samples: MP4.Sample[]) {
let decoder = this.decoders.get(track_id);
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];
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 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)
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,
@ -108,7 +116,7 @@ export default class Decoder {
const audioDecoder = new AudioDecoder({
output: this.renderer.push.bind(this.renderer),
error: console.warn,
});
})
audioDecoder.configure({
codec: track.codec,
@ -126,23 +134,27 @@ export default class 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
const timestamp = (1000 * 1000 * sample.dts) / sample.timescale
const duration = (1000 * 1000 * sample.duration) / sample.timescale
if (isAudioDecoder(decoder)) {
decoder.decode(new EncodedAudioChunk({
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({
decoder.decode(
new EncodedVideoChunk({
type: sample.is_sync ? "key" : "delta",
data: sample.data,
duration: duration,
timestamp: timestamp,
}))
})
)
} else {
throw new Error("unknown decoder type")
}
@ -150,18 +162,22 @@ export default class Decoder {
}
}
function isAudioDecoder(decoder: AudioDecoder | VideoDecoder): decoder is AudioDecoder {
function isAudioDecoder(
decoder: AudioDecoder | VideoDecoder
): decoder is AudioDecoder {
return decoder instanceof AudioDecoder
}
function isVideoDecoder(decoder: AudioDecoder | VideoDecoder): decoder is VideoDecoder {
function isVideoDecoder(
decoder: AudioDecoder | VideoDecoder
): decoder is VideoDecoder {
return decoder instanceof VideoDecoder
}
function isAudioTrack(track: MP4.Track): track is MP4.AudioTrack {
return (track as MP4.AudioTrack).audio !== undefined;
return (track as MP4.AudioTrack).audio !== undefined
}
function isVideoTrack(track: MP4.Track): track is MP4.VideoTrack {
return (track as MP4.VideoTrack).video !== undefined;
return (track as MP4.VideoTrack).video !== undefined
}

View File

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

View File

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

View File

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

View File

@ -3,24 +3,28 @@
enum STATE {
READ_POS = 0, // The current read position
WRITE_POS, // The current write position
LENGTH // Clever way of saving the total number of enums values.
LENGTH, // Clever way of saving the total number of enums values.
}
// No prototype to make this easier to send via postMessage
export class Buffer {
state: SharedArrayBuffer;
state: SharedArrayBuffer
channels: SharedArrayBuffer[];
capacity: number;
channels: SharedArrayBuffer[]
capacity: number
constructor(channels: number, capacity: number) {
// Store the current state in a separate ring buffer.
this.state = new SharedArrayBuffer(STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT)
this.state = new SharedArrayBuffer(
STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT
)
// Create a buffer for each audio channel
this.channels = []
for (let i = 0; i < channels; i += 1) {
const buffer = new SharedArrayBuffer(capacity * Float32Array.BYTES_PER_ELEMENT)
const buffer = new SharedArrayBuffer(
capacity * Float32Array.BYTES_PER_ELEMENT
)
this.channels.push(buffer)
}
@ -29,9 +33,9 @@ export class Buffer {
}
export class Ring {
state: Int32Array;
channels: Float32Array[];
capacity: number;
state: Int32Array
channels: Float32Array[]
capacity: number
constructor(buffer: Buffer) {
this.state = new Int32Array(buffer.state)
@ -50,7 +54,7 @@ export class Ring {
const writePos = Atomics.load(this.state, STATE.WRITE_POS)
const startPos = writePos
let endPos = writePos + frame.numberOfFrames;
let endPos = writePos + frame.numberOfFrames
if (endPos > readPos + this.capacity) {
endPos = readPos + this.capacity
@ -60,8 +64,8 @@ export class Ring {
}
}
const startIndex = startPos % this.capacity;
const endIndex = endPos % this.capacity;
const startIndex = startPos % this.capacity
const endIndex = endPos % this.capacity
// Loop over each channel
for (let i = 0; i < this.channels.length; i += 1) {
@ -105,8 +109,8 @@ export class Ring {
const readPos = Atomics.load(this.state, STATE.READ_POS)
const writePos = Atomics.load(this.state, STATE.WRITE_POS)
const startPos = readPos;
let endPos = startPos + dst[0].length;
const startPos = readPos
let endPos = startPos + dst[0].length
if (endPos > writePos) {
endPos = writePos
@ -116,8 +120,8 @@ export class Ring {
}
}
const startIndex = startPos % this.capacity;
const endIndex = endPos % this.capacity;
const startIndex = startPos % this.capacity
const endIndex = endPos % this.capacity
// Loop over each channel
for (let i = 0; i < dst.length; i += 1) {

View File

@ -1,18 +1,18 @@
import * as Message from "./message";
import * as Message from "./message"
export default class Video {
canvas: OffscreenCanvas;
queue: Array<VideoFrame>;
canvas: OffscreenCanvas
queue: Array<VideoFrame>
render: number; // non-zero if requestAnimationFrame has been called
sync?: number; // the wall clock value for timestamp 0, in microseconds
last?: number; // the timestamp of the last rendered frame, in microseconds
render: number // non-zero if requestAnimationFrame has been called
sync?: number // the wall clock value for timestamp 0, in microseconds
last?: number // the timestamp of the last rendered frame, in microseconds
constructor(config: Message.Config) {
this.canvas = config.canvas;
this.queue = [];
this.canvas = config.canvas
this.queue = []
this.render = 0;
this.render = 0
}
push(frame: VideoFrame) {
@ -23,18 +23,21 @@ export default class Video {
}
// Insert the frame into the queue sorted by timestamp.
if (this.queue.length > 0 && this.queue[this.queue.length - 1].timestamp <= frame.timestamp) {
if (
this.queue.length > 0 &&
this.queue[this.queue.length - 1].timestamp <= frame.timestamp
) {
// Fast path because we normally append to the end.
this.queue.push(frame)
} else {
// Do a full binary search
let low = 0
let high = this.queue.length;
let high = this.queue.length
while (low < high) {
const mid = (low + high) >>> 1;
if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1;
else high = mid;
const mid = (low + high) >>> 1
if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1
else high = mid
}
this.queue.splice(low, 0, frame)
@ -43,7 +46,7 @@ export default class Video {
draw(now: number) {
// Draw and then queue up the next draw call.
this.drawOnce(now);
this.drawOnce(now)
// Queue up the new draw frame.
this.render = self.requestAnimationFrame(this.draw.bind(this))
@ -51,16 +54,16 @@ export default class Video {
drawOnce(now: number) {
// Convert to microseconds
now *= 1000;
now *= 1000
if (!this.queue.length) {
return
}
let frame = this.queue[0];
let frame = this.queue[0]
if (!this.sync) {
this.sync = now - frame.timestamp;
this.sync = now - frame.timestamp
}
// Determine the target timestamp.
@ -71,7 +74,7 @@ export default class Video {
return
}
this.queue.shift();
this.queue.shift()
// Check if we should skip some frames
while (this.queue.length) {
@ -79,13 +82,13 @@ export default class Video {
if (next.timestamp > target) break
frame.close()
frame = this.queue.shift()!;
frame = this.queue.shift()!
}
const ctx = this.canvas.getContext("2d");
const ctx = this.canvas.getContext("2d")
ctx!.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height) // TODO aspect ratio
this.last = frame.timestamp;
this.last = frame.timestamp
frame.close()
}

View File

@ -2,10 +2,10 @@ import Renderer from "./renderer"
import Decoder from "./decoder"
import * as Message from "./message"
let decoder: Decoder;
let renderer: Renderer;
let decoder: Decoder
let renderer: Renderer
self.addEventListener('message', async (e: MessageEvent) => {
self.addEventListener("message", async (e: MessageEvent) => {
if (e.data.config) {
const config = e.data.config as Message.Config
@ -22,4 +22,3 @@ self.addEventListener('message', async (e: MessageEvent) => {
await renderer.play(play)
}
})

View File

@ -7,12 +7,12 @@ import * as Message from "./message"
import { Ring } from "./ring"
class Renderer extends AudioWorkletProcessor {
ring?: Ring;
base: number;
ring?: Ring
base: number
constructor(_params: AudioWorkletNodeOptions) {
// The super constructor call is required.
super();
super()
this.base = 0
this.port.onmessage = this.onMessage.bind(this)
@ -29,7 +29,11 @@ class Renderer extends AudioWorkletProcessor {
}
// Inputs and outputs in groups of 128 samples.
process(inputs: Float32Array[][], outputs: Float32Array[][], _parameters: Record<string, Float32Array>): boolean {
process(
inputs: Float32Array[][],
outputs: Float32Array[][],
_parameters: Record<string, Float32Array>
): boolean {
if (!this.ring) {
// Paused
return true
@ -46,8 +50,8 @@ class Renderer extends AudioWorkletProcessor {
// TODO trigger rebuffering event
}
return true;
return true
}
}
registerProcessor("renderer", Renderer);
registerProcessor("renderer", Renderer)

View File

@ -1,18 +1,20 @@
// Reader wraps a stream and provides convience methods for reading pieces from a stream
export default class Reader {
reader: ReadableStream;
buffer: Uint8Array;
reader: ReadableStream
buffer: Uint8Array
constructor(reader: ReadableStream, buffer: Uint8Array = new Uint8Array(0)) {
constructor(
reader: ReadableStream,
buffer: Uint8Array = new Uint8Array(0)
) {
this.reader = reader
this.buffer = buffer
}
// Returns any number of bytes
async read(): Promise<Uint8Array | undefined> {
if (this.buffer.byteLength) {
const buffer = this.buffer;
const buffer = this.buffer
this.buffer = new Uint8Array()
return buffer
}
@ -39,7 +41,9 @@ export default class Reader {
if (this.buffer.byteLength == 0) {
this.buffer = buffer
} else {
const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength)
const temp = new Uint8Array(
this.buffer.byteLength + buffer.byteLength
)
temp.set(this.buffer)
temp.set(buffer, this.buffer.byteLength)
this.buffer = temp
@ -68,15 +72,24 @@ export default class Reader {
if (this.buffer.byteLength == 0) {
this.buffer = buffer
} else {
const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength)
const temp = new Uint8Array(
this.buffer.byteLength + buffer.byteLength
)
temp.set(this.buffer)
temp.set(buffer, this.buffer.byteLength)
this.buffer = temp
}
}
const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size)
this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size)
const result = new Uint8Array(
this.buffer.buffer,
this.buffer.byteOffset,
size
)
this.buffer = new Uint8Array(
this.buffer.buffer,
this.buffer.byteOffset + size
)
r.releaseLock()
@ -97,14 +110,20 @@ export default class Reader {
if (this.buffer.byteLength == 0) {
this.buffer = buffer
} else {
const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength)
const temp = new Uint8Array(
this.buffer.byteLength + buffer.byteLength
)
temp.set(this.buffer)
temp.set(buffer, this.buffer.byteLength)
this.buffer = temp
}
}
const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size)
const result = new Uint8Array(
this.buffer.buffer,
this.buffer.byteOffset,
size
)
r.releaseLock()
@ -160,7 +179,11 @@ export default class Reader {
// NOTE: Returns a BigInt instead of a Number
async vint64(): Promise<bigint> {
const peek = await this.peek(1)
const first = new DataView(peek.buffer, peek.byteOffset, peek.byteLength).getUint8(0)
const first = new DataView(
peek.buffer,
peek.byteOffset,
peek.byteLength
).getUint8(0)
const size = (first & 0xc0) >> 6
switch (size) {

View File

@ -1,7 +1,7 @@
// Writer wraps a stream and writes chunks of data
export default class Writer {
buffer: ArrayBuffer;
writer: WritableStreamDefaultWriter;
buffer: ArrayBuffer
writer: WritableStreamDefaultWriter
constructor(stream: WritableStream) {
this.buffer = new ArrayBuffer(8)
@ -31,7 +31,7 @@ export default class Writer {
async uint24(v: number) {
const v1 = (v >> 16) & 0xff
const v2 = (v >> 8) & 0xff
const v3 = (v) & 0xff
const v3 = v & 0xff
const view = new DataView(this.buffer, 0, 3)
view.setUint8(0, v1)
@ -60,12 +60,12 @@ export default class Writer {
throw "value too large"
}
if (v < (1 << 6)) {
if (v < 1 << 6) {
return this.uint8(v)
} else if (v < (1 << 14)) {
return this.uint16(v|0x4000)
} else if (v < (1 << 30)) {
return this.uint32(v|0x80000000)
} else if (v < 1 << 14) {
return this.uint16(v | 0x4000)
} else if (v < 1 << 30) {
return this.uint32(v | 0x80000000)
} else {
return this.uint64(BigInt(v) | 0xc000000000000000n)
}
@ -78,12 +78,12 @@ export default class Writer {
}
async vint64(v: bigint) {
if (v < (1 << 6)) {
if (v < 1 << 6) {
return this.uint8(Number(v))
} else if (v < (1 << 14)) {
return this.uint16(Number(v)|0x4000)
} else if (v < (1 << 30)) {
return this.uint32(Number(v)|0x80000000)
} else if (v < 1 << 14) {
return this.uint16(Number(v) | 0x4000)
} else if (v < 1 << 30) {
return this.uint32(Number(v) | 0x80000000)
} else {
return this.uint64(v | 0xc000000000000000n)
}

View File

@ -2,14 +2,14 @@ import * as Stream from "../stream"
import * as Interface from "./interface"
export interface Config {
url: string;
fingerprint?: WebTransportHash; // the certificate fingerprint, temporarily needed for local development
url: string
fingerprint?: WebTransportHash // the certificate fingerprint, temporarily needed for local development
}
export default class Transport {
quic: Promise<WebTransport>;
api: Promise<WritableStream>;
callback?: Interface.Callback;
quic: Promise<WebTransport>
api: Promise<WritableStream>
callback?: Interface.Callback
constructor(config: Config) {
this.quic = this.connect(config)
@ -24,14 +24,14 @@ export default class Transport {
}
async close() {
(await this.quic).close()
;(await this.quic).close()
}
// Helper function to make creating a promise easier
private async connect(config: Config): Promise<WebTransport> {
const options: WebTransportOptions = {};
const options: WebTransportOptions = {}
if (config.fingerprint) {
options.serverCertificateHashes = [ config.fingerprint ]
options.serverCertificateHashes = [config.fingerprint]
}
const quic = new WebTransport(config.url, options)
@ -68,14 +68,16 @@ export default class Transport {
async handleStream(stream: ReadableStream) {
const r = new Stream.Reader(stream)
while (!await r.done()) {
const size = await r.uint32();
const typ = new TextDecoder('utf-8').decode(await r.bytes(4));
while (!(await r.done())) {
const size = await r.uint32()
const typ = new TextDecoder("utf-8").decode(await r.bytes(4))
if (typ != "warp") throw "expected warp atom"
if (size < 8) throw "atom too small"
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)
if (msg.init) {
@ -89,7 +91,7 @@ export default class Transport {
reader: r.reader,
})
} else {
console.warn("unknown message", msg);
console.warn("unknown message", msg)
}
}
}

View File

@ -4,11 +4,11 @@ export interface Callback {
}
export interface Init {
buffer: Uint8Array; // unread buffered data
reader: ReadableStream; // unread unbuffered data
buffer: Uint8Array // unread buffered data
reader: ReadableStream // unread unbuffered data
}
export interface Segment {
buffer: Uint8Array; // unread buffered data
reader: ReadableStream; // unread unbuffered data
buffer: Uint8Array // unread buffered data
reader: ReadableStream // unread unbuffered data
}

View File

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

View File

@ -5,12 +5,16 @@ export default class Deferred<T> {
constructor() {
// Set initial values so TS stops being annoying.
this.resolve = (_value: T | PromiseLike<T>) => { /* noop */ };
this.reject = (_value: T | PromiseLike<T>) => { /* noop */ };
this.resolve = (_value: T | PromiseLike<T>) => {
/* noop */
}
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": [
"src/**/*"
],
"include": ["src/**/*"],
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "node",
"strict": true,
"strict": true
}
}

View File

@ -101,7 +101,7 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@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"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz"
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"
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":
version "0.1.0"
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"
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":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@ -153,7 +203,7 @@
"@nodelib/fs.stat" "2.0.5"
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"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@ -238,7 +288,7 @@
"@parcel/transformer-react-refresh-wrap" "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"
resolved "https://registry.npmjs.org/@parcel/core/-/core-2.8.3.tgz"
integrity sha512-Euf/un4ZAiClnlUXqPB9phQlKbveU+2CotZv7m7i+qkgvFn5nAGnrV4h1OzQU42j9dpgOxWi7AttUDMrvkbhCQ==
@ -806,7 +856,7 @@
semver "^7.3.7"
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"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.7.tgz"
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"
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"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
@ -960,7 +1010,7 @@ braces@^3.0.2:
dependencies:
fill-range "^7.0.1"
browserslist@^4.6.6, "browserslist@>= 4.21.0":
browserslist@^4.6.6:
version "4.21.5"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz"
integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==
@ -1026,16 +1076,16 @@ color-convert@^2.0.1:
dependencies:
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:
version "1.1.3"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
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:
version "2.20.3"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
@ -1102,21 +1152,7 @@ csso@^4.2.0:
dependencies:
css-tree "^1.1.2"
debug@^4.1.1:
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:
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==
@ -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"
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:
version "5.1.1"
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"
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"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz"
integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
@ -1318,12 +1359,7 @@ estraverse@^4.1.1:
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.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:
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==
@ -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"
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:
version "1.19.0"
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"
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:
version "2.3.0"
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"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
srcset@4, srcset@4.0.0:
srcset@4:
version "4.0.0"
resolved "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz"
integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==
@ -2050,7 +2126,7 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
svgo@^2.4.0, svgo@^2.8.0:
svgo@^2.4.0:
version "2.8.0"
resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz"
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"
integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
terser@^5.10.0, terser@^5.2.0:
terser@^5.2.0:
version "5.16.8"
resolved "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz"
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"
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"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz"
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==