aboutsummaryrefslogtreecommitdiff
path: root/client-web/source
diff options
context:
space:
mode:
Diffstat (limited to 'client-web/source')
-rw-r--r--client-web/source/keybinds.ts8
-rw-r--r--client-web/source/preferences/mod.ts2
-rw-r--r--client-web/source/resource/mod.ts48
-rw-r--r--client-web/source/resource/track.ts44
-rw-r--r--client-web/source/room.ts2
-rw-r--r--client-web/source/user/local.ts68
-rw-r--r--client-web/source/user/mod.ts49
-rw-r--r--client-web/source/user/remote.ts64
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) {