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 name: server
on: on:
push:
branches: [ "main" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
@ -15,9 +13,23 @@ jobs:
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 working-directory: server
run: cargo test --verbose 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 name: web
on: on:
push:
branches: [ "main" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
@ -12,15 +10,23 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: install - name: install
working-directory: web 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 working-directory: web
run: yarn build run: yarn build
- name: fmt
working-directory: web
run: yarn prettier --check .
- name: lint - name: lint
working-directory: web working-directory: web
run: yarn lint run: yarn lint

View File

@ -1,22 +1,26 @@
/* 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",
root: true, "prettier",
ignorePatterns: [ 'dist', 'node_modules' ], ],
rules: { parser: "@typescript-eslint/parser",
"@typescript-eslint/ban-ts-comment": "off", plugins: ["@typescript-eslint"],
"@typescript-eslint/no-non-null-assertion": "off", root: true,
"@typescript-eslint/no-explicit-any": "off", ignorePatterns: ["dist", "node_modules"],
"no-unused-vars": "off", // note you must disable the base rule as it can report incorrect errors rules: {
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/ban-ts-comment": "off",
"warn", // or "error" "@typescript-eslint/no-non-null-assertion": "off",
{ "@typescript-eslint/no-explicit-any": "off",
"argsIgnorePattern": "^_", "no-unused-vars": "off", // note you must disable the base rule as it can report incorrect errors
"varsIgnorePattern": "^_", "@typescript-eslint/no-unused-vars": [
"caughtErrorsIgnorePattern": "^_" "warn", // or "error"
} {
], 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) { 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

@ -1,24 +1,27 @@
{ {
"license": "Apache-2.0", "license": "Apache-2.0",
"source": "src/index.html", "source": "src/index.html",
"scripts": { "scripts": {
"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": { },
"@parcel/transformer-inline-string": "2.8.3", "devDependencies": {
"@parcel/validator-typescript": "^2.6.0", "@parcel/transformer-inline-string": "2.8.3",
"@types/audioworklet": "^0.0.41", "@parcel/validator-typescript": "^2.6.0",
"@types/dom-webcodecs": "^0.1.6", "@types/audioworklet": "^0.0.41",
"@typescript-eslint/eslint-plugin": "^5.59.7", "@types/dom-webcodecs": "^0.1.6",
"@typescript-eslint/parser": "^5.59.7", "@typescript-eslint/eslint-plugin": "^5.59.7",
"eslint": "^8.41.0", "@typescript-eslint/parser": "^5.59.7",
"parcel": "^2.8.0", "eslint": "^8.41.0",
"typescript": "^5.0.4" "eslint-config-prettier": "^8.8.0",
}, "parcel": "^2.8.0",
"dependencies": { "prettier": "^2.8.8",
"mp4box": "^0.5.2" "typescript": "^5.0.4"
} },
"dependencies": {
"mp4box": "^0.5.2"
}
} }

View File

@ -6,19 +6,19 @@ export class Encoder {
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",
@ -43,7 +43,7 @@ export class Encoder {
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({
@ -56,25 +56,25 @@ export class Encoder {
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({
@ -86,19 +86,19 @@ export class Encoder {
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,4 +1,6 @@
html, body, #player { html,
body,
#player {
width: 100%; width: 100%;
} }

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 id="stats">
<label>Audio Buffer:</label>
<div class="audio buffer"></div>
<label>Video Buffer:</label>
<div class="video buffer"></div>
</div>
</div> </div>
<div id="controls"> <script src="index.ts" type="module"></script>
<button type="button" id="live">Go Live</button> </body>
<button type="button" id="throttle">Throttle: None</button>
</div>
<div id="stats">
<label>Audio Buffer:</label>
<div class="audio buffer"></div>
<label>Video Buffer:</label>
<div class="video buffer"></div>
</div>
</div>
<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 @@
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()
@ -26,7 +26,7 @@ export class InitParser {
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

View File

@ -1,225 +1,243 @@
// 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) { push(frame: AudioData) {
// Drop any old frames // Drop any old frames
if (this.last && frame.timestamp <= this.last) { if (this.last && frame.timestamp <= this.last) {
frame.close() frame.close()
return return
} }
// Insert the frame into the queue sorted by timestamp. // Insert the frame into the queue sorted by timestamp.
if (this.queue.length > 0 && this.queue[this.queue.length - 1].timestamp <= frame.timestamp) { if (
// Fast path because we normally append to the end. this.queue.length > 0 &&
this.queue.push(frame) this.queue[this.queue.length - 1].timestamp <= frame.timestamp
} else { ) {
// Do a full binary search // Fast path because we normally append to the end.
let low = 0 this.queue.push(frame)
let high = this.queue.length; } else {
// Do a full binary search
let low = 0
let high = this.queue.length
while (low < high) { while (low < high) {
const mid = (low + high) >>> 1; const mid = (low + high) >>> 1
if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1; if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1
else high = mid; else high = mid
} }
this.queue.splice(low, 0, frame) this.queue.splice(low, 0, frame)
} }
this.emit() this.emit()
} }
emit() { emit() {
const ring = this.ring const ring = this.ring
if (!ring) { if (!ring) {
return return
} }
while (this.queue.length) { while (this.queue.length) {
const frame = this.queue[0]; const frame = this.queue[0]
if (ring.size() + frame.numberOfFrames > ring.capacity) { if (ring.size() + frame.numberOfFrames > ring.capacity) {
// Buffer is full // Buffer is full
break break
} }
const size = ring.write(frame) const size = ring.write(frame)
if (size < frame.numberOfFrames) { if (size < frame.numberOfFrames) {
throw new Error("audio buffer is full") throw new Error("audio buffer is full")
} }
this.last = frame.timestamp this.last = frame.timestamp
frame.close() frame.close()
this.queue.shift() this.queue.shift()
} }
} }
play(play: Message.Play) { play(play: Message.Play) {
this.ring = new Ring(play.buffer) this.ring = new Ring(play.buffer)
if (!this.render) { if (!this.render) {
const sampleRate = 44100 // TODO dynamic const sampleRate = 44100 // TODO dynamic
// Refresh every half buffer // 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) this.render = setInterval(this.emit.bind(this), refresh)
} }
} }
} }

View File

@ -1,167 +1,183 @@
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) { async receiveInit(msg: Message.Init) {
const stream = new Stream.Reader(msg.reader, msg.buffer); const stream = new Stream.Reader(msg.reader, msg.buffer)
for (;;) { for (;;) {
const data = await stream.read() const data = await stream.read()
if (!data) break if (!data) break
this.init.push(data) this.init.push(data)
} }
// TODO make sure the init segment is fully received // TODO make sure the init segment is fully received
} }
async receiveSegment(msg: Message.Segment) { async receiveSegment(msg: Message.Segment) {
// Wait for the init segment to be fully received and parsed // Wait for the init segment to be fully received and parsed
const init = await this.init.info 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) => { input.onReady = (track: any) => {
// Extract all of the tracks, because we don't know if it's audio or video. // Extract all of the tracks, because we don't know if it's audio or video.
for (const i of init.tracks) { 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 // MP4box requires us to reparse the init segment unfortunately
let offset = 0; let offset = 0
for (const raw of this.init.raw) { for (const raw of this.init.raw) {
raw.fileStart = offset raw.fileStart = offset
offset = input.appendBuffer(raw) offset = input.appendBuffer(raw)
} }
const stream = new Stream.Reader(msg.reader, msg.buffer) const stream = new Stream.Reader(msg.reader, msg.buffer)
// For whatever reason, mp4box doesn't work until you read an atom at a time. // 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) const raw = await stream.peek(4)
// TODO this doesn't support when size = 0 (until EOF) or size = 1 (extended size) // TODO this doesn't support when size = 0 (until EOF) or size = 1 (extended size)
const size = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(0) const size = new DataView(
raw.buffer,
raw.byteOffset,
raw.byteLength
).getUint32(0)
const atom = await stream.bytes(size) const atom = await stream.bytes(size)
// 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(atom.byteLength); const box = new Uint8Array(atom.byteLength)
box.set(atom) box.set(atom)
// and for some reason we need to modify the underlying ArrayBuffer with offset // and for some reason we need to modify the underlying ArrayBuffer with offset
const buffer = box.buffer as MP4.ArrayBuffer const buffer = box.buffer as MP4.ArrayBuffer
buffer.fileStart = offset buffer.fileStart = offset
// Parse the data // Parse the data
offset = input.appendBuffer(buffer) offset = input.appendBuffer(buffer)
input.flush() input.flush()
} }
} }
onSamples(track_id: number, track: MP4.Track, samples: MP4.Sample[]) { 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) { if (!decoder) {
// We need a sample to initalize the video decoder, because of mp4box limitations. // We need a sample to initalize the video decoder, because of mp4box limitations.
const sample = samples[0]; const sample = samples[0]
if (isVideoTrack(track)) { if (isVideoTrack(track)) {
// Configure the decoder using the AVC box for H.264 // 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. // TODO it should be easy to support other codecs, just need to know the right boxes.
const avcc = sample.description.avcC; const avcc = sample.description.avcC
if (!avcc) throw new Error("TODO only h264 is supported"); 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(
avcc.write(description) new Uint8Array(avcc.size),
0,
false
)
avcc.write(description)
const videoDecoder = new VideoDecoder({ const videoDecoder = new VideoDecoder({
output: this.renderer.push.bind(this.renderer), output: this.renderer.push.bind(this.renderer),
error: console.warn, error: console.warn,
}); })
videoDecoder.configure({ videoDecoder.configure({
codec: track.codec, codec: track.codec,
codedHeight: track.video.height, codedHeight: track.video.height,
codedWidth: track.video.width, codedWidth: track.video.width,
description: description.buffer?.slice(8), description: description.buffer?.slice(8),
// optimizeForLatency: true // optimizeForLatency: true
}) })
decoder = videoDecoder decoder = videoDecoder
} else if (isAudioTrack(track)) { } else if (isAudioTrack(track)) {
const audioDecoder = new AudioDecoder({ const audioDecoder = new AudioDecoder({
output: this.renderer.push.bind(this.renderer), output: this.renderer.push.bind(this.renderer),
error: console.warn, error: console.warn,
}); })
audioDecoder.configure({ audioDecoder.configure({
codec: track.codec, codec: track.codec,
numberOfChannels: track.audio.channel_count, numberOfChannels: track.audio.channel_count,
sampleRate: track.audio.sample_rate, sampleRate: track.audio.sample_rate,
}) })
decoder = audioDecoder decoder = audioDecoder
} else { } else {
throw new Error("unknown track type") throw new Error("unknown track type")
} }
this.decoders.set(track_id, decoder) this.decoders.set(track_id, decoder)
} }
for (const sample of samples) { for (const sample of samples) {
// Convert to microseconds // Convert to microseconds
const timestamp = 1000 * 1000 * sample.dts / sample.timescale const timestamp = (1000 * 1000 * sample.dts) / sample.timescale
const duration = 1000 * 1000 * sample.duration / sample.timescale const duration = (1000 * 1000 * sample.duration) / sample.timescale
if (isAudioDecoder(decoder)) { if (isAudioDecoder(decoder)) {
decoder.decode(new EncodedAudioChunk({ decoder.decode(
type: sample.is_sync ? "key" : "delta", new EncodedAudioChunk({
data: sample.data, type: sample.is_sync ? "key" : "delta",
duration: duration, data: sample.data,
timestamp: timestamp, duration: duration,
})) timestamp: timestamp,
} else if (isVideoDecoder(decoder)) { })
decoder.decode(new EncodedVideoChunk({ )
type: sample.is_sync ? "key" : "delta", } else if (isVideoDecoder(decoder)) {
data: sample.data, decoder.decode(
duration: duration, new EncodedVideoChunk({
timestamp: timestamp, type: sample.is_sync ? "key" : "delta",
})) data: sample.data,
} else { duration: duration,
throw new Error("unknown decoder type") 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 { 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, { const worker = new Worker(url, {
type: "module", type: "module",
name: "media", name: "media",
}) })
const msg = { const msg = {
canvas: config.canvas, canvas: config.canvas,
} }
worker.postMessage({ config: msg }, [msg.canvas]) worker.postMessage({ config: msg }, [msg.canvas])
return worker return worker
} }
private async setupWorklet(_config: Config): Promise<AudioWorkletNode> { private async setupWorklet(_config: Config): Promise<AudioWorkletNode> {
// Load the worklet source code. // 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) await this.context.audioWorklet.addModule(url)
const volume = this.context.createGain() const volume = this.context.createGain()
volume.gain.value = 2.0; volume.gain.value = 2.0
// Create a worklet // Create a worklet
const worklet = new AudioWorkletNode(this.context, 'renderer'); const worklet = new AudioWorkletNode(this.context, "renderer")
worklet.onprocessorerror = (e: Event) => { worklet.onprocessorerror = (e: Event) => {
console.error("Audio worklet error:", e) console.error("Audio worklet error:", e)
}; }
// Connect the worklet to the volume node and then to the speakers // Connect the worklet to the volume node and then to the speakers
worklet.connect(volume) worklet.connect(volume)
volume.connect(this.context.destination) volume.connect(this.context.destination)
return worklet return worklet
} }
onInit(init: Message.Init) { onInit(init: Message.Init) {
this.worker.postMessage({ init }, [init.buffer.buffer, init.reader]) this.worker.postMessage({ init }, [init.buffer.buffer, init.reader])
} }
onSegment(segment: Message.Segment) { onSegment(segment: Message.Segment) {
this.worker.postMessage({ segment }, [segment.buffer.buffer, segment.reader]) this.worker.postMessage({ segment }, [
} segment.buffer.buffer,
segment.reader,
])
}
async play() { async play() {
this.context.resume() this.context.resume()
const play = { const play = {
buffer: new Ring.Buffer(2, 44100 / 10), // 100ms of audio buffer: new Ring.Buffer(2, 44100 / 10), // 100ms of audio
} }
const worklet = await this.worklet; const worklet = await this.worklet
worklet.port.postMessage({ play }) worklet.port.postMessage({ play })
this.worker.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 this.capacity = buffer.capacity
} }
// Write samples for single audio frame, returning the total number written. // Write samples for single audio frame, returning the total number written.
write(frame: AudioData): number { write(frame: AudioData): number {
const readPos = Atomics.load(this.state, STATE.READ_POS) const readPos = Atomics.load(this.state, STATE.READ_POS)
const writePos = Atomics.load(this.state, STATE.WRITE_POS) const writePos = Atomics.load(this.state, STATE.WRITE_POS)
const startPos = writePos const startPos = writePos
let endPos = writePos + frame.numberOfFrames; let endPos = writePos + frame.numberOfFrames
if (endPos > readPos + this.capacity) { if (endPos > readPos + this.capacity) {
endPos = readPos + this.capacity endPos = readPos + this.capacity
if (endPos <= startPos) { if (endPos <= startPos) {
// No space to write // No space to write
return 0 return 0
} }
} }
const startIndex = startPos % this.capacity; const startIndex = startPos % this.capacity
const endIndex = endPos % this.capacity; const endIndex = endPos % this.capacity
// Loop over each channel // Loop over each channel
for (let i = 0; i < this.channels.length; i += 1) { for (let i = 0; i < this.channels.length; i += 1) {
const channel = this.channels[i] const channel = this.channels[i]
if (startIndex < endIndex) { if (startIndex < endIndex) {
// One continuous range to copy. // One continuous range to copy.
const full = channel.subarray(startIndex, endIndex) const full = channel.subarray(startIndex, endIndex)
frame.copyTo(full, { frame.copyTo(full, {
planeIndex: i, planeIndex: i,
frameCount: endIndex - startIndex, frameCount: endIndex - startIndex,
}) })
} else { } else {
const first = channel.subarray(startIndex) const first = channel.subarray(startIndex)
const second = channel.subarray(0, endIndex) const second = channel.subarray(0, endIndex)
frame.copyTo(first, { frame.copyTo(first, {
planeIndex: i, planeIndex: i,
frameCount: first.length, frameCount: first.length,
}) })
// We need this conditional when startIndex == 0 and endIndex == 0 // We need this conditional when startIndex == 0 and endIndex == 0
// When capacity=4410 and frameCount=1024, this was happening 52s into the audio. // When capacity=4410 and frameCount=1024, this was happening 52s into the audio.
if (second.length) { if (second.length) {
frame.copyTo(second, { frame.copyTo(second, {
planeIndex: i, planeIndex: i,
frameOffset: first.length, frameOffset: first.length,
frameCount: second.length, frameCount: second.length,
}) })
} }
} }
} }
Atomics.store(this.state, STATE.WRITE_POS, endPos) Atomics.store(this.state, STATE.WRITE_POS, endPos)
return endPos - startPos return endPos - startPos
} }
read(dst: Float32Array[]): number { read(dst: Float32Array[]): number {
const readPos = Atomics.load(this.state, STATE.READ_POS) const readPos = Atomics.load(this.state, STATE.READ_POS)
const writePos = Atomics.load(this.state, STATE.WRITE_POS) const writePos = Atomics.load(this.state, STATE.WRITE_POS)
const startPos = readPos; const startPos = readPos
let endPos = startPos + dst[0].length; let endPos = startPos + dst[0].length
if (endPos > writePos) { if (endPos > writePos) {
endPos = writePos endPos = writePos
if (endPos <= startPos) { if (endPos <= startPos) {
// Nothing to read // Nothing to read
return 0 return 0
} }
} }
const startIndex = startPos % this.capacity; const startIndex = startPos % this.capacity
const endIndex = endPos % this.capacity; const endIndex = endPos % this.capacity
// Loop over each channel // Loop over each channel
for (let i = 0; i < dst.length; i += 1) { for (let i = 0; i < dst.length; i += 1) {
if (i >= this.channels.length) { if (i >= this.channels.length) {
// ignore excess channels // ignore excess channels
} }
const input = this.channels[i] const input = this.channels[i]
const output = dst[i] const output = dst[i]
if (startIndex < endIndex) { if (startIndex < endIndex) {
const full = input.subarray(startIndex, endIndex) const full = input.subarray(startIndex, endIndex)
output.set(full) output.set(full)
} else { } else {
const first = input.subarray(startIndex) const first = input.subarray(startIndex)
const second = input.subarray(0, endIndex) const second = input.subarray(0, endIndex)
output.set(first) output.set(first)
output.set(second, first.length) output.set(second, first.length)
} }
} }
Atomics.store(this.state, STATE.READ_POS, endPos) Atomics.store(this.state, STATE.READ_POS, endPos)
return endPos - startPos return endPos - startPos
} }
size() { size() {
// TODO is this thread safe? // TODO is this thread safe?
const readPos = Atomics.load(this.state, STATE.READ_POS) const readPos = Atomics.load(this.state, STATE.READ_POS)
const writePos = Atomics.load(this.state, STATE.WRITE_POS) const writePos = Atomics.load(this.state, STATE.WRITE_POS)
return writePos - readPos 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) { push(frame: VideoFrame) {
// Drop any old frames // Drop any old frames
if (this.last && frame.timestamp <= this.last) { if (this.last && frame.timestamp <= this.last) {
frame.close() frame.close()
return return
} }
// Insert the frame into the queue sorted by timestamp. // Insert the frame into the queue sorted by timestamp.
if (this.queue.length > 0 && this.queue[this.queue.length - 1].timestamp <= frame.timestamp) { if (
// Fast path because we normally append to the end. this.queue.length > 0 &&
this.queue.push(frame) this.queue[this.queue.length - 1].timestamp <= frame.timestamp
} else { ) {
// Do a full binary search // Fast path because we normally append to the end.
let low = 0 this.queue.push(frame)
let high = this.queue.length; } else {
// Do a full binary search
let low = 0
let high = this.queue.length
while (low < high) { while (low < high) {
const mid = (low + high) >>> 1; const mid = (low + high) >>> 1
if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1; if (this.queue[mid].timestamp < frame.timestamp) low = mid + 1
else high = mid; else high = mid
} }
this.queue.splice(low, 0, frame) this.queue.splice(low, 0, frame)
} }
} }
draw(now: number) { draw(now: number) {
// Draw and then queue up the next draw call. // Draw and then queue up the next draw call.
this.drawOnce(now); this.drawOnce(now)
// Queue up the new draw frame. // Queue up the new draw frame.
this.render = self.requestAnimationFrame(this.draw.bind(this)) this.render = self.requestAnimationFrame(this.draw.bind(this))
} }
drawOnce(now: number) { drawOnce(now: number) {
// Convert to microseconds // Convert to microseconds
now *= 1000; now *= 1000
if (!this.queue.length) { if (!this.queue.length) {
return return
} }
let frame = this.queue[0]; let frame = this.queue[0]
if (!this.sync) { if (!this.sync) {
this.sync = now - frame.timestamp; this.sync = now - frame.timestamp
} }
// Determine the target timestamp. // Determine the target timestamp.
const target = now - this.sync const target = now - this.sync
if (frame.timestamp >= target) { if (frame.timestamp >= target) {
// nothing to render yet, wait for the next animation frame // nothing to render yet, wait for the next animation frame
return return
} }
this.queue.shift(); this.queue.shift()
// Check if we should skip some frames // Check if we should skip some frames
while (this.queue.length) { while (this.queue.length) {
const next = this.queue[0] const next = this.queue[0]
if (next.timestamp > target) break if (next.timestamp > target) break
frame.close() 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 ctx!.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height) // TODO aspect ratio
this.last = frame.timestamp; this.last = frame.timestamp
frame.close() frame.close()
} }
play(_play: Message.Play) { play(_play: Message.Play) {
// Queue up to render the next frame. // Queue up to render the next frame.
if (!this.render) { if (!this.render) {
this.render = self.requestAnimationFrame(this.draw.bind(this)) 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) { onMessage(e: MessageEvent) {
if (e.data.play) { if (e.data.play) {
this.onPlay(e.data.play) this.onPlay(e.data.play)
} }
} }
onPlay(play: Message.Play) { onPlay(play: Message.Play) {
this.ring = new Ring(play.buffer) this.ring = new Ring(play.buffer)
} }
// Inputs and outputs in groups of 128 samples. // Inputs and outputs in groups of 128 samples.
process(inputs: Float32Array[][], outputs: Float32Array[][], _parameters: Record<string, Float32Array>): boolean { process(
if (!this.ring) { inputs: Float32Array[][],
// Paused outputs: Float32Array[][],
return true _parameters: Record<string, Float32Array>
} ): boolean {
if (!this.ring) {
// Paused
return true
}
if (inputs.length != 1 && outputs.length != 1) { if (inputs.length != 1 && outputs.length != 1) {
throw new Error("only a single track is supported") throw new Error("only a single track is supported")
} }
const output = outputs[0] const output = outputs[0]
const size = this.ring.read(output) const size = this.ring.read(output)
if (size < output.length) { if (size < output.length) {
// TODO trigger rebuffering event // 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 // 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) { if (this.buffer.byteLength) {
const buffer = this.buffer; const buffer = this.buffer
this.buffer = new Uint8Array() this.buffer = new Uint8Array()
return buffer return buffer
} }
@ -39,7 +41,9 @@ export default class Reader {
if (this.buffer.byteLength == 0) { if (this.buffer.byteLength == 0) {
this.buffer = buffer this.buffer = buffer
} else { } 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(this.buffer)
temp.set(buffer, this.buffer.byteLength) temp.set(buffer, this.buffer.byteLength)
this.buffer = temp this.buffer = temp
@ -68,15 +72,24 @@ export default class Reader {
if (this.buffer.byteLength == 0) { if (this.buffer.byteLength == 0) {
this.buffer = buffer this.buffer = buffer
} else { } 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(this.buffer)
temp.set(buffer, this.buffer.byteLength) temp.set(buffer, this.buffer.byteLength)
this.buffer = temp this.buffer = temp
} }
} }
const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) const result = new Uint8Array(
this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size) this.buffer.buffer,
this.buffer.byteOffset,
size
)
this.buffer = new Uint8Array(
this.buffer.buffer,
this.buffer.byteOffset + size
)
r.releaseLock() r.releaseLock()
@ -97,14 +110,20 @@ export default class Reader {
if (this.buffer.byteLength == 0) { if (this.buffer.byteLength == 0) {
this.buffer = buffer this.buffer = buffer
} else { } 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(this.buffer)
temp.set(buffer, this.buffer.byteLength) temp.set(buffer, this.buffer.byteLength)
this.buffer = temp this.buffer = temp
} }
} }
const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) const result = new Uint8Array(
this.buffer.buffer,
this.buffer.byteOffset,
size
)
r.releaseLock() r.releaseLock()
@ -160,28 +179,32 @@ export default class Reader {
// NOTE: Returns a BigInt instead of a Number // NOTE: Returns a BigInt instead of a Number
async vint64(): Promise<bigint> { async vint64(): Promise<bigint> {
const peek = await this.peek(1) 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 const size = (first & 0xc0) >> 6
switch (size) { switch (size) {
case 0: { case 0: {
const v = await this.uint8() const v = await this.uint8()
return BigInt(v) & 0x3fn return BigInt(v) & 0x3fn
} }
case 1: { case 1: {
const v = await this.uint16() const v = await this.uint16()
return BigInt(v) & 0x3fffn return BigInt(v) & 0x3fffn
} }
case 2: { case 2: {
const v = await this.uint32() const v = await this.uint32()
return BigInt(v) & 0x3fffffffn return BigInt(v) & 0x3fffffffn
} }
case 3: { case 3: {
const v = await this.uint64() const v = await this.uint64()
return v & 0x3fffffffffffffffn return v & 0x3fffffffffffffffn
} }
default: default:
throw "impossible" throw "impossible"
} }
} }

View File

@ -1,7 +1,7 @@
// 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)
@ -31,7 +31,7 @@ export default class Writer {
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)
@ -60,12 +60,12 @@ export default class Writer {
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)
} }
@ -78,12 +78,12 @@ export default class Writer {
} }
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)
} }

View File

@ -2,14 +2,14 @@ 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)
@ -24,14 +24,14 @@ export default class Transport {
} }
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)
@ -68,14 +68,16 @@ export default class Transport {
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) {
@ -89,7 +91,7 @@ export default class Transport {
reader: r.reader, reader: r.reader,
}) })
} else { } else {
console.warn("unknown message", msg); console.warn("unknown message", msg)
} }
} }
} }

View File

@ -4,11 +4,11 @@ export interface Callback {
} }
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

@ -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.reject = (_value: T | PromiseLike<T>) => {
/* noop */
}
this.promise = new Promise((resolve, reject) => { this.promise = new Promise((resolve, reject) => {
this.resolve = resolve this.resolve = resolve
this.reject = reject this.reject = reject
}) })
} }
} }

View File

@ -1,11 +1,9 @@
{ {
"include": [ "include": ["src/**/*"],
"src/**/*" "compilerOptions": {
], "target": "es2022",
"compilerOptions": { "module": "es2022",
"target": "es2022", "moduleResolution": "node",
"module": "es2022", "strict": true
"moduleResolution": "node", }
"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==