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