summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2022-10-03 11:28:16 +0200
committermetamuffin <metamuffin@disroot.org>2022-10-03 11:28:16 +0200
commit4e99a3325318c902cd78ea9f760f46d79acde5c0 (patch)
treecc2bc54f4a0eb27db2b5d38dfbb785c1e9b84bd6
parentfa44b02da29a0bd1b60026d4f6ffd6c9748a09da (diff)
downloadkeks-meet-4e99a3325318c902cd78ea9f760f46d79acde5c0.tar
keks-meet-4e99a3325318c902cd78ea9f760f46d79acde5c0.tar.bz2
keks-meet-4e99a3325318c902cd78ea9f760f46d79acde5c0.tar.zst
riesencommit (part 1)
-rw-r--r--client-web/source/chat.ts3
-rw-r--r--client-web/source/helper.ts12
-rw-r--r--client-web/source/index.ts2
-rw-r--r--client-web/source/keybinds.ts7
-rw-r--r--client-web/source/menu.ts15
-rw-r--r--client-web/source/resource/file.ts44
-rw-r--r--client-web/source/resource/mod.ts167
-rw-r--r--client-web/source/resource/track.ts160
-rw-r--r--client-web/source/room.ts15
-rw-r--r--client-web/source/track_handle.ts1
-rw-r--r--client-web/source/user/local.ts124
-rw-r--r--client-web/source/user/mod.ts37
-rw-r--r--client-web/source/user/remote.ts133
-rw-r--r--common/packets.d.ts4
-rw-r--r--readme.md6
-rw-r--r--server/src/room.rs4
16 files changed, 404 insertions, 330 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
diff --git a/common/packets.d.ts b/common/packets.d.ts
index c8f65d6..7991e21 100644
--- a/common/packets.d.ts
+++ b/common/packets.d.ts
@@ -39,9 +39,11 @@ export interface /* enum */ RelayMessage {
ice_candidate?: F_RTCIceCandidateInit,
}
export interface ChatMessage { text?: string, image?: string }
+export type ResourceKind = "track" | "file"
export interface ProvideInfo {
id: string, // for datachannels this is `label`, for tracks this will be the `id` of the only associated stream.
- kind: "audio" | "video" | "file"
+ kind: ResourceKind
+ track_kind?: "audio" | "video"
label?: string
size?: number
}
diff --git a/readme.md b/readme.md
index 8b90250..11e4ecb 100644
--- a/readme.md
+++ b/readme.md
@@ -41,8 +41,8 @@ If you use this project or have any suggestions, please
_Rift_ is similar to the
[magic wormhole](https://github.com/magic-wormhole/magic-wormhole), except that
-is peer-to-peer. It reuses the keks-meet signaling server to establish a WebRTC
-data channel.
+it's peer-to-peer. It reuses the keks-meet signaling server to establish a
+WebRTC data channel.
```sh
pacman -S --needed rustup; rustup install nightly
@@ -129,6 +129,8 @@ system works as follows:
- Prevent join notification bypass by not identifying
- Tray icon for native
- Pin js by bookmarking data:text/html loader page
+- convert protocol enums to `A | B | C`
+- add "contributing" stuff to readme
## Protocol
diff --git a/server/src/room.rs b/server/src/room.rs
index 61d978e..e0c2239 100644
--- a/server/src/room.rs
+++ b/server/src/room.rs
@@ -6,7 +6,7 @@
use crate::protocol::{ClientboundPacket, ServerboundPacket};
use futures_util::{SinkExt, StreamExt, TryFutureExt};
use log::{debug, error};
-use std::{collections::HashMap, sync::atomic::AtomicUsize};
+use std::{collections::HashMap, sync::atomic::AtomicUsize, time::Duration};
use tokio::sync::{mpsc, RwLock};
use warp::ws::{Message, WebSocket};
@@ -109,6 +109,8 @@ impl Room {
ServerboundPacket::Ping => (),
ServerboundPacket::Relay { recipient, message } => {
let packet = ClientboundPacket::Message { sender, message };
+ // Add some delay for testing scenarios with latency.
+ // tokio::time::sleep(Duration::from_millis(1000)).await;
if let Some(recipient) = recipient {
self.send_to_client(recipient, packet).await;
} else {