mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
✨ 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:
@@ -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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
Reference in New Issue
Block a user