Files
Wanjohi 379db1c87b feat: Add streaming support (#125)
This adds:
- [x] Keyboard and mouse handling on the frontend
- [x] Video and audio streaming from the backend to the frontend
- [x] Input server that works with Websockets

Update - 17/11
- [ ] Master docker container to run this
- [ ] Steam runtime
- [ ] Entrypoint.sh

---------

Co-authored-by: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Kristian Ollikainen <DatCaptainHorse@users.noreply.github.com>
2024-12-08 14:54:56 +03:00

173 lines
4.3 KiB
TypeScript

// Ring buffer with audio samples.
enum STATE {
READ_POS = 0, // The current read position
WRITE_POS = 1, // The current write position
LENGTH = 2, // Clever way of saving the total number of enums values.
}
interface FrameCopyToOptions {
frameCount?: number
frameOffset?: number
planeIndex: number
}
// This is implemented by AudioData in WebCodecs, but we don't import it because it's a DOM type.
interface Frame {
numberOfFrames: number
numberOfChannels: number
copyTo(dst: Float32Array, options: FrameCopyToOptions): void
}
// No prototype to make this easier to send via postMessage
export class RingShared {
state: SharedArrayBuffer
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)
// 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
}
}
export class Ring {
state: Int32Array
channels: Float32Array[]
capacity: number
constructor(shared: RingShared) {
this.state = new Int32Array(shared.state)
this.channels = []
for (const channel of shared.channels) {
this.channels.push(new Float32Array(channel))
}
this.capacity = shared.capacity
}
// Write samples for single audio frame, returning the total number written.
write(frame: Frame): number {
const readPos = Atomics.load(this.state, STATE.READ_POS)
const writePos = Atomics.load(this.state, STATE.WRITE_POS)
const available = this.capacity - (writePos - readPos)
if (available <= 0) return 0
const toWrite = Math.min(frame.numberOfFrames, available)
const startPos = writePos
const endPos = writePos + toWrite
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]
// If the AudioData doesn't have enough channels, duplicate it.
const planeIndex = Math.min(i, frame.numberOfChannels - 1)
if (startIndex < endIndex) {
// One continuous range to copy.
const full = channel.subarray(startIndex, endIndex)
frame.copyTo(full, {
planeIndex,
frameCount: endIndex - startIndex,
})
} else {
const first = channel.subarray(startIndex)
const second = channel.subarray(0, endIndex)
frame.copyTo(first, {
planeIndex,
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,
frameOffset: first.length,
frameCount: second.length,
})
}
}
}
Atomics.store(this.state, STATE.WRITE_POS, endPos)
return toWrite
}
read(dst: Float32Array[]): number {
const readPos = Atomics.load(this.state, STATE.READ_POS)
const writePos = Atomics.load(this.state, STATE.WRITE_POS)
const startPos = readPos
let endPos = startPos + dst[0].length
if (endPos > writePos) {
endPos = writePos
if (endPos <= startPos) {
// Nothing to read
return 0
}
}
const startIndex = startPos % this.capacity
const endIndex = endPos % this.capacity
// Loop over each channel
for (let i = 0; i < dst.length; i += 1) {
if (i >= this.channels.length) {
// ignore excess channels
}
const input = this.channels[i]
const output = dst[i]
if (startIndex < endIndex) {
const full = input.subarray(startIndex, endIndex)
output.set(full)
} else {
const first = input.subarray(startIndex)
const second = input.subarray(0, endIndex)
output.set(first)
output.set(second, first.length)
}
}
Atomics.store(this.state, STATE.READ_POS, endPos)
return endPos - startPos
}
clear() {
const pos = Atomics.load(this.state, STATE.WRITE_POS)
Atomics.store(this.state, STATE.READ_POS, 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
}
}