aboutsummaryrefslogtreecommitdiff
path: root/client-web/source/user
diff options
context:
space:
mode:
Diffstat (limited to 'client-web/source/user')
-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
3 files changed, 105 insertions, 189 deletions
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