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>
This commit is contained in:
Wanjohi
2024-12-08 14:54:56 +03:00
committed by GitHub
parent 5eb21eeadb
commit 379db1c87b
137 changed files with 12737 additions and 5234 deletions

View File

@@ -0,0 +1,20 @@
import { type Track, decodeTrack } from "./track"
export interface Audio {
track: Track
codec: string
sample_rate: number
channel_count: number
bitrate?: number
}
export function decodeAudio(o: unknown): o is Audio {
if (typeof o !== "object" || o === null) return false
const obj = o as Partial<Audio>
if (!decodeTrack(obj.track)) return false
if (typeof obj.codec !== "string") return false
if (typeof obj.sample_rate !== "number") return false
if (typeof obj.channel_count !== "number") return false
return true
}

View File

@@ -0,0 +1,62 @@
import * as Transfork from "../../transfork"
import { type Audio, decodeAudio } from "./audio"
import { type Video, decodeVideo } from "./video"
export interface Broadcast {
path: string[]
video: Video[]
audio: Audio[]
}
export function encode(catalog: Broadcast): Uint8Array {
const encoder = new TextEncoder()
console.debug("encoding catalog", catalog)
const str = JSON.stringify(catalog)
return encoder.encode(str)
}
export function decode(path: string[], raw: Uint8Array): Broadcast {
const decoder = new TextDecoder()
const str = decoder.decode(raw)
const catalog = JSON.parse(str)
if (!decodeBroadcast(catalog)) {
console.error("invalid catalog", catalog)
throw new Error("invalid catalog")
}
catalog.path = path
return catalog
}
export async function fetch(connection: Transfork.Connection, path: string[]): Promise<Broadcast> {
const track = new Transfork.Track(path.concat("catalog.json"), 0)
const sub = await connection.subscribe(track)
try {
const segment = await sub.nextGroup()
if (!segment) throw new Error("no catalog data")
const frame = await segment.readFrame()
if (!frame) throw new Error("no catalog frame")
segment.close()
return decode(path, frame)
} finally {
sub.close()
}
}
export function decodeBroadcast(o: unknown): o is Broadcast {
if (typeof o !== "object" || o === null) return false
const catalog = o as Partial<Broadcast>
if (catalog.audio === undefined) catalog.audio = []
if (!Array.isArray(catalog.audio)) return false
if (!catalog.audio.every((track: unknown) => decodeAudio(track))) return false
if (catalog.video === undefined) catalog.video = []
if (!Array.isArray(catalog.video)) return false
if (!catalog.video.every((track: unknown) => decodeVideo(track))) return false
return true
}

View File

@@ -0,0 +1,7 @@
import type { Audio } from "./audio"
import { type Broadcast, decode, encode, fetch } from "./broadcast"
import type { Track } from "./track"
import type { Video } from "./video"
export type { Audio, Video, Track, Broadcast }
export { encode, decode, fetch }

View File

@@ -0,0 +1,15 @@
export type GroupOrder = "desc" | "asc"
export interface Track {
name: string
priority: number
}
export function decodeTrack(o: unknown): o is Track {
if (typeof o !== "object" || o === null) return false
const obj = o as Partial<Track>
if (typeof obj.name !== "string") return false
if (typeof obj.priority !== "number") return false
return true
}

View File

@@ -0,0 +1,29 @@
import * as Hex from "../../common/hex"
import { type Track, decodeTrack } from "./track"
export interface Video {
track: Track
codec: string
description?: Uint8Array
bitrate?: number
frame_rate?: number
resolution: Dimensions
}
export interface Dimensions {
width: number
height: number
}
export function decodeVideo(o: unknown): o is Video {
if (typeof o !== "object" || o === null) return false
const obj = o as Partial<Video>
if (!decodeTrack(obj.track)) return false
if (typeof obj.codec !== "string") return false
if (typeof obj.description !== "string") return false
obj.description = Hex.decode(obj.description)
return true
}