diff options
author | metamuffin <metamuffin@disroot.org> | 2022-09-16 17:16:38 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2022-09-16 17:16:38 +0200 |
commit | 969444b32101a45d5917a3947b94bb09c3fc01a1 (patch) | |
tree | ac6d17ad6f3a1535c619aef9eba83179cd4c0878 /client-web/source | |
parent | ff2329cac03703a10ce8b3793d707131403318b4 (diff) | |
download | keks-meet-969444b32101a45d5917a3947b94bb09c3fc01a1.tar keks-meet-969444b32101a45d5917a3947b94bb09c3fc01a1.tar.bz2 keks-meet-969444b32101a45d5917a3947b94bb09c3fc01a1.tar.zst |
optional channels (1)
Diffstat (limited to 'client-web/source')
-rw-r--r-- | client-web/source/keybinds.ts | 8 | ||||
-rw-r--r-- | client-web/source/preferences/mod.ts | 2 | ||||
-rw-r--r-- | client-web/source/resource/mod.ts | 48 | ||||
-rw-r--r-- | client-web/source/resource/track.ts | 44 | ||||
-rw-r--r-- | client-web/source/room.ts | 2 | ||||
-rw-r--r-- | client-web/source/user/local.ts | 68 | ||||
-rw-r--r-- | client-web/source/user/mod.ts | 49 | ||||
-rw-r--r-- | client-web/source/user/remote.ts | 64 |
8 files changed, 182 insertions, 103 deletions
diff --git a/client-web/source/keybinds.ts b/client-web/source/keybinds.ts index ad20d37..bbae1b8 100644 --- a/client-web/source/keybinds.ts +++ b/client-web/source/keybinds.ts @@ -18,10 +18,10 @@ export function setup_keybinds(room: Room) { return } if (command_mode) { - if (ev.code == "KeyM" || ev.code == "KeyR") room.local_user.publish_track(room.local_user.create_mic_track()) - if (ev.code == "KeyS") room.local_user.publish_track(room.local_user.create_screencast_track()) - if (ev.code == "KeyC" && !ev.ctrlKey) room.local_user.publish_track(room.local_user.create_camera_track()) - if (ev.code == "KeyC" && ev.ctrlKey) room.local_user.tracks.forEach(t => t.end()) + 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 == "KeyC" && ev.ctrlKey) room.local_user.resources.forEach(t => t.end()) } command_mode = false }) diff --git a/client-web/source/preferences/mod.ts b/client-web/source/preferences/mod.ts index f4fa551..ecff8fa 100644 --- a/client-web/source/preferences/mod.ts +++ b/client-web/source/preferences/mod.ts @@ -18,7 +18,7 @@ type TypeMapper = { "string": string, "number": number, "boolean": boolean } type PrefMap<T extends { [key: string]: { type: unknown } }> = { [Key in keyof T]: T[Key]["type"] } type Optional<T extends { [key: string]: unknown }> = { [Key in keyof T]?: T[Key] } export const { prefs: PREFS, explicit: PREFS_EXPLICIT } = register_prefs(PREF_DECLS) -const pref_change_handlers: Map<keyof typeof PREFS, Set<() => unknown>> = new Map() +const pref_change_handlers: Map<keyof typeof PREFS, Map<() => unknown>> = new Map() export const on_pref_changed = (key: keyof typeof PREFS, cb: () => unknown): (() => void) => { const m = (pref_change_handlers.get(key) ?? (() => { diff --git a/client-web/source/resource/mod.ts b/client-web/source/resource/mod.ts new file mode 100644 index 0000000..d437260 --- /dev/null +++ b/client-web/source/resource/mod.ts @@ -0,0 +1,48 @@ +import { ProvideInfo } from "../../../common/packets.d.ts" +import { ediv } from "../helper.ts" +import { log } from "../logger.ts" +import { User } from "../user/mod.ts" +import { RemoteUser } from "../user/remote.ts" +import { TrackResource } from "./track.ts" + +export type ChannelState = "running" | "disconnected" + +export abstract class Resource { + el: HTMLElement = ediv({ class: ["channel"] }) + constructor( + public user: User, + public info: ProvideInfo, + ) { + setTimeout(() => this.update_el(), 0) + } + + private _state: ChannelState = "disconnected" + get state() { return this._state } + set state(value: ChannelState) { + if (value != this._state) this.update_el() + this._state = value + } + + abstract create_element(): HTMLElement + abstract create_preview(): HTMLElement + + static create(user: User, info: ProvideInfo): Resource | undefined { + if (info.kind == "audio" || info.kind == "video") return new TrackResource(user, info) + if (info.kind == "file") throw new Error(""); + log({ scope: "media", warn: true }, "unknown resource kind") + } + + request() { + if (!(this.user instanceof RemoteUser)) return + this.user.send_to({ request: { id: this.info.id } }) + } + request_stop() { + if (!(this.user instanceof RemoteUser)) return + this.user.send_to({ request: { id: this.info.id } }) + } + + update_el() { + this.el.innerHTML = "" + this.el.append(this.create_element()) + } +} diff --git a/client-web/source/resource/track.ts b/client-web/source/resource/track.ts new file mode 100644 index 0000000..ee87917 --- /dev/null +++ b/client-web/source/resource/track.ts @@ -0,0 +1,44 @@ +import { ProvideInfo } from "../../../common/packets.d.ts"; +import { ebutton } from "../helper.ts"; +import { TrackHandle } from "../track_handle.ts"; +import { User } from "../user/mod.ts"; +import { Resource } from "./mod.ts"; + +export class TrackResource extends Resource { + constructor(user: User, info: ProvideInfo, public track?: TrackHandle) { + super(user, info) + } + + create_preview(): HTMLElement { + return ebutton("Enable", { onclick: () => this.request() }) + } + create_element() { + if (!this.track) { return this.create_preview() } + const el = document.createElement("div") + + const is_video = this.track.kind == "video" + const media_el = is_video ? document.createElement("video") : document.createElement("audio") + const stream = new MediaStream([this.track.track]) + media_el.srcObject = stream + media_el.classList.add("media") + media_el.autoplay = true + media_el.controls = true + if (this.track.local) media_el.muted = true + el.append(media_el) + + if (this.track.local) { + const end_button = document.createElement("button") + end_button.textContent = "End" + end_button.addEventListener("click", () => { + this.track?.end() + }) + el.append(end_button) + } + this.el.append(el) + this.track.addEventListener("ended", () => { + media_el.srcObject = null // TODO + el.remove() + }) + return el + } +}
\ No newline at end of file diff --git a/client-web/source/room.ts b/client-web/source/room.ts index 1fa8f86..168a2c2 100644 --- a/client-web/source/room.ts +++ b/client-web/source/room.ts @@ -38,7 +38,7 @@ export class Room { this.on_ready() } else { const ru = new RemoteUser(this, p.id) - this.local_user.add_initial_to_remote(ru) + this.local_user.provide_initial_to_remote(ru) this.local_user.identify(ru.id) } } else if (packet.client_leave) { diff --git a/client-web/source/user/local.ts b/client-web/source/user/local.ts index 9b34dba..4b057ee 100644 --- a/client-web/source/user/local.ts +++ b/client-web/source/user/local.ts @@ -7,12 +7,12 @@ import { get_rnnoise_node } from "../rnnoise.ts"; import { Room } from "../room.ts"; import { TrackHandle } from "../track_handle.ts"; import { User } from "./mod.ts"; -import { ROOM_CONTAINER } from "../index.ts"; import { ediv } from "../helper.ts"; -import { ChatMessage } from "../../../common/packets.d.ts"; +import { ChatMessage, ProvideInfo } from "../../../common/packets.d.ts"; +import { TrackResource } from "../resource/track.ts"; +import { Resource } from "../resource/mod.ts"; export class LocalUser extends User { - constructor(room: Room, id: number) { super(room, id) this.el.classList.add("local") @@ -22,28 +22,27 @@ export class LocalUser extends User { this.add_initial_tracks() log("usermodel", `added local user: ${this.display_name}`) } - leave() { // we might never need this but ok - this.room.local_user = undefined as unknown as LocalUser - super.leave() - ROOM_CONTAINER.removeChild(this.el) - } + leave() { throw new Error("local users cant leave"); } add_initial_tracks() { - if (PREFS.microphone_enabled) this.publish_track(this.create_mic_track()) - if (PREFS.camera_enabled) this.publish_track(this.create_camera_track()) - if (PREFS.screencast_enabled) this.publish_track(this.create_screencast_track()) + 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()) } - chat(message: ChatMessage) { - this.room.signaling.send_relay({ chat: message }) - } - - add_initial_to_remote(u: RemoteUser) { - this.tracks.forEach(t => u.peer.addTrack(t.track)) + provide_initial_to_remote(u: RemoteUser) { + this.resources.forEach(t => { + if (t instanceof TrackResource && t.track) + u.peer.addTrack(t.track.track) + }) } identify(recipient?: number) { if (this.name) this.room.signaling.send_relay({ identify: { username: this.name } }, recipient) } + chat(message: ChatMessage) { + this.room.signaling.send_relay({ chat: message }) + } + create_controls() { const mic_toggle = document.createElement("input") @@ -53,21 +52,30 @@ export class LocalUser extends User { mic_toggle.value = "Microphone" camera_toggle.value = "Camera" screen_toggle.value = "Screencast" - mic_toggle.addEventListener("click", () => this.publish_track(this.create_mic_track())) - camera_toggle.addEventListener("click", () => this.publish_track(this.create_camera_track())) - screen_toggle.addEventListener("click", () => this.publish_track(this.create_screencast_track())) + 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 publish_track(tp: Promise<TrackHandle>) { + async await_add_resource(tp: Promise<Resource>) { log("media", "awaiting track") - let t!: TrackHandle; + let t!: Resource; try { t = await tp } catch (_) { log("media", "request failed") } if (!t) return log("media", "got track") + this.add_resource(t) + } + + add_resource(r: Resource) { + this.resources.set(r.info.id, r) + this.el.append(r.el) + const provide: ProvideInfo = r.info + this.room.signaling.send_relay({ provide }) + } + send_track(t: TrackHandle) { this.room.remote_users.forEach(u => u.peer.addTrack(t.track)) - this.add_track(t) t.addEventListener("ended", () => { this.room.remote_users.forEach(u => { u.peer.getSenders().forEach(s => { @@ -77,7 +85,7 @@ export class LocalUser extends User { }) } - async create_camera_track() { + async create_camera_res() { log("media", "requesting user media (camera)") const user_media = await window.navigator.mediaDevices.getUserMedia({ video: { @@ -86,10 +94,11 @@ export class LocalUser extends User { width: { ideal: PREFS.video_resolution } } }) - return new TrackHandle(user_media.getVideoTracks()[0], true) + const t = new TrackHandle(user_media.getVideoTracks()[0], true) + return new TrackResource(this, { id: t.id, kind: "video", label: "Camera" }, t) } - async create_screencast_track() { + async create_screencast_res() { log("media", "requesting user media (screen)") const user_media = await window.navigator.mediaDevices.getDisplayMedia({ video: { @@ -97,10 +106,11 @@ export class LocalUser extends User { width: { ideal: PREFS.video_resolution } }, }) - return new TrackHandle(user_media.getVideoTracks()[0], true) + const t = new TrackHandle(user_media.getVideoTracks()[0], true) + return new TrackResource(this, { id: t.id, kind: "video", label: "Screen" }, t) } - async create_mic_track() { + async create_mic_res() { log("media", "requesting user media (audio)") const user_media = await window.navigator.mediaDevices.getUserMedia({ audio: { @@ -136,6 +146,6 @@ export class LocalUser extends User { clear_gain_cb() destination.disconnect() }) - return t + 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 581ac7e..59c58b7 100644 --- a/client-web/source/user/mod.ts +++ b/client-web/source/user/mod.ts @@ -2,15 +2,13 @@ import { epre, espan } from "../helper.ts"; import { ROOM_CONTAINER } from "../index.ts"; -import { log } from "../logger.ts" +import { Resource } from "../resource/mod.ts"; import { Room } from "../room.ts" -import { TrackHandle } from "../track_handle.ts"; - export abstract class User { protected el: HTMLElement public local = false - public tracks: Set<TrackHandle> = new Set() + public resources: Map<string, Resource> = new Map() private name_el = espan("") protected stats_el = epre("", { class: "stats" }) @@ -31,21 +29,6 @@ export abstract class User { this.room.users.delete(this.id) } - add_track(t: TrackHandle) { - this.tracks.add(t) - this.create_track_element(t) - t.addEventListener("ended", () => { - log("media", "track ended", t) - this.tracks.delete(t) - }) - t.addEventListener("mute", () => { - log("media", "track muted", t) - }) - t.addEventListener("unmute", () => { - log("media", "track unmuted", t) - }) - } - setup_view() { const info_el = document.createElement("div") info_el.classList.add("info") @@ -54,32 +37,4 @@ export abstract class User { info_el.append(this.name_el, this.stats_el) this.el.append(info_el) } - - create_track_element(t: TrackHandle) { - const is_video = t.kind == "video" - const media_el = is_video ? document.createElement("video") : document.createElement("audio") - const stream = new MediaStream([t.track]) - media_el.srcObject = stream - media_el.classList.add("media") - media_el.autoplay = true - media_el.controls = true - - if (this.local) media_el.muted = true - - const el = document.createElement("div") - if (t.local) { - const end_button = document.createElement("button") - end_button.textContent = "End" - end_button.addEventListener("click", () => { - t.end() - }) - el.append(end_button) - } - el.append(media_el) - this.el.append(el) - t.addEventListener("ended", () => { - media_el.srcObject = null - el.remove() - }) - } }
\ No newline at end of file diff --git a/client-web/source/user/remote.ts b/client-web/source/user/remote.ts index acd52ac..110fd40 100644 --- a/client-web/source/user/remote.ts +++ b/client-web/source/user/remote.ts @@ -1,16 +1,21 @@ /// <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 { Room } from "../room.ts" import { TrackHandle } from "../track_handle.ts"; -import { User } from "./mod.ts" +import { User } from "./mod.ts"; +import { TrackResource } from "../resource/track.ts"; export class RemoteUser extends User { peer: RTCPeerConnection + senders: Map<string, RTCRtpSender> = new Map() + data_channels: Map<string, RTCDataChannel> = new Map() + negotiation_busy = false constructor(room: Room, id: number) { @@ -28,7 +33,9 @@ export class RemoteUser extends User { this.peer.ontrack = ev => { const t = ev.track log("media", `remote track: ${this.display_name}`, t) - this.add_track(new TrackHandle(t)) + const r = this.resources.get(t.label) + if (r instanceof TrackResource) { r.track = new TrackHandle(t); r.state = "running" } + else log({ scope: "media", warn: true }, "got a track for a resource that should use data channel") this.update_stats() } this.peer.onnegotiationneeded = () => { @@ -38,26 +45,13 @@ export class RemoteUser extends User { this.update_stats() } this.peer.onicecandidateerror = () => { - console.log("onicecandidateerror") log({ scope: "webrtc", warn: true }, "ICE error") this.update_stats() } - this.peer.oniceconnectionstatechange = () => { - console.log("oniceconnectionstatechange") - this.update_stats() - } - this.peer.onicegatheringstatechange = () => { - console.log("onicegatheringstatechange") - this.update_stats() - } - this.peer.onsignalingstatechange = () => { - console.log("onsignalingstatechange") - this.update_stats() - } - this.peer.onconnectionstatechange = () => { - console.log("onconnectionstatechange") - 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.update_stats() } leave() { @@ -78,6 +72,34 @@ export class RemoteUser extends User { this.name = message.identify.username if (PREFS.notify_join) notify(`${this.display_name} joined`) } + if (message.provide) { + const d = Resource.create(this, message.provide) + if (!d) return + this.el.append(d.el) + this.resources.set(message.provide.id, d) + } + if (message.provide_stop) { + this.resources.get(message.provide_stop.id)?.el.remove() + this.resources.delete(message.provide_stop.id) + } + 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) + this.senders.set(r.track.id, sender) + r.track.addEventListener("end", () => { this.senders.delete(r.track?.id ?? "") }) + } + } + 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) + } + } + send_to(message: RelayMessage) { + this.room.signaling.send_relay(message, this.id) } async update_stats() { @@ -111,7 +133,7 @@ export class RemoteUser extends User { const offer_description = await this.peer.createOffer() await this.peer.setLocalDescription(offer_description) log("webrtc", `sent offer: ${this.display_name}`, { offer: offer_description.sdp }) - this.room.signaling.send_relay({ offer: offer_description.sdp }, this.id) + this.send_to({ offer: offer_description.sdp }) } async on_offer(offer: string) { this.negotiation_busy = true @@ -124,7 +146,7 @@ export class RemoteUser extends User { const answer_description = await this.peer.createAnswer() await this.peer.setLocalDescription(answer_description) log("webrtc", `sent answer: ${this.display_name}`, { answer: answer_description.sdp }) - this.room.signaling.send_relay({ answer: answer_description.sdp }, this.id) + this.send_to({ answer: answer_description.sdp }) this.negotiation_busy = false } async on_answer(answer: string) { |