summaryrefslogtreecommitdiff
path: root/client-web/source
diff options
context:
space:
mode:
Diffstat (limited to 'client-web/source')
-rw-r--r--client-web/source/chat.ts9
-rw-r--r--client-web/source/helper.ts24
-rw-r--r--client-web/source/index.ts4
-rw-r--r--client-web/source/keybinds.ts13
-rw-r--r--client-web/source/menu.ts10
-rw-r--r--client-web/source/preferences/decl.ts2
-rw-r--r--client-web/source/preferences/ui.ts11
-rw-r--r--client-web/source/resource/track.ts55
8 files changed, 95 insertions, 33 deletions
diff --git a/client-web/source/chat.ts b/client-web/source/chat.ts
index b7c572d..47b0678 100644
--- a/client-web/source/chat.ts
+++ b/client-web/source/chat.ts
@@ -6,7 +6,7 @@
/// <reference lib="dom" />
import { ChatMessage } from "../../common/packets.d.ts";
-import { ediv, espan, image_view, notify, OverlayUi } from "./helper.ts";
+import { ediv, esection, espan, image_view, notify, OverlayUi } from "./helper.ts";
import { log } from "./logger.ts";
import { PREFS } from "./preferences/mod.ts";
import { Room } from "./room.ts";
@@ -20,13 +20,15 @@ export class Chat extends OverlayUi {
constructor(public room: Room) {
const send = document.createElement("input")
+ send.ariaLabel = "send message"
send.type = "text"
+ send.placeholder = "send a message..."
- const messages = ediv({ class: "messages" })
+ const messages = ediv({ class: "messages", aria_live: "polite" })
const controls = ediv({ class: "controls" })
controls.append(send)
messages.append(document.createElement("hr"))
- super(ediv({ class: "chat" }, messages, controls))
+ super(esection({ class: "chat", aria_label: "chat", role: "dialog" }, messages, controls))
this.messages = messages
this.controls = controls
this.send_el = send
@@ -61,6 +63,7 @@ export class Chat extends OverlayUi {
}
}
+ on_show(): void { this.focus() }
focus() { this.send_el.focus() }
send(msg: ChatMessage) {
this.room.local_user.chat(msg)
diff --git a/client-web/source/helper.ts b/client-web/source/helper.ts
index c23beb6..1133afe 100644
--- a/client-web/source/helper.ts
+++ b/client-web/source/helper.ts
@@ -9,12 +9,25 @@ import { PREFS } from "./preferences/mod.ts";
const elem = <K extends keyof HTMLElementTagNameMap>(s: K): HTMLElementTagNameMap[K] => document.createElement(s)
-interface Opts<El> { class?: string[] | string, id?: string, src?: string, onclick?: (e: El) => void }
+interface Opts<El> {
+ class?: string[] | string,
+ id?: string, src?: string,
+ for?: string,
+ onclick?: (e: El) => void,
+ role?: "dialog"
+ aria_label?: string
+ aria_live?: "polite" | "assertive"
+ aria_modal?: boolean
+}
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 (o.aria_label) e.ariaLabel = o.aria_label
+ if (o.aria_live) e.ariaLive = o.aria_live
+ if (o.for) (e as unknown as HTMLLabelElement).htmlFor = o.for
+ if (o.aria_modal) e.ariaModal = "true"
if (typeof o?.class == "string") e.classList.add(o.class)
if (typeof o?.class == "object") e.classList.add(...o.class)
}
@@ -42,6 +55,9 @@ export const eh5 = elem_with_content("h5")
export const eh6 = elem_with_content("h6")
export const epre = elem_with_content("pre")
export const ediv = elem_with_children("div")
+export const efooter = elem_with_children("footer")
+export const esection = elem_with_children("section")
+export const enav = elem_with_children("nav")
export const etr = elem_with_children("tr")
export const etd = elem_with_children("td")
export const eth = elem_with_children("th")
@@ -59,10 +75,12 @@ export class OverlayUi {
}
get shown() { return this._shown }
set shown(v: boolean) {
- if (v && !this._shown) OVERLAYS.append(this.el)
- if (!v && this._shown) OVERLAYS.removeChild(this.el)
+ if (v && !this._shown) OVERLAYS.append(this.el), this.on_show()
+ if (!v && this._shown) OVERLAYS.removeChild(this.el), this.on_hide()
this._shown = v
}
+ on_show() { }
+ on_hide() { }
}
export function image_view(url: string, opts?: Opts<HTMLElement>): HTMLElement {
diff --git a/client-web/source/index.ts b/client-web/source/index.ts
index bc52f8b..cc3492f 100644
--- a/client-web/source/index.ts
+++ b/client-web/source/index.ts
@@ -6,7 +6,7 @@
/// <reference lib="dom" />
import { init_serviceworker } from "./sw/init.ts";
-import { ediv, OVERLAYS } from "./helper.ts";
+import { esection, OVERLAYS } from "./helper.ts";
import { setup_keybinds } from "./keybinds.ts";
import { log, LOGGER_CONTAINER } from "./logger.ts"
import { BottomMenu, MenuBr } from "./menu.ts";
@@ -15,7 +15,7 @@ import { SignalingConnection } from "./protocol/mod.ts";
import { Room } from "./room.ts"
export const VERSION = "0.1.12"
-export const ROOM_CONTAINER = ediv({ class: "room" })
+export const ROOM_CONTAINER = esection({ class: "room", aria_label: "user list" })
export const RTC_CONFIG: RTCConfiguration = {
iceServers: [
diff --git a/client-web/source/keybinds.ts b/client-web/source/keybinds.ts
index 5463e47..d096501 100644
--- a/client-web/source/keybinds.ts
+++ b/client-web/source/keybinds.ts
@@ -10,28 +10,23 @@ import { Room } from "./room.ts"
import { update_serviceworker } from "./sw/init.ts";
export function setup_keybinds(room: Room) {
- let command_mode = false
+ // let command_mode = false
document.body.addEventListener("keydown", ev => {
// TODO is there a proper solution?
if (ev.target instanceof HTMLInputElement && !(ev.target.type == "button")) return
if (ev.repeat) return
- if (ev.code == "Enter") {
+ if (ev.code == "Enter" && ev.ctrlKey) {
room.chat.shown = !room.chat.shown
if (room.chat.shown) room.chat.focus()
ev.preventDefault() // so focused buttons dont trigger
}
- if (ev.code == "Space") {
- command_mode = true
- ev.preventDefault() // so focused buttons dont trigger
- return
- }
- if (command_mode) {
+ if (ev.shiftKey) {
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())
if (ev.code == "KeyU") if (window.confirm("really update?")) update_serviceworker()
}
- command_mode = false
+ // command_mode = false
})
}
diff --git a/client-web/source/menu.ts b/client-web/source/menu.ts
index 9126dc5..035f3aa 100644
--- a/client-web/source/menu.ts
+++ b/client-web/source/menu.ts
@@ -5,7 +5,7 @@
*/
/// <reference lib="dom" />
-import { ebutton, ediv, ep, OverlayUi } from "./helper.ts"
+import { ebutton, ediv, efooter, enav, ep, OverlayUi } from "./helper.ts"
import { VERSION } from "./index.ts"
import { PrefUi } from "./preferences/ui.ts"
import { create_file_res } from "./resource/file.ts";
@@ -26,7 +26,7 @@ export class MenuBr extends OverlayUi {
return p
}
- super(ediv({ class: "menu-br" },
+ super(efooter({ class: "menu-br" },
ep(`keks-meet ${VERSION}`, { class: "version" }),
item("License", "https://codeberg.org/metamuffin/keks-meet/raw/branch/master/COPYING"),
item("Source code", "https://codeberg.org/metamuffin/keks-meet"),
@@ -41,6 +41,7 @@ export class BottomMenu extends OverlayUi {
const chat_toggle = document.createElement("input")
chat_toggle.type = "button"
chat_toggle.value = "Chat"
+ chat_toggle.ariaHasPopup = "menu"
chat_toggle.onclick = () => {
room.chat.shown = !room.chat.shown
if (room.chat.shown) chat_toggle.classList.add("active")
@@ -50,6 +51,7 @@ export class BottomMenu extends OverlayUi {
const prefs_button = document.createElement("input")
prefs_button.type = "button"
prefs_button.value = "Settings"
+ prefs_button.ariaHasPopup = "menu"
const prefs = new PrefUi()
prefs_button.onclick = () => {
@@ -58,13 +60,13 @@ export class BottomMenu extends OverlayUi {
else prefs_button.classList.remove("active")
}
- const local_controls = ediv({ class: "local-controls" },
+ const local_controls = ediv({ class: "local-controls", aria_label: "local resources" },
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))
+ super(enav({ class: "bottom-menu" }, chat_toggle, prefs_button, local_controls))
}
}
diff --git a/client-web/source/preferences/decl.ts b/client-web/source/preferences/decl.ts
index effd885..f3f8e84 100644
--- a/client-web/source/preferences/decl.ts
+++ b/client-web/source/preferences/decl.ts
@@ -32,6 +32,8 @@ export const PREF_DECLS = {
camera_facing_mode: { type: optional(string), possible_values: ["environment", "user"], description: "Prefer user-facing or env-facing camera" },
auto_gain_control: { type: bool, description: "Automatically adjust mic gain" },
echo_cancellation: { type: bool, description: "Cancel echo" },
+ audio_activity_threshold: { type: number, optional: true, default: 0.003, description: "Audio activity threshold" },
+
// TODO differenciate between mic, cam and screen
optional_audio_default_enable: { type: bool, default: true, description: "Enable audio tracks by default" },
optional_video_default_enable: { type: bool, default: false, description: "Enable video tracks by default" },
diff --git a/client-web/source/preferences/ui.ts b/client-web/source/preferences/ui.ts
index bc0d123..2b1d0c7 100644
--- a/client-web/source/preferences/ui.ts
+++ b/client-web/source/preferences/ui.ts
@@ -5,7 +5,7 @@
*/
/// <reference lib="dom" />
-import { ebr, ebutton, ediv, elabel, espan, etd, etr, OverlayUi } from "../helper.ts";
+import { ebr, ebutton, ediv, eh2, elabel, espan, etd, etr, OverlayUi } from "../helper.ts";
import { PREF_DECLS } from "./decl.ts";
import { change_pref, on_pref_changed, PrefDecl, PREFS } from "./mod.ts";
@@ -65,7 +65,7 @@ export class PrefUi extends OverlayUi {
if (decl.default === undefined || decl.optional) {
const use_opt = document.createElement("input")
use_opt.type = "checkbox"
- use_opt.id = id
+ use_opt.id = "enable-" + id
use_opt.checked = PREFS[key] !== undefined
if (prim_control) prim_control.disabled = !use_opt.checked
use_opt.onchange = () => {
@@ -79,10 +79,10 @@ export class PrefUi extends OverlayUi {
use_opt_ = use_opt;
}
- const label = elabel(decl.description ?? `[${key}]`, { id })
+ const label = elabel(decl.description ?? `[${key}]`, { for: id })
return etr({ class: "pref" }, etd({}, label), etd({}, use_opt_ ?? ""), etd({}, prim_control ?? ""))
})
-
+
const notification_perm = Notification.permission == "granted" ? ediv() : ediv({},
espan("For keks-meet to send notifications, it needs you to grant permission: "),
ebutton("Grant", { onclick: () => Notification.requestPermission() }),
@@ -95,7 +95,6 @@ export class PrefUi extends OverlayUi {
const table = document.createElement("table")
table.append(...rows)
- super(ediv({ class: "prefs-overlay" }, notification_perm, reset, ebr(), table))
+ super(ediv({ class: "prefs-overlay" }, eh2("Settings"), notification_perm, ebr(), table, ebr(), reset))
}
-
}
diff --git a/client-web/source/resource/track.ts b/client-web/source/resource/track.ts
index 7d53522..22af16f 100644
--- a/client-web/source/resource/track.ts
+++ b/client-web/source/resource/track.ts
@@ -5,7 +5,7 @@
*/
/// <reference lib="dom" />
import { ProvideInfo } from "../../../common/packets.d.ts";
-import { ebutton, ediv } from "../helper.ts";
+import { ebutton, ediv, elabel } from "../helper.ts";
import { log } from "../logger.ts";
import { on_pref_changed, PREFS } from "../preferences/mod.ts";
import { get_rnnoise_node } from "../rnnoise.ts";
@@ -48,11 +48,12 @@ export const resource_track: ResourceHandlerDecl = {
}
}
-export function new_local_track(info: ProvideInfo, track: TrackHandle): LocalResource {
+export function new_local_track(info: ProvideInfo, track: TrackHandle, ...extra_controls: HTMLElement[]): LocalResource {
return {
info,
el: ediv({},
- create_track_display(track)
+ create_track_display(track),
+ ...extra_controls
),
destroy() { track.end() },
on_request(_user, _create_channel) {
@@ -62,24 +63,55 @@ export function new_local_track(info: ProvideInfo, track: TrackHandle): LocalRes
}
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 is_audio = track.kind == "audio"
+
const stream = new MediaStream([track.track])
+
+ const el = document.createElement("div")
+
+ const media_el = is_video
+ ? document.createElement("video")
+ : document.createElement("audio")
+
media_el.srcObject = stream
media_el.classList.add("media")
media_el.autoplay = true
media_el.controls = true
media_el.addEventListener("pause", () => media_el.play())
+
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()
})
+
+ if (is_audio && PREFS.audio_activity_threshold !== undefined) check_volume(stream, vol => {
+ const active = vol > PREFS.audio_activity_threshold
+ if (active != el.classList.contains("audio-active")) {
+ if (active) el.classList.add("audio-active")
+ else el.classList.remove("audio-active")
+ }
+ })
+
return el
}
+function check_volume(track: MediaStream, cb: (vol: number) => void) {
+ const ctx = new AudioContext();
+ const s = ctx.createMediaStreamSource(track)
+ const a = ctx.createAnalyser()
+ s.connect(a)
+ const samples = new Float32Array(a.fftSize);
+ setInterval(() => {
+ a.getFloatTimeDomainData(samples);
+ let sum = 0.0;
+ for (const amplitude of samples) { sum += amplitude * amplitude; }
+ cb(Math.sqrt(sum / samples.length))
+ }, 1000 / 15)
+}
+
export async function create_camera_res() {
log("media", "requesting user media (camera)")
const user_media = await window.navigator.mediaDevices.getUserMedia({
@@ -141,5 +173,16 @@ export async function create_mic_res() {
clear_gain_cb()
destination.disconnect()
})
- return new_local_track({ id: t.id, kind: "track", track_kind: "audio", label: "Microphone" }, t)
+
+ const mute = document.createElement("input")
+ mute.type = "checkbox"
+ mute.onchange = () => {
+ log("media", mute.checked ? "muted" : "unmuted")
+ if (mute.checked) gain.gain.value = Number.MIN_VALUE
+ else gain.gain.value = PREFS.microphone_gain
+ }
+ const mute_label = elabel("Mute", { class: "check-button" })
+ mute_label.prepend(mute)
+
+ return new_local_track({ id: t.id, kind: "track", track_kind: "audio", label: "Microphone" }, t, mute_label)
}