From 4a6f4dd6d833a93fbea51aa7a18d4074159b5bd0 Mon Sep 17 00:00:00 2001 From: Sebastian Reimers Date: Wed, 28 May 2025 10:02:32 +0200 Subject: [PATCH 1/2] misc: add call feature --- include/mix.h | 1 + src/http.c | 50 +++++++++++++++ src/sess.c | 1 + src/users.c | 1 + webui/src/api.ts | 8 +++ webui/src/components/BottomActions.vue | 17 +++++- webui/src/components/Calls.vue | 84 ++++++++++++++++++++++++++ webui/src/views/HomeView.vue | 2 + webui/src/ws/users.ts | 36 +++++++++-- 9 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 webui/src/components/Calls.vue diff --git a/include/mix.h b/include/mix.h index e0ea7a4b..2649f838 100644 --- a/include/mix.h +++ b/include/mix.h @@ -64,6 +64,7 @@ struct user { bool audio; bool hand; bool solo; + bool calling; }; struct chat { diff --git a/src/http.c b/src/http.c index 715d4bbb..4240b0ee 100644 --- a/src/http.c +++ b/src/http.c @@ -740,6 +740,56 @@ static void http_req_handler(struct http_conn *conn, return; } + ROUTE("/api/v1/client/call", "POST") + { + if (sess->user) + sess->user->calling = true; + + err = slmix_session_user_updated(sess); + if (err) + goto err; + + http_sreply(conn, 204, "OK", "text/html", "", 0, NULL); + return; + } + + ROUTE("/api/v1/client/call", "DELETE") + { + char user[512] = {0}; + struct pl user_id = PL_INIT; + + err = re_regex((char *)mbuf_buf(msg->mb), + mbuf_get_left(msg->mb), "[a-zA-Z0-9@:]+", + &user_id); + if (err) + goto err; + + pl_strcpy(&user_id, user, sizeof(user)); + + struct session *sess_call = + slmix_session_lookup_user_id(&mix->sessl, &user_id); + if (!sess_call) { + warning("http/client/call/delete: user not found %r\n", + &user_id); + goto notfound; + } + + if (sess_call != sess) { + /* check host permission */ + if (!sess->user || !sess->user->host) + goto auth; + } + + sess_call->user->calling = false; + + err = slmix_session_user_updated(sess_call); + if (err) + goto err; + + http_sreply(conn, 204, "OK", "text/html", "", 0, NULL); + return; + } + ROUTE("/api/v1/client/hangup", "POST") { pc_close(sess); diff --git a/src/sess.c b/src/sess.c index 9c5a8926..0a2d1f80 100644 --- a/src/sess.c +++ b/src/sess.c @@ -534,6 +534,7 @@ int slmix_session_speaker(struct session *sess, bool enable) stream_enable(media_get_stream(sess->mvideo), enable); stream_enable_tx(media_get_stream(sess->mvideo), true); sess->user->hand = false; + sess->user->calling = false; /* only allow disable for privacy reasons */ if (!enable) diff --git a/src/users.c b/src/users.c index 1e1047ba..1c4aa92d 100644 --- a/src/users.c +++ b/src/users.c @@ -112,6 +112,7 @@ int user_event_json(char **json, enum user_event event, struct session *sess) odict_entry_add(o, "hand", ODICT_BOOL, sess->user->hand); odict_entry_add(o, "solo", ODICT_BOOL, sess->user->solo); odict_entry_add(o, "webrtc", ODICT_BOOL, sess->pc ? true : false); + odict_entry_add(o, "calling", ODICT_BOOL, sess->user->calling); err = re_sdprintf(json, "%H", json_encode_odict, o); diff --git a/webui/src/api.ts b/webui/src/api.ts index e456fba6..21e5a074 100644 --- a/webui/src/api.ts +++ b/webui/src/api.ts @@ -98,6 +98,14 @@ export default { Users.websocket() }, + async call() { + await api_fetch('POST', '/client/call', null) + }, + + async call_delete(user_id: string) { + await api_fetch('DELETE', '/client/call', user_id) + }, + async hangup() { Webrtc.hangup() await api_fetch('POST', '/client/hangup', null) diff --git a/webui/src/components/BottomActions.vue b/webui/src/components/BottomActions.vue index 202c7769..d1cb2085 100644 --- a/webui/src/components/BottomActions.vue +++ b/webui/src/components/BottomActions.vue @@ -67,7 +67,7 @@
+ +
+ +
+
+
+
+
+
+ Your call is + waiting... + +
+
+
+ +
+ + + + + +
+ +
+ +
+
+ + +
+
+
+
+ Call from {{ + user.name }} + +
+
+
+ + +
+
+
+
+
+
+ + + diff --git a/webui/src/views/HomeView.vue b/webui/src/views/HomeView.vue index 712ea83b..9c51285b 100644 --- a/webui/src/views/HomeView.vue +++ b/webui/src/views/HomeView.vue @@ -39,6 +39,7 @@ + @@ -50,6 +51,7 @@ import Listeners from '../components/Listeners.vue' import BottomActions from '../components/BottomActions.vue' import Chat from '../components/Chat.vue' import RecButton from '../components/RecButton.vue' +import Calls from '../components/Calls.vue' import api from '../api' import { onMounted } from 'vue' //import StudioNav from '../components/StudioNav.vue' diff --git a/webui/src/ws/users.ts b/webui/src/ws/users.ts index ab794a6c..9d6109c8 100644 --- a/webui/src/ws/users.ts +++ b/webui/src/ws/users.ts @@ -24,7 +24,7 @@ interface Source { solo: boolean } -interface User { +export interface User { id: string speaker_id: number pidx: number @@ -36,6 +36,7 @@ interface User { webrtc: boolean talk: boolean solo: boolean + calling: boolean stats: Stats } @@ -62,6 +63,7 @@ interface Users { socket?: WebSocket ws_close(): void websocket(): void + calls: Ref room: Ref rooms: Ref sources: Ref @@ -80,6 +82,7 @@ interface Users { host_status: Ref user_name: Ref emojis: Ref + user: Ref } function pad(num: number, size: number) { @@ -87,9 +90,27 @@ function pad(num: number, size: number) { return s.substring(s.length - size) } +const default_user: User = +{ + id: "0", + speaker_id: 0, + pidx: 0, + name: "", + host: false, + video: false, + audio: false, + hand: false, + webrtc: false, + solo: false, + talk: false, + calling: false, + stats: { artt: 0, vrtt: 0 } +} + export const Users: Users = { room: ref(undefined), rooms: ref([]), + calls: ref([]), sources: ref([]), speakers: ref([]), vspeakers: ref([]), @@ -106,6 +127,7 @@ export const Users: Users = { host_status: ref(false), user_name: ref(''), emojis: ref([]), + user: ref(default_user), ws_close() { this.socket?.close() @@ -113,12 +135,12 @@ export const Users: Users = { websocket() { this.socket = new WebSocket(config.ws_host() + config.base() + 'ws/v1/users') - this.socket.onerror = () => { - console.log('Websocket users error') + this.socket.onerror = (e) => { + console.log('Websocket users error', e) } this.socket.onclose = (e) => { - console.log('Websocket users closed', e.reason) + console.log('Websocket users closed', e) if (e.code === 1007) { api.logout() } @@ -163,6 +185,7 @@ export const Users: Users = { this.speakers.value = this.speakers.value?.filter((u) => u.id !== data.id) this.listeners.value = this.listeners.value?.filter((u) => u.id !== data.id) this.vspeakers.value = this.vspeakers.value?.filter((u) => u.id !== data.id) + this.calls.value = this.calls.value?.filter((u) => u.id !== data.id) if (data.event === 'deleted') return @@ -178,14 +201,19 @@ export const Users: Users = { hand: data.hand, webrtc: data.webrtc, solo: data.solo, + calling: data.calling, talk: false, stats: { artt: 0, vrtt: 0 } } + if (user.calling) + this.calls.value.push(user) + if (user.id === api.user_id()) { this.hand_status.value = user.hand this.speaker_status.value = data.speaker this.host_status.value = data.host + this.user.value = user /* Only allow remote disable */ if (!data.speaker) { From 6c69721d9dd3f539bac0d6e99ab480360857c5e5 Mon Sep 17 00:00:00 2001 From: Sebastian Reimers Date: Wed, 28 May 2025 10:44:24 +0200 Subject: [PATCH 2/2] tests: add call api test --- tests/phpunit/tests/ApiTest.php | 37 +++++++++++++++++++++++++++++++++ tests/phpunit/tests/Client.php | 4 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/ApiTest.php b/tests/phpunit/tests/ApiTest.php index 0e85ad62..8d75a445 100644 --- a/tests/phpunit/tests/ApiTest.php +++ b/tests/phpunit/tests/ApiTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Tests\Client; +use Tests\ClientAuth; use Tests\TestCase; use PHPUnit\Framework\Attributes\TestDox; @@ -165,4 +166,40 @@ public function test_chat() $this->assertEquals("test", $json->chats[0]->msg); } + + #[TestDox('POST/DELETE /api/v1/client/call')] + public function test_client_call() + { + $client = new Client(); + $client->login("Alice"); + + $host= new Client(); + $host->login("Bob", ClientAuth::Host); + + $client->ws_next(); /* connect websocket */ + $host->ws_next(); /* connect websocket */ + + $r = $client->post("/api/v1/client/call"); + $this->assertEquals(204, $r->getStatusCode()); + + $msg = $host->ws_next("user", "updated"); + $this->assertEquals("Alice", $msg->name); + $this->assertEquals(true, $msg->calling); + $user_id = $msg->id; + + $msg = $client->ws_next("user", "updated"); + $this->assertEquals("Alice", $msg->name); + $this->assertEquals(true, $msg->calling); + + $r = $host->delete("/api/v1/client/call", $user_id); + $this->assertEquals(204, $r->getStatusCode()); + + $msg = $host->ws_next("user", "updated"); + $this->assertEquals(false, $msg->calling); + $this->assertEquals($user_id, $msg->id); + + $msg = $client->ws_next("user", "updated"); + $this->assertEquals(false, $msg->calling); + $this->assertEquals($user_id, $msg->id); + } } diff --git a/tests/phpunit/tests/Client.php b/tests/phpunit/tests/Client.php index bedc0ece..1f8a551e 100644 --- a/tests/phpunit/tests/Client.php +++ b/tests/phpunit/tests/Client.php @@ -35,12 +35,12 @@ function get($url) ); } - function delete($url) + function delete($url, $body = NULL) { return $this->client->request( 'DELETE', $url, - ['cookies' => $this->cookies] + ['cookies' => $this->cookies, 'body' => $body] ); }