diff options
author | metamuffin <metamuffin@disroot.org> | 2022-10-03 11:28:16 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2022-10-03 11:28:16 +0200 |
commit | 4e99a3325318c902cd78ea9f760f46d79acde5c0 (patch) | |
tree | cc2bc54f4a0eb27db2b5d38dfbb785c1e9b84bd6 /client-web | |
parent | fa44b02da29a0bd1b60026d4f6ffd6c9748a09da (diff) | |
download | keks-meet-4e99a3325318c902cd78ea9f760f46d79acde5c0.tar keks-meet-4e99a3325318c902cd78ea9f760f46d79acde5c0.tar.bz2 keks-meet-4e99a3325318c902cd78ea9f760f46d79acde5c0.tar.zst |
riesencommit (part 1)
Diffstat (limited to 'client-web')
-rw-r--r-- | client-web/source/chat.ts | 3 | ||||
-rw-r--r-- | client-web/source/helper.ts | 12 | ||||
-rw-r--r-- | client-web/source/index.ts | 2 | ||||
-rw-r--r-- | client-web/source/keybinds.ts | 7 | ||||
-rw-r--r-- | client-web/source/menu.ts | 15 | ||||
-rw-r--r-- | client-web/source/resource/file.ts | 44 | ||||
-rw-r--r-- | client-web/source/resource/mod.ts | 167 | ||||
-rw-r--r-- | client-web/source/resource/track.ts | 160 | ||||
-rw-r--r-- | client-web/source/room.ts | 15 | ||||
-rw-r--r-- | client-web/source/track_handle.ts | 1 | ||||
-rw-r--r-- | client-web/source/user/local.ts | 124 | ||||
-rw-r--r-- | client-web/source/user/mod.ts | 37 | ||||
-rw-r--r-- | client-web/source/user/remote.ts | 133 |
13 files changed, 394 insertions, 326 deletions
diff --git a/client-web/source/chat.ts b/client-web/source/chat.ts index 6ddab8b..b7c572d 100644 --- a/client-web/source/chat.ts +++ b/client-web/source/chat.ts @@ -10,6 +10,7 @@ import { ediv, espan, image_view, notify, OverlayUi } from "./helper.ts"; import { log } from "./logger.ts"; import { PREFS } from "./preferences/mod.ts"; import { Room } from "./room.ts"; +import { LocalUser } from "./user/local.ts"; import { User } from "./user/mod.ts"; export class Chat extends OverlayUi { @@ -79,6 +80,6 @@ export class Chat extends OverlayUi { let body_str = "(empty message)" if (message.text) body_str = message.text if (message.image) body_str = "(image)" - if (!sender.local && PREFS.notify_chat) notify(body_str, sender.display_name) + if (!(sender instanceof LocalUser) && PREFS.notify_chat) notify(body_str, sender.display_name) } } diff --git a/client-web/source/helper.ts b/client-web/source/helper.ts index 1ec42aa..d43fc3e 100644 --- a/client-web/source/helper.ts +++ b/client-web/source/helper.ts @@ -7,24 +7,24 @@ import { PREFS } from "./preferences/mod.ts"; -const elem = (s: string) => document.createElement(s) +const elem = <K extends keyof HTMLElementTagNameMap>(s: K): HTMLElementTagNameMap[K] => document.createElement(s) -interface Opts { class?: string[] | string, id?: string, src?: string, onclick?: (e: HTMLElement) => void } +interface Opts<El> { class?: string[] | string, id?: string, src?: string, onclick?: (e: El) => void } -function apply_opts(e: HTMLElement, o: Opts | undefined) { +function apply_opts<El extends HTMLElement>(e: El, o: Opts<El> | undefined) { if (!o) return if (o.id) e.id = o.id if (o.onclick) e.onclick = () => o.onclick!(e) if (typeof o?.class == "string") e.classList.add(o.class) if (typeof o?.class == "object") e.classList.add(...o.class) } -const elem_with_content = (s: string) => (c: string, opts?: Opts) => { +const elem_with_content = <K extends keyof HTMLElementTagNameMap>(s: K) => (c: string, opts?: Opts<HTMLElementTagNameMap[K]>) => { const e = elem(s) apply_opts(e, opts) e.textContent = c return e } -const elem_with_children = (s: string) => (opts?: Opts, ...cs: (HTMLElement | string)[]) => { +const elem_with_children = <K extends keyof HTMLElementTagNameMap>(s: K) => (opts?: Opts<HTMLElementTagNameMap[K]>, ...cs: (HTMLElement | string)[]) => { const e = elem(s) apply_opts(e, opts) for (const c of cs) { @@ -65,7 +65,7 @@ export class OverlayUi { } } -export function image_view(url: string, opts?: Opts): HTMLElement { +export function image_view(url: string, opts?: Opts<HTMLElement>): HTMLElement { const img = document.createElement("img") apply_opts(img, opts) img.src = url diff --git a/client-web/source/index.ts b/client-web/source/index.ts index 8612397..c36ea1f 100644 --- a/client-web/source/index.ts +++ b/client-web/source/index.ts @@ -13,7 +13,7 @@ import { load_params, PREFS } from "./preferences/mod.ts"; import { SignalingConnection } from "./protocol/mod.ts"; import { Room } from "./room.ts" -export const VERSION = "0.1.8" +export const VERSION = "0.1.9" export const ROOM_CONTAINER = ediv({ class: "room" }) export const RTC_CONFIG: RTCConfiguration = { diff --git a/client-web/source/keybinds.ts b/client-web/source/keybinds.ts index f531cfa..ddd1c82 100644 --- a/client-web/source/keybinds.ts +++ b/client-web/source/keybinds.ts @@ -5,6 +5,7 @@ */ /// <reference lib="dom" /> +import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts"; import { Room } from "./room.ts" export function setup_keybinds(room: Room) { @@ -24,9 +25,9 @@ export function setup_keybinds(room: Room) { return } if (command_mode) { - if (ev.code == "KeyM" || ev.code == "KeyR") room.local_user.await_add_resource(room.local_user.create_mic_res()) - if (ev.code == "KeyS") room.local_user.await_add_resource(room.local_user.create_screencast_res()) - if (ev.code == "KeyC" && !ev.ctrlKey) room.local_user.await_add_resource(room.local_user.create_camera_res()) + if (ev.code == "KeyM" || ev.code == "KeyR") room.local_user.await_add_resource(create_mic_res()) + if (ev.code == "KeyS") room.local_user.await_add_resource(create_screencast_res()) + if (ev.code == "KeyC" && !ev.ctrlKey) room.local_user.await_add_resource(create_camera_res()) if (ev.code == "KeyC" && ev.ctrlKey) room.local_user.resources.forEach(t => t.destroy()) } command_mode = false diff --git a/client-web/source/menu.ts b/client-web/source/menu.ts index 99f0169..9126dc5 100644 --- a/client-web/source/menu.ts +++ b/client-web/source/menu.ts @@ -5,9 +5,11 @@ */ /// <reference lib="dom" /> -import { ediv, ep, OverlayUi } from "./helper.ts" +import { ebutton, ediv, ep, OverlayUi } from "./helper.ts" import { VERSION } from "./index.ts" import { PrefUi } from "./preferences/ui.ts" +import { create_file_res } from "./resource/file.ts"; +import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts"; import { Room } from "./room.ts" export class MenuBr extends OverlayUi { @@ -55,7 +57,14 @@ export class BottomMenu extends OverlayUi { if (prefs.shown) prefs_button.classList.add("active") else prefs_button.classList.remove("active") } - - super(ediv({ class: "bottom-menu" }, chat_toggle, prefs_button, room.local_user.create_controls())) + + const local_controls = ediv({ class: "local-controls" }, + ebutton("Microphone", { onclick: () => room.local_user.await_add_resource(create_mic_res()) }), + ebutton("Camera", { onclick: () => room.local_user.await_add_resource(create_camera_res()) }), + ebutton("Screen", { onclick: () => room.local_user.await_add_resource(create_screencast_res()) }), + ebutton("File", { onclick: () => room.local_user.await_add_resource(create_file_res()) }), + ) + + super(ediv({ class: "bottom-menu" }, chat_toggle, prefs_button, local_controls)) } } diff --git a/client-web/source/resource/file.ts b/client-web/source/resource/file.ts index 690c765..b6629bc 100644 --- a/client-web/source/resource/file.ts +++ b/client-web/source/resource/file.ts @@ -1,11 +1,39 @@ -import { TrackHandle } from "../track_handle.ts"; -import { Resource } from "./mod.ts"; - -export class FileResource extends Resource { - - on_track(_track: TrackHandle): HTMLElement { - throw new Error("Method not implemented."); - } +import { ediv } from "../helper.ts"; +import { LocalResource, ResourceHandlerDecl } from "./mod.ts"; +export const resource_file: ResourceHandlerDecl = { + kind: "file", + new_remote(info, _user, _enable) { + return { + info, + el: ediv(), + on_statechange(_s) { }, + on_enable(_track, _disable) { + return { + on_disable() { + } + } + } + } + } } + +export function create_file_res(): Promise<LocalResource> { + const picker = document.createElement("input") + picker.type = "file" + picker.click() + return new Promise((resolve, reject) => { + picker.addEventListener("change", () => { + if (!picker.files) return reject() + const f = picker.files.item(0) + if (!f) return reject() + resolve({ + info: { kind: "file", id: Math.random().toString(), label: f.name, size: f.size }, + destroy() { /* TODO */ }, + el: ediv(), + on_request(_user, _create_channel) { return _create_channel("TODO") } + }) + }) + }) +}
\ No newline at end of file diff --git a/client-web/source/resource/mod.ts b/client-web/source/resource/mod.ts index 716d42b..f99a252 100644 --- a/client-web/source/resource/mod.ts +++ b/client-web/source/resource/mod.ts @@ -6,84 +6,111 @@ /// <reference lib="dom" /> import { ProvideInfo } from "../../../common/packets.d.ts" -import { ediv } from "../helper.ts"; import { TrackHandle } from "../track_handle.ts"; -import { LocalUser } from "../user/local.ts"; -import { User } from "../user/mod.ts" import { RemoteUser } from "../user/remote.ts" -import { TrackResource } from "./track.ts"; +import { resource_file } from "./file.ts"; +import { resource_track } from "./track.ts"; -export type ChannelState = "enabled" | "await_enable" | "disabled" | "await_disable" -export abstract class Resource extends EventTarget { - local: boolean - el: HTMLElement = ediv({ class: ["channel"] }) - inner_el?: HTMLElement +// export abstract class Resource extends EventTarget { +// abstract transport_method: TransportMethod +// local: boolean +// el: HTMLElement = ediv({ class: ["channel"] }) +// inner_el?: HTMLElement - constructor( - public user: User, - public info: ProvideInfo, - ) { - super() - this.local = this.user instanceof LocalUser - const button = document.createElement("button") - button.onclick = () => { - this.state == "enabled" ? this.request_stop() : this.request() - } - this.addEventListener("statechange", () => { - if (this.user instanceof LocalUser) button.textContent = "End", button.disabled = false - else if (this.state == "enabled") button.textContent = "Disable", button.disabled = false - else if (this.state == "disabled") button.textContent = `Enable ${this.info.kind}`, button.disabled = false - else button.textContent = "Working…", button.disabled = true; - }) - this.dispatchEvent(new CustomEvent("statechange")) - this.el.append(button) - } +// constructor( +// public user: User, +// public info: ProvideInfo, +// ) { +// super() +// this.local = this.user instanceof LocalUser +// const button = document.createElement("button") +// button.onclick = () => { +// this.state == "enabled" ? this.request_stop() : this.request() +// } +// this.addEventListener("statechange", () => { +// if (this.local) button.textContent = "End", button.disabled = false +// else if (this.state == "enabled") button.textContent = "Disable", button.disabled = false +// else if (this.state == "disabled") button.textContent = `Enable ${this.info.kind}`, button.disabled = false +// else button.textContent = "Working…", button.disabled = true; +// }) +// this.dispatchEvent(new CustomEvent("statechange")) +// this.el.append(button) +// } - static create(user: User, info: ProvideInfo): Resource { - if (info.kind == "audio" || info.kind == "video") return new TrackResource(user, info) - else throw new Error("blub"); - } +// static create(user: User, info: ProvideInfo): Resource { +// } - private _state: ChannelState = "disabled" - get state() { return this._state } - set state(value: ChannelState) { - const old_value = this._state - this._state = value - if (value != old_value) this.dispatchEvent(new CustomEvent("statechange")) - } +// private _state: ChannelState = "disabled" +// get state() { return this._state } +// set state(value: ChannelState) { +// const old_value = this._state +// this._state = value +// if (value != old_value) this.dispatchEvent(new CustomEvent("statechange")) +// } - private _track?: TrackHandle - get track() { return this._track } - set track(value: TrackHandle | undefined) { - const handle_end = () => { - this.track = undefined - this.state = "disabled" - this.inner_el?.remove() - if (this.user instanceof LocalUser) this.destroy() - } - this._track?.removeEventListener("ended", handle_end) - this._track = value - if (value) this.el.append(this.inner_el = this.on_track(value)) - if (value) this.state = "enabled" - else this.state = "disabled" - this._track?.addEventListener("ended", handle_end) - } +// private _channel?: TrackHandle | RTCDataChannel +// get channel() { return this._channel } +// set channel(value: TrackHandle | RTCDataChannel | undefined) { +// const handle_end = () => { +// this.channel = undefined +// this.state = "disabled" +// this.inner_el?.remove() +// if (this.user instanceof LocalUser) this.destroy() +// } +// this._channel?.removeEventListener("ended", handle_end) +// this._channel = value +// if (value) this.el.append(this.inner_el = this.on_channel(value)) +// if (value) this.state = "enabled" +// else this.state = "disabled" +// this._channel?.addEventListener("ended", handle_end) +// } - abstract on_track(_track: TrackHandle): HTMLElement +// abstract on_channel(channel: TrackHandle | RTCDataChannel): HTMLElement +// abstract on_request(): void; - destroy() { this.dispatchEvent(new CustomEvent("destroy")) } +// destroy() { this.dispatchEvent(new CustomEvent("destroy")) } - request() { - if (!(this.user instanceof RemoteUser)) return - this.state = "await_enable" - this.user.send_to({ request: { id: this.info.id } }) - } - request_stop() { - if (this.user instanceof RemoteUser) { - this.state = "await_disable" - this.user.send_to({ request_stop: { id: this.info.id } }) - } else if (this.user instanceof LocalUser) { - this.destroy() - } - } +// request() { +// if (!(this.user instanceof RemoteUser)) return +// this.state = "await_enable" +// this.user.send_to({ request: { id: this.info.id } }) +// } +// request_stop() { +// if (this.user instanceof RemoteUser) { +// this.state = "await_disable" +// this.user.send_to({ request_stop: { id: this.info.id } }) +// } else if (this.user instanceof LocalUser) { +// this.destroy() +// } +// } +// } + +export type TransportMethod = "data-channel" | "track" +export type RemoteResourceState = "connected" | "disconnected" | "await_connect" | "await_disconnect" +export interface ResourceHandlerDecl { + kind: string + new_remote(info: ProvideInfo, user: RemoteUser, enable: () => void): RemoteResource +} +export interface RemoteResource { + el: HTMLElement + info: ProvideInfo, + on_statechange(state: RemoteResourceState): void + on_enable(t: TrackHandle | RTCDataChannel, disable: () => void): void +} +export interface LocalResource { + el: HTMLElement + info: ProvideInfo, + destroy(): void + on_request(user: RemoteUser, create_channel: (label: string) => RTCDataChannel): TrackHandle | RTCDataChannel } + +const RESOURCE_HANDLERS: ResourceHandlerDecl[] = [resource_file, resource_track] + +export function new_remote_resource(user: RemoteUser, info: ProvideInfo): RemoteResource | undefined { + const h = RESOURCE_HANDLERS.find(h => h.kind == info.kind) + if (!h) return undefined + const res = h.new_remote(info, user, () => { + user.request_resource(res) + }) + return res +}
\ No newline at end of file diff --git a/client-web/source/resource/track.ts b/client-web/source/resource/track.ts index 1ae6f94..0e99416 100644 --- a/client-web/source/resource/track.ts +++ b/client-web/source/resource/track.ts @@ -4,48 +4,138 @@ Copyright (C) 2022 metamuffin <metamuffin@disroot.org> */ /// <reference lib="dom" /> - import { ProvideInfo } from "../../../common/packets.d.ts"; +import { ebutton, ediv } from "../helper.ts"; +import { log } from "../logger.ts"; +import { on_pref_changed, PREFS } from "../preferences/mod.ts"; +import { get_rnnoise_node } from "../rnnoise.ts"; import { TrackHandle } from "../track_handle.ts"; -import { User } from "../user/mod.ts"; -import { Resource } from "./mod.ts"; +import { LocalResource, ResourceHandlerDecl } from "./mod.ts"; -export class TrackResource extends Resource { - constructor(user: User, info: ProvideInfo, track?: TrackHandle) { - super(user, info) - this.track = track +export const resource_track: ResourceHandlerDecl = { + kind: "track", + new_remote: (info, _user, enable) => { + const enable_button = ebutton("Enable", { + onclick: self => { + self.disabled = true; + self.textContent = "Awaiting track…"; + enable() + } + }) + return { + info, + el: ediv({}, enable_button), + on_statechange() { }, + on_enable(track, disable) { + this.el.removeChild(enable_button) + this.el.append(ebutton("Disable", { + onclick: (self) => { + disable() + this.el.appendChild(enable_button) + self.disabled = true + enable_button.disabled = false + enable_button.textContent = "Enable"; + self.remove() + } + })) + if (!(track instanceof TrackHandle)) return console.warn("aservuoivasretuoip"); + this.el.append(create_track_display(track)) + } + } } +} - destroy() { - this.track?.end() - super.destroy() +export function new_local_track(info: ProvideInfo, track: TrackHandle): LocalResource { + return { + info, + el: ediv({}, + create_track_display(track) + ), + destroy() { track.end() }, + on_request(_user, _create_channel) { + return track + } } +} - on_track(track: TrackHandle): HTMLElement { - const el = document.createElement("div") - const is_video = track.kind == "video" - const media_el = is_video ? document.createElement("video") : document.createElement("audio") - const stream = new MediaStream([track.track]) - media_el.srcObject = stream - media_el.classList.add("media") - media_el.autoplay = true - media_el.controls = true - if (track.local) media_el.muted = true - el.append(media_el) +function create_track_display(track: TrackHandle): HTMLElement { + const el = document.createElement("div") + const is_video = track.kind == "video" + const media_el = is_video ? document.createElement("video") : document.createElement("audio") + const stream = new MediaStream([track.track]) + media_el.srcObject = stream + media_el.classList.add("media") + media_el.autoplay = true + media_el.controls = true + if (track.local) media_el.muted = true + el.append(media_el) + track.addEventListener("ended", () => { + media_el.srcObject = null // TODO // TODO figure out why i wrote todo here + el.remove() + }) + return el +} - if (track.local) { - const end_button = document.createElement("button") - end_button.textContent = "End" - end_button.addEventListener("click", () => { - track?.end() - }) - el.append(end_button) +export async function create_camera_res() { + log("media", "requesting user media (camera)") + const user_media = await window.navigator.mediaDevices.getUserMedia({ + video: { + facingMode: { ideal: PREFS.camera_facing_mode }, + frameRate: { ideal: PREFS.video_fps }, + width: { ideal: PREFS.video_resolution } } - this.el.append(el) - track.addEventListener("ended", () => { - media_el.srcObject = null // TODO - el.remove() - }) - return el + }) + const t = new TrackHandle(user_media.getVideoTracks()[0], true) + return new_local_track({ id: t.id, kind: "track", track_kind: "video", label: "Camera" }, t) +} + +export async function create_screencast_res() { + log("media", "requesting user media (screen)") + const user_media = await window.navigator.mediaDevices.getDisplayMedia({ + video: { + frameRate: { ideal: PREFS.video_fps }, + width: { ideal: PREFS.video_resolution } + }, + }) + const t = new TrackHandle(user_media.getVideoTracks()[0], true) + return new_local_track({ id: t.id, kind: "track", track_kind: "video", label: "Screen" }, t) +} + +export async function create_mic_res() { + log("media", "requesting user media (audio)") + const user_media = await window.navigator.mediaDevices.getUserMedia({ + audio: { + channelCount: { ideal: 1 }, + noiseSuppression: { ideal: PREFS.rnnoise ? false : PREFS.native_noise_suppression }, + echoCancellation: { ideal: PREFS.echo_cancellation }, + autoGainControl: { ideal: PREFS.auto_gain_control }, + } + }) + const context = new AudioContext() + const source = context.createMediaStreamSource(user_media) + const destination = context.createMediaStreamDestination() + const gain = context.createGain() + gain.gain.value = PREFS.microphone_gain + const clear_gain_cb = on_pref_changed("microphone_gain", () => gain.gain.value = PREFS.microphone_gain) + + let rnnoise: RNNoiseNode; + if (PREFS.rnnoise) { + rnnoise = await get_rnnoise_node(context) + source.connect(rnnoise) + rnnoise.connect(gain) + } else { + source.connect(gain) } -}
\ No newline at end of file + gain.connect(destination) + + const t = new TrackHandle(destination.stream.getAudioTracks()[0], true) + t.addEventListener("ended", () => { + user_media.getTracks().forEach(t => t.stop()) + source.disconnect() + if (rnnoise) rnnoise.disconnect() + gain.disconnect() + clear_gain_cb() + destination.disconnect() + }) + return new_local_track({ id: t.id, kind: "track", track_kind: "audio", label: "Microphone" }, t) +} diff --git a/client-web/source/room.ts b/client-web/source/room.ts index a1a30b2..a685f1d 100644 --- a/client-web/source/room.ts +++ b/client-web/source/room.ts @@ -7,14 +7,12 @@ import { log } from "./logger.ts"; import { RemoteUser } from "./user/remote.ts"; -import { User } from "./user/mod.ts"; import { LocalUser } from "./user/local.ts"; import { ClientboundPacket, RelayMessage } from "../../common/packets.d.ts"; import { SignalingConnection } from "./protocol/mod.ts"; import { Chat } from "./chat.ts"; export class Room { - public users: Map<number, User> = new Map() public remote_users: Map<number, RemoteUser> = new Map() public local_user!: LocalUser public my_id!: number @@ -50,16 +48,13 @@ export class Room { log("ws", `<- [client leave]: `, packet); const p = packet.client_leave; log("*", `${p.id} left`); - this.users.get(p.id)!.leave() + this.remote_users.get(p.id)?.leave() } } relay_handler(sender_id: number, message: RelayMessage) { - const sender = this.users.get(sender_id) - log("ws", `<- [relay for ${sender?.display_name}]: `, message); - if (sender instanceof RemoteUser) { - sender.on_relay(message) - } else { - console.warn("we received a message for ourselves, the server might be broken"); - } + const sender = this.remote_users.get(sender_id) + if (!sender) return console.warn("sender invalid, somebody is not in sync"); + log("ws", `<- [relay from ${sender.display_name}]: `, message); + sender.on_relay(message) } }
\ No newline at end of file diff --git a/client-web/source/track_handle.ts b/client-web/source/track_handle.ts index b17d397..51e4371 100644 --- a/client-web/source/track_handle.ts +++ b/client-web/source/track_handle.ts @@ -5,6 +5,7 @@ */ /// <reference lib="dom" /> +/// We need this to adjust the way events are fired export class TrackHandle extends EventTarget { stream: MediaStream // this is used to create an id that is persistent across clients diff --git a/client-web/source/user/local.ts b/client-web/source/user/local.ts index 56ff9f0..8ec78d7 100644 --- a/client-web/source/user/local.ts +++ b/client-web/source/user/local.ts @@ -6,33 +6,31 @@ /// <reference lib="dom" /> import { log } from "../logger.ts"; -import { on_pref_changed, PREFS } from "../preferences/mod.ts"; import { RemoteUser } from "./remote.ts"; -import { get_rnnoise_node } from "../rnnoise.ts"; import { Room } from "../room.ts"; -import { TrackHandle } from "../track_handle.ts"; -import { User } from "./mod.ts"; -import { ediv } from "../helper.ts"; import { ChatMessage, ProvideInfo } from "../../../common/packets.d.ts"; -import { TrackResource } from "../resource/track.ts"; -import { Resource } from "../resource/mod.ts"; +import { User } from "./mod.ts"; +import { create_camera_res, create_mic_res, create_screencast_res } from "../resource/track.ts"; +import { LocalResource } from "../resource/mod.ts"; +import { PREFS } from "../preferences/mod.ts"; +import { ebutton } from "../helper.ts"; export class LocalUser extends User { + resources: Map<string, LocalResource> = new Map() + constructor(room: Room, id: number) { super(room, id) this.el.classList.add("local") - this.local = true this.name = PREFS.username - this.create_controls() - this.add_initial_tracks() log("usermodel", `added local user: ${this.display_name}`) + this.add_initial_tracks() } leave() { throw new Error("local users cant leave"); } add_initial_tracks() { - if (PREFS.microphone_enabled) this.await_add_resource(this.create_mic_res()) - if (PREFS.camera_enabled) this.await_add_resource(this.create_camera_res()) - if (PREFS.screencast_enabled) this.await_add_resource(this.create_screencast_res()) + if (PREFS.microphone_enabled) this.await_add_resource(create_mic_res()) + if (PREFS.camera_enabled) this.await_add_resource(create_camera_res()) + if (PREFS.screencast_enabled) this.await_add_resource(create_screencast_res()) } provide_initial_to_remote(u: RemoteUser) { @@ -49,101 +47,31 @@ export class LocalUser extends User { this.room.signaling.send_relay({ chat: message }) } - create_controls() { - const mic_toggle = document.createElement("input") - const camera_toggle = document.createElement("input") - const screen_toggle = document.createElement("input") - mic_toggle.type = camera_toggle.type = screen_toggle.type = "button" - mic_toggle.value = "Microphone" - camera_toggle.value = "Camera" - screen_toggle.value = "Screencast" - mic_toggle.addEventListener("click", () => this.await_add_resource(this.create_mic_res())) - camera_toggle.addEventListener("click", () => this.await_add_resource(this.create_camera_res())) - screen_toggle.addEventListener("click", () => this.await_add_resource(this.create_screencast_res())) - return ediv({ class: "local-controls" }, mic_toggle, camera_toggle, screen_toggle) - } - async await_add_resource(tp: Promise<Resource>) { - log("media", "awaiting track") - let t!: Resource; + async await_add_resource(tp: Promise<LocalResource>) { + log("media", "awaiting local resource") + let t!: LocalResource; try { t = await tp } - catch (_) { log("media", "request failed") } + catch (e) { log("media", `failed ${e.toString()}`) } if (!t) return - log("media", "got track") + log("media", "ok") this.add_resource(t) } - add_resource(r: Resource) { + add_resource(r: LocalResource) { this.resources.set(r.info.id, r) this.el.append(r.el) const provide: ProvideInfo = r.info this.room.signaling.send_relay({ provide }) - r.addEventListener("destroy", () => { - this.el.removeChild(r.el); - this.room.signaling.send_relay({ provide_stop: { id: r.info.id } }) - }) - } - async create_camera_res() { - log("media", "requesting user media (camera)") - const user_media = await window.navigator.mediaDevices.getUserMedia({ - video: { - facingMode: { ideal: PREFS.camera_facing_mode }, - frameRate: { ideal: PREFS.video_fps }, - width: { ideal: PREFS.video_resolution } - } - }) - const t = new TrackHandle(user_media.getVideoTracks()[0], true) - return new TrackResource(this, { id: t.id, kind: "video", label: "Camera" }, t) - } - - async create_screencast_res() { - log("media", "requesting user media (screen)") - const user_media = await window.navigator.mediaDevices.getDisplayMedia({ - video: { - frameRate: { ideal: PREFS.video_fps }, - width: { ideal: PREFS.video_resolution } - }, - }) - const t = new TrackHandle(user_media.getVideoTracks()[0], true) - return new TrackResource(this, { id: t.id, kind: "video", label: "Screen" }, t) - } + r.el.append( + ebutton("Stop", { + onclick: () => { + r.destroy() + this.el.removeChild(r.el); + this.room.signaling.send_relay({ provide_stop: { id: r.info.id } }) + } + }), - async create_mic_res() { - log("media", "requesting user media (audio)") - const user_media = await window.navigator.mediaDevices.getUserMedia({ - audio: { - channelCount: { ideal: 1 }, - noiseSuppression: { ideal: PREFS.rnnoise ? false : PREFS.native_noise_suppression }, - echoCancellation: { ideal: PREFS.echo_cancellation }, - autoGainControl: { ideal: PREFS.auto_gain_control }, - } - }) - const context = new AudioContext() - const source = context.createMediaStreamSource(user_media) - const destination = context.createMediaStreamDestination() - const gain = context.createGain() - gain.gain.value = PREFS.microphone_gain - const clear_gain_cb = on_pref_changed("microphone_gain", () => gain.gain.value = PREFS.microphone_gain) - - let rnnoise: RNNoiseNode; - if (PREFS.rnnoise) { - rnnoise = await get_rnnoise_node(context) - source.connect(rnnoise) - rnnoise.connect(gain) - } else { - source.connect(gain) - } - gain.connect(destination) - - const t = new TrackHandle(destination.stream.getAudioTracks()[0], true) - t.addEventListener("ended", () => { - user_media.getTracks().forEach(t => t.stop()) - source.disconnect() - if (rnnoise) rnnoise.disconnect() - gain.disconnect() - clear_gain_cb() - destination.disconnect() - }) - return new TrackResource(this, { id: t.id, kind: "audio", label: "Microphone" }, t) + ) } } diff --git a/client-web/source/user/mod.ts b/client-web/source/user/mod.ts index 2ff60a8..e83c2e6 100644 --- a/client-web/source/user/mod.ts +++ b/client-web/source/user/mod.ts @@ -5,41 +5,26 @@ */ /// <reference lib="dom" /> -import { epre, espan } from "../helper.ts"; +import { ediv, epre, espan } from "../helper.ts"; import { ROOM_CONTAINER } from "../index.ts"; -import { Resource } from "../resource/mod.ts"; -import { Room } from "../room.ts" +import { Room } from "../room.ts"; -export abstract class User { - public el: HTMLElement - public local = false - public resources: Map<string, Resource> = new Map() - - private name_el = espan("") - protected stats_el = epre("", { class: "stats" }) +export class User { private _name?: string + set name(v: string | undefined) { this._name = v; this.name_el.textContent = this.display_name } get name() { return this._name } - set name(n: string | undefined) { this._name = n; this.name_el.textContent = this.display_name } - get display_name() { return this.name ?? `unknown (${this.id})` } - - constructor(public room: Room, public id: number) { - room.users.set(this.id, this) + get display_name() { return this.name ?? "Unknown" } - this.el = document.createElement("div") - this.el.classList.add("user") - ROOM_CONTAINER.append(this.el) - this.setup_view() - } - leave() { - this.room.users.delete(this.id) - } + name_el = espan(this.display_name) + stats_el = epre("") + el = ediv({ class: "user" }) - setup_view() { - const info_el = document.createElement("div") - info_el.classList.add("info") + constructor(public room: Room, public id: number) { + const info_el = ediv({ class: "info" }) this.name_el.textContent = this.display_name this.name_el.classList.add("name") info_el.append(this.name_el, this.stats_el) this.el.append(info_el) + ROOM_CONTAINER.append(this.el) } }
\ No newline at end of file diff --git a/client-web/source/user/remote.ts b/client-web/source/user/remote.ts index fbab9c4..677d362 100644 --- a/client-web/source/user/remote.ts +++ b/client-web/source/user/remote.ts @@ -6,72 +6,72 @@ /// <reference lib="dom" /> import { RelayMessage } from "../../../common/packets.d.ts"; -import { Resource } from "../resource/mod.ts"; import { notify } from "../helper.ts"; import { ROOM_CONTAINER, RTC_CONFIG } from "../index.ts" import { log } from "../logger.ts" import { PREFS } from "../preferences/mod.ts"; +import { new_remote_resource, RemoteResource } from "../resource/mod.ts"; import { Room } from "../room.ts" import { TrackHandle } from "../track_handle.ts"; import { User } from "./mod.ts"; -import { TrackResource } from "../resource/track.ts"; export class RemoteUser extends User { - peer: RTCPeerConnection + pc: RTCPeerConnection senders: Map<string, RTCRtpSender> = new Map() data_channels: Map<string, RTCDataChannel> = new Map() + resources: Map<string, RemoteResource> = new Map() negotiation_busy = false constructor(room: Room, id: number) { super(room, id) - room.remote_users.set(this.id, this) + room.remote_users.set(id, this) log("usermodel", `added remote user: ${this.display_name}`) - this.peer = new RTCPeerConnection(RTC_CONFIG) - this.peer.onicecandidate = ev => { + this.pc = new RTCPeerConnection(RTC_CONFIG) + this.pc.onicecandidate = ev => { if (!ev.candidate) return room.signaling.send_relay({ ice_candidate: ev.candidate.toJSON() }, this.id) log("webrtc", `ICE candidate set`, ev.candidate) this.update_stats() } - this.peer.ontrack = ev => { + this.pc.ontrack = ev => { console.log(ev) const t = ev.track const id = ev.streams[0]?.id if (!id) { ev.transceiver.stop(); return log({ scope: "media", warn: true }, "got a track without stream") } const r = this.resources.get(id) if (!r) { ev.transceiver.stop(); return log({ scope: "media", warn: true }, "got an unassociated track") } - if (r instanceof TrackResource) r.track = new TrackHandle(t); - else { ev.transceiver.stop(); return log({ scope: "media", warn: true }, "got a track for a resource that should use data channel") } + r.on_enable(new TrackHandle(t), () => { + this.request_resource_stop(r) + }) + // else { ev.transceiver.stop(); return log({ scope: "media", warn: true }, "got a track for a resource that should use data channel") } log("media", `remote track: ${this.display_name}`, t) this.update_stats() } - this.peer.onnegotiationneeded = () => { + this.pc.onnegotiationneeded = () => { log("webrtc", `negotiation needed: ${this.display_name}`) - if (this.negotiation_busy && this.peer.signalingState == "stable") return + if (this.negotiation_busy && this.pc.signalingState == "stable") return this.offer() this.update_stats() } - this.peer.onicecandidateerror = () => { + this.pc.onicecandidateerror = () => { log({ scope: "webrtc", warn: true }, "ICE error") this.update_stats() } - this.peer.oniceconnectionstatechange = () => { this.update_stats() } - this.peer.onicegatheringstatechange = () => { this.update_stats() } - this.peer.onsignalingstatechange = () => { this.update_stats() } - this.peer.onconnectionstatechange = () => { this.update_stats() } + this.pc.oniceconnectionstatechange = () => { this.update_stats() } + this.pc.onicegatheringstatechange = () => { this.update_stats() } + this.pc.onsignalingstatechange = () => { this.update_stats() } + this.pc.onconnectionstatechange = () => { this.update_stats() } this.update_stats() } leave() { log("usermodel", `remove remote user: ${this.display_name}`) - this.peer.close() + this.pc.close() this.room.remote_users.delete(this.id) - super.leave() ROOM_CONTAINER.removeChild(this.el) if (PREFS.notify_leave) notify(`${this.display_name} left`) } - on_relay(message: RelayMessage) { if (message.chat) this.room.chat.add_message(this, message.chat) if (message.ice_candidate) this.add_ice_candidate(message.ice_candidate) @@ -83,12 +83,10 @@ export class RemoteUser extends User { } if (message.provide) { console.log(message.provide.id); - const d = Resource.create(this, message.provide) + const d = new_remote_resource(this, message.provide) if (!d) return - if (d instanceof TrackResource) { - if (d.info.kind == "video" && PREFS.optional_video_default_enable) d.request() - if (d.info.kind == "audio" && PREFS.optional_audio_default_enable) d.request() - } + if (d.info.kind == "track" && d.info.track_kind == "audio" && PREFS.optional_audio_default_enable) this.request_resource(d) + if (d.info.kind == "track" && d.info.track_kind == "video" && PREFS.optional_video_default_enable) this.request_resource(d) this.el.append(d.el) this.resources.set(message.provide.id, d) } @@ -99,53 +97,36 @@ export class RemoteUser extends User { if (message.request) { const r = this.room.local_user.resources.get(message.request.id) if (!r) return log({ scope: "*", warn: true }, "somebody requested an unknown resource") - if (r instanceof TrackResource) { - if (!r.track) throw new Error("local resources not avail"); - const sender = this.peer.addTrack(r.track.track, r.track.stream) - this.senders.set(r.track.id, sender) - r.track.addEventListener("end", () => { this.senders.delete(r.track?.id ?? "") }) - } + const channel = r.on_request(this, label => this.pc.createDataChannel(label)) + if (channel instanceof TrackHandle) { + const sender = this.pc.addTrack(channel.track, channel.stream) + this.senders.set(channel.id, sender) + channel.addEventListener("end", () => { this.senders.delete(r.info.id) }) + } else if (channel instanceof RTCDataChannel) { + this.data_channels.set(r.info.id, channel) + channel.addEventListener("close", () => this.data_channels.delete(r.info.id)) + } else throw new Error("unreachable"); } if (message.request_stop) { const sender = this.senders.get(message.request_stop.id) if (!sender) return log({ scope: "*", warn: true }, "somebody requested us to stop transmitting an unknown resource") - this.peer.removeTrack(sender) + this.pc.removeTrack(sender) } } send_to(message: RelayMessage) { this.room.signaling.send_relay(message, this.id) } + request_resource(r: RemoteResource) { this.send_to({ request: { id: r.info.id } }) } + request_resource_stop(r: RemoteResource) { this.send_to({ request_stop: { id: r.info.id } }) } - async update_stats() { - if (!PREFS.webrtc_debug) return - try { - const stats = await this.peer.getStats() - let stuff = ""; - stuff += `ice-conn=${this.peer.iceConnectionState}; ice-gathering=${this.peer.iceGatheringState}; ice-trickle=${this.peer.canTrickleIceCandidates}; signaling=${this.peer.signalingState};\n` - stats.forEach(s => { - console.log("stat", s); - if (s.type == "candidate-pair" && s.selected) { - //@ts-ignore spec is weird.... - if (!stats.get) return console.warn("no RTCStatsReport.get"); - //@ts-ignore spec is weird.... - const cpstat = stats.get(s.localCandidateId) - if (!cpstat) return console.warn("no stats"); - console.log("cp", cpstat); - stuff += `via ${cpstat.candidateType}:${cpstat.protocol}:${cpstat.address}\n` - } else if (s.type == "codec") { - stuff += `using ${s.codecType ?? "dec/enc"}:${s.mimeType}(${s.sdpFmtpLine})\n` - } - }) - this.stats_el.textContent = stuff - } catch (e) { - console.warn(e); - } + add_ice_candidate(candidate: RTCIceCandidateInit) { + this.pc.addIceCandidate(new RTCIceCandidate(candidate)) + this.update_stats() } - async offer() { this.negotiation_busy = true - const offer_description = await this.peer.createOffer() - await this.peer.setLocalDescription(offer_description) + const offer_description = await this.pc.createOffer() + await this.pc.setLocalDescription(offer_description) log("webrtc", `sent offer: ${this.display_name}`, { offer: offer_description.sdp }) this.send_to({ offer: offer_description.sdp }) } @@ -153,12 +134,12 @@ export class RemoteUser extends User { this.negotiation_busy = true log("webrtc", `got offer: ${this.display_name}`, { offer }) const offer_description = new RTCSessionDescription({ sdp: offer, type: "offer" }) - await this.peer.setRemoteDescription(offer_description) + await this.pc.setRemoteDescription(offer_description) this.answer() } async answer() { - const answer_description = await this.peer.createAnswer() - await this.peer.setLocalDescription(answer_description) + const answer_description = await this.pc.createAnswer() + await this.pc.setLocalDescription(answer_description) log("webrtc", `sent answer: ${this.display_name}`, { answer: answer_description.sdp }) this.send_to({ answer: answer_description.sdp }) this.negotiation_busy = false @@ -166,12 +147,34 @@ export class RemoteUser extends User { async on_answer(answer: string) { log("webrtc", `got answer: ${this.display_name}`, { answer }) const answer_description = new RTCSessionDescription({ sdp: answer, type: "answer" }) - await this.peer.setRemoteDescription(answer_description) + await this.pc.setRemoteDescription(answer_description) this.negotiation_busy = false } - add_ice_candidate(candidate: RTCIceCandidateInit) { - this.peer.addIceCandidate(new RTCIceCandidate(candidate)) - this.update_stats() + async update_stats() { + if (!PREFS.webrtc_debug) return + try { + const stats = await this.pc.getStats() + let stuff = ""; + stuff += `ice-conn=${this.pc.iceConnectionState}; ice-gathering=${this.pc.iceGatheringState}; ice-trickle=${this.pc.canTrickleIceCandidates}; signaling=${this.pc.signalingState};\n` + stats.forEach(s => { + console.log("stat", s); + if (s.type == "candidate-pair" && s.selected) { + //@ts-ignore trust me, this works + if (!stats.get) return console.warn("no RTCStatsReport.get"); + //@ts-ignore trust me, this works + const cpstat = stats.get(s.localCandidateId) + if (!cpstat) return console.warn("no stats"); + console.log("cp", cpstat); + stuff += `via ${cpstat.candidateType}:${cpstat.protocol}:${cpstat.address}\n` + } else if (s.type == "codec") { + stuff += `using ${s.codecType ?? "dec/enc"}:${s.mimeType}(${s.sdpFmtpLine})\n` + } + }) + this.stats_el.textContent = stuff + } catch (e) { + console.warn(e); + } } + }
\ No newline at end of file |