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

@@ -1,208 +1,208 @@
import type { Connection, SubscribeRecv } from "@nestri/moq/transport"
import { asError } from "@nestri/moq/common/error"
import { Client } from "@nestri/moq/transport/client"
import * as Catalog from "@nestri/moq/media/catalog"
import { type GroupWriter } from "@nestri/moq/transport/objects"
// import type { Connection, SubscribeRecv } from "@nestri/libmoq/transport"
// import { asError } from "@nestri/moq/common/error"
// import { Client } from "@nestri/moq/transport/client"
// import * as Catalog from "@nestri/moq/media/catalog"
// import { type GroupWriter } from "@nestri/moq/transport/objects"
export interface BroadcastConfig {
namespace: string
connection: Connection
}
export interface BroadcasterConfig {
url: string
namespace: string
fingerprint?: string // URL to fetch TLS certificate fingerprint
}
// export interface BroadcastConfig {
// namespace: string
// connection: Connection
// }
// export interface BroadcasterConfig {
// url: string
// namespace: string
// fingerprint?: string // URL to fetch TLS certificate fingerprint
// }
export interface BroadcastConfigTrack {
input: string
bitrate: number
}
// export interface BroadcastConfigTrack {
// input: string
// bitrate: number
// }
export class Broadcast {
stream: GroupWriter | null
subscriber: SubscribeRecv | null
subscribed: boolean;
// export class Broadcast {
// stream: GroupWriter | null
// subscriber: SubscribeRecv | null
// subscribed: boolean;
readonly config: BroadcastConfig
readonly catalog: Catalog.Root
readonly connection: Connection
readonly namespace: string
// readonly config: BroadcastConfig
// readonly catalog: Catalog.Root
// readonly connection: Connection
// readonly namespace: string
#running: Promise<void>
// #running: Promise<void>
constructor(config: BroadcastConfig) {
this.subscribed = false
this.namespace = config.namespace
this.connection = config.connection
this.config = config
//Arbitrary values, just to keep TypeScript happy :)
this.catalog = {
version: 1,
streamingFormat: 1,
streamingFormatVersion: "0.2",
supportsDeltaUpdates: false,
commonTrackFields: {
packaging: "loc",
renderGroup: 1,
},
tracks: [{
name: "tester",
namespace: "tester",
selectionParams: {}
}],
}
this.stream = null
this.subscriber = null
// constructor(config: BroadcastConfig) {
// this.subscribed = false
// this.namespace = config.namespace
// this.connection = config.connection
// this.config = config
// //Arbitrary values, just to keep TypeScript happy :)
// this.catalog = {
// version: 1,
// streamingFormat: 1,
// streamingFormatVersion: "0.2",
// supportsDeltaUpdates: false,
// commonTrackFields: {
// packaging: "loc",
// renderGroup: 1,
// },
// tracks: [{
// name: "tester",
// namespace: "tester",
// selectionParams: {}
// }],
// }
// this.stream = null
// this.subscriber = null
this.#running = this.#run()
}
// this.#running = this.#run()
// }
static async init(config: BroadcasterConfig): Promise<Broadcast> {
const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "publisher" })
const connection = await client.connect();
// static async init(config: BroadcasterConfig): Promise<Broadcast> {
// const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "publisher" })
// const connection = await client.connect();
return new Broadcast({ connection, namespace: config.namespace })
}
// return new Broadcast({ connection, namespace: config.namespace })
// }
async #run() {
try {
await this.connection.announce(this.namespace)
this.subscribed = true
} catch (error) {
// async #run() {
// try {
// await this.connection.announce(this.namespace)
// this.subscribed = true
// } catch (error) {
this.subscribed = false
}
// this.subscribed = false
// }
for (; ;) {
const subscriber = await this.connection.subscribed()
// for (; ;) {
// const subscriber = await this.connection.subscribed()
if (!subscriber) {
this.subscribed = false
// if (!subscriber) {
// this.subscribed = false
break
}
// break
// }
await subscriber.ack()
// await subscriber.ack()
this.subscriber = subscriber
// this.subscriber = subscriber
this.subscribed = true
// this.subscribed = true
const bytes = Catalog.encode(this.catalog);
// const bytes = Catalog.encode(this.catalog);
const stream = await subscriber.group({ group: 0 });
// const stream = await subscriber.group({ group: 0 });
await stream.write({ object: 0, payload: bytes })
// await stream.write({ object: 0, payload: bytes })
this.stream = stream
}
}
// this.stream = stream
// }
// }
isSubscribed(): boolean {
return this.subscribed;
}
// isSubscribed(): boolean {
// return this.subscribed;
// }
// async #serveSubscribe(subscriber: SubscribeRecv) {
// try {
// // async #serveSubscribe(subscriber: SubscribeRecv) {
// // try {
// // Send a SUBSCRIBE_OK
// await subscriber.ack()
// // // Send a SUBSCRIBE_OK
// // await subscriber.ack()
// console.log("catalog track name:", subscriber.track)
// // console.log("catalog track name:", subscriber.track)
// const stream = await subscriber.group({ group: 0 });
// // const stream = await subscriber.group({ group: 0 });
// // const bytes = this.catalog.encode("Hello World")
// // // const bytes = this.catalog.encode("Hello World")
// await stream.write({ object: 0, payload: bytes })
// // await stream.write({ object: 0, payload: bytes })
// } catch (e) {
// const err = asError(e)
// await subscriber.close(1n, `failed to process publish: ${err.message}`)
// } finally {
// // TODO we can't close subscribers because there's no support for clean termination
// // await subscriber.close()
// }
// }
// // } catch (e) {
// // const err = asError(e)
// // await subscriber.close(1n, `failed to process publish: ${err.message}`)
// // } finally {
// // // TODO we can't close subscribers because there's no support for clean termination
// // // await subscriber.close()
// // }
// // }
// async mouseUpdatePosition({ x, y }: { x: number, y: number }, stream: GroupWriter) {
// // async mouseUpdatePosition({ x, y }: { x: number, y: number }, stream: GroupWriter) {
// const mouse_move = {
// input_type: "mouse_move",
// delta_y: y,
// delta_x: x,
// }
// // const mouse_move = {
// // input_type: "mouse_move",
// // delta_y: y,
// // delta_x: x,
// // }
// const bytes = Catalog.encode(this.catalog)
// // const bytes = Catalog.encode(this.catalog)
// await stream.write({ object: 0, payload: bytes });
// }
// // await stream.write({ object: 0, payload: bytes });
// // }
// async mouseUpdateButtons(e: MouseEvent, stream: GroupWriter) {
// const data: { input_type?: "mouse_key_down" | "mouse_key_up"; button: number; } = { button: e.button };
// // async mouseUpdateButtons(e: MouseEvent, stream: GroupWriter) {
// // const data: { input_type?: "mouse_key_down" | "mouse_key_up"; button: number; } = { button: e.button };
// if (e.type === "mousedown") {
// data["input_type"] = "mouse_key_down"
// } else if (e.type === "mouseup") {
// data["input_type"] = "mouse_key_up"
// }
// // if (e.type === "mousedown") {
// // data["input_type"] = "mouse_key_down"
// // } else if (e.type === "mouseup") {
// // data["input_type"] = "mouse_key_up"
// // }
// const bytes = Catalog.encode(this.catalog)
// // const bytes = Catalog.encode(this.catalog)
// await stream.write({ object: 0, payload: bytes });
// }
// // await stream.write({ object: 0, payload: bytes });
// // }
// async mouseUpdateWheel(e: WheelEvent, stream: GroupWriter) {
// const data: { input_type?: "mouse_wheel_up" | "mouse_wheel_down" } = {}
// // async mouseUpdateWheel(e: WheelEvent, stream: GroupWriter) {
// // const data: { input_type?: "mouse_wheel_up" | "mouse_wheel_down" } = {}
// if (e.deltaY < 0.0) {
// data["input_type"] = "mouse_wheel_up"
// } else {
// data["input_type"] = "mouse_wheel_down"
// }
// // if (e.deltaY < 0.0) {
// // data["input_type"] = "mouse_wheel_up"
// // } else {
// // data["input_type"] = "mouse_wheel_down"
// // }
// const bytes = Catalog.encode(this.catalog)
// // const bytes = Catalog.encode(this.catalog)
// await stream.write({ object: 0, payload: bytes });
// }
// // await stream.write({ object: 0, payload: bytes });
// // }
// async updateKeyUp(e: KeyboardEvent, stream: GroupWriter) {
// const data = {
// input_type: "key_up",
// key_code: e.keyCode
// }
// // async updateKeyUp(e: KeyboardEvent, stream: GroupWriter) {
// // const data = {
// // input_type: "key_up",
// // key_code: e.keyCode
// // }
// const bytes = Catalog.encode(this.catalog)
// // const bytes = Catalog.encode(this.catalog)
// await stream.write({ object: 0, payload: bytes });
// }
// // await stream.write({ object: 0, payload: bytes });
// // }
// async updateKeyDown(e: KeyboardEvent, stream: GroupWriter) {
// const data = {
// input_type: "key_down",
// key_code: e.keyCode
// }
// // async updateKeyDown(e: KeyboardEvent, stream: GroupWriter) {
// // const data = {
// // input_type: "key_down",
// // key_code: e.keyCode
// // }
// const bytes = Catalog.encode(this.catalog)
// // const bytes = Catalog.encode(this.catalog)
// await stream.write({ object: 0, payload: bytes });
// }
// // await stream.write({ object: 0, payload: bytes });
// // }
close() {
// TODO implement publish close
}
// close() {
// // TODO implement publish close
// }
// Returns the error message when the connection is closed
async closed(): Promise<Error> {
try {
await this.#running
return new Error("closed") // clean termination
} catch (e) {
return asError(e)
}
}
}
// // Returns the error message when the connection is closed
// async closed(): Promise<Error> {
// try {
// await this.#running
// return new Error("closed") // clean termination
// } catch (e) {
// return asError(e)
// }
// }
// }