From 945115527f827c311005a3728d39cf6552625a06 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 1 Apr 2026 11:50:32 +0200 Subject: [PATCH] 0.1.1 --- Cargo.toml | 5 +- capabilities/default.json | 9 +- frontend/chat.ts | 198 ++++++++++++++++++++++++ frontend/index.html | 35 ++++- frontend/main.ts | 107 ++++++++++++- frontend/styles.css | 16 ++ frontend/ts/bindings/ClientWsMessage.ts | 3 + frontend/ts/bindings/PeerInfo.ts | 3 + frontend/ts/bindings/ServerWsMessage.ts | 4 + package.json | 5 +- src/lib.rs | 1 + tauri.conf.json | 2 +- vite.config.ts | 3 +- 13 files changed, 379 insertions(+), 12 deletions(-) create mode 100644 frontend/chat.ts create mode 100644 frontend/ts/bindings/ClientWsMessage.ts create mode 100644 frontend/ts/bindings/PeerInfo.ts create mode 100644 frontend/ts/bindings/ServerWsMessage.ts diff --git a/Cargo.toml b/Cargo.toml index c72c0df..7654313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-video03" -version = "0.1.0" +version = "0.1.1" description = "A Tauri Video App" authors = ["sinus@sasedev.net"] edition = "2024" @@ -28,10 +28,11 @@ serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0", features = [] } tauri = { version = "^2.10", features = ["default"] } tauri-plugin-opener = { version = "^2.5", features = [] } +tauri-plugin-websocket = "2" tokio = { version = "^1.50", features = ["full"] } tokio-tungstenite = { version = "^0.29", features = ["rustls", "tokio-rustls", "url"] } tracing = { version = "^0.1", features = ["async-await", "log"] } tracing-subscriber = { version = "^0.3", features = ["ansi", "env-filter", "chrono", "serde", "json"] } ts-rs = { version = "^12.0", features = [] } -uuid = { version = "^1.23", features = [] } +uuid = { version = "^1.23", features = ["v4"] } webrtc = { version = "^0.17", features = [] } diff --git a/capabilities/default.json b/capabilities/default.json index 4cdbf49..efe04f9 100644 --- a/capabilities/default.json +++ b/capabilities/default.json @@ -2,9 +2,12 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:default", - "opener:default" + "opener:default", + "websocket:default" ] -} +} \ No newline at end of file diff --git a/frontend/chat.ts b/frontend/chat.ts new file mode 100644 index 0000000..9d148ed --- /dev/null +++ b/frontend/chat.ts @@ -0,0 +1,198 @@ +import WebSocket from "@tauri-apps/plugin-websocket"; +import { PeerInfo } from './ts/bindings/PeerInfo'; +import { ClientWsMessage } from './ts/bindings/ClientWsMessage'; +import { ServerWsMessage } from './ts/bindings/ServerWsMessage'; + +type RemoveListener = (() => void) | null; + +export class LocalSignallingClient { + private socket: WebSocket | null = null; + private removeListener: RemoveListener = null; + private myPeerId: string | null = null; + private peers: PeerInfo[] = []; + + constructor( + private readonly displayName: string, + private readonly onLog: (message: string) => void, + private readonly onPeers: (peers: PeerInfo[]) => void, + private readonly onChat: (line: string) => void, + ) {} + + public async connect(): Promise { + if (this.socket !== null) { + this.onLog("signalling already connected"); + return; + } + + const url = "ws://127.0.0.1:3012"; + + this.onLog(`connecting to ${url}`); + + let socket: WebSocket; + try { + socket = await WebSocket.connect(url); + } catch (error) { + throw new Error(`websocket connection failed: ${String(error)}`); + } + + this.socket = socket; + + const removeListener = socket.addListener((message) => { + if (message.type === "Text") { + this.handleServerMessage(message.data); + return; + } + + if (message.type === "Close") { + this.onLog("signalling disconnected"); + this.cleanupDisconnectedState(); + return; + } + + if (message.type === "Binary") { + this.onLog("unexpected binary websocket message"); + return; + } + }); + + this.removeListener = removeListener; + + this.onLog("signalling connected"); + + await this.send({ + type: "hello", + display_name: this.displayName, + }); + } + + public async disconnect(): Promise { + if (this.socket === null) { + return; + } + + const socket = this.socket; + + if (this.removeListener !== null) { + this.removeListener(); + this.removeListener = null; + } + + this.socket = null; + + try { + await socket.disconnect(); + } catch (error) { + this.onLog(`disconnect error: ${String(error)}`); + } + + this.cleanupDisconnectedState(); + } + + public async sendChat(text: string): Promise { + await this.send({ + type: "chat_send", + text, + }); + } + + public getMyPeerId(): string | null { + return this.myPeerId; + } + + public getPeers(): PeerInfo[] { + return [...this.peers]; + } + + private async send(message: ClientWsMessage): Promise { + if (this.socket === null) { + this.onLog("cannot send: signalling not connected"); + return; + } + + const payload = JSON.stringify(message); + + try { + await this.socket.send(payload); + } catch (error) { + this.onLog(`send failed: ${String(error)}`); + } + } + + private cleanupDisconnectedState(): void { + this.socket = null; + this.myPeerId = null; + this.peers = []; + this.onPeers([]); + } + + private handleServerMessage(raw: string): void { + let message: ServerWsMessage; + + try { + message = JSON.parse(raw) as ServerWsMessage; + } catch (error) { + this.onLog(`invalid server message: ${String(error)}`); + return; + } + + switch (message.type) { + case "welcome": + this.myPeerId = message.peer_id; + this.onLog(`welcome peer_id=${message.peer_id}`); + break; + + case "peer_list": + this.peers = message.peers; + this.onPeers([...this.peers]); + this.onLog(`peer_list received (${message.peers.length})`); + break; + + case "peer_joined": + this.peers = mergePeer(this.peers, message.peer); + this.onPeers([...this.peers]); + this.onLog(`peer joined: ${message.peer.display_name}`); + break; + + case "peer_left": + this.peers = this.peers.filter((peer) => peer.peer_id !== message.peer_id); + this.onPeers([...this.peers]); + this.onLog(`peer left: ${message.peer_id}`); + break; + + case "chat_receive": + this.onChat(`${message.from_display_name}: ${message.text}`); + break; + + case "offer": + this.onLog(`offer received from ${message.from_peer_id}`); + break; + + case "answer": + this.onLog(`answer received from ${message.from_peer_id}`); + break; + + case "ice_candidate": + this.onLog(`ice candidate received from ${message.from_peer_id}`); + break; + + case "pong": + this.onLog("pong"); + break; + + case "error": + this.onLog(`server error: ${message.message}`); + break; + + default: + this.onLog(`unhandled server message: ${raw}`); + break; + } + } +} + +function mergePeer(peers: PeerInfo[], incoming: PeerInfo): PeerInfo[] { + const next = peers.filter((peer) => peer.peer_id !== incoming.peer_id); + next.push(incoming); + next.sort((a, b) => a.display_name.localeCompare(b.display_name)); + return next; +} diff --git a/frontend/index.html b/frontend/index.html index a6474d1..61aa395 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,12 +6,43 @@ -Tauri GST Record + Preview +Tauri GST Signalling + Chat
-

POC 2 - Recording + Preview

+

POC 3 - Chat

+ +
+

Signalling + Chat

+ +
+ + + +
+ +
+
+

Peers

+
[]
+
+ +
+

Logs

+
Ready.
+
+
+ +
+ + +
+ +
No messages yet.
+

Video Preview

diff --git a/frontend/main.ts b/frontend/main.ts index 1db63f6..4913b1b 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -2,6 +2,8 @@ import { invoke } from "@tauri-apps/api/core"; import { AvStartResponse } from './ts/bindings/AvStartResponse'; import { AvStopResponse } from './ts/bindings/AvStopResponse'; +import { LocalSignallingClient } from "./chat"; +import { PeerInfo } from './ts/bindings/PeerInfo'; type Mode = "idle" | "audio" | "video" | "av"; @@ -19,6 +21,15 @@ const avStatus = document.querySelector("#av-status"); const previewElement = document.querySelector("#video-preview"); +const chatDisplayNameInput = document.querySelector("#chat-display-name"); +const chatConnectBtn = document.querySelector("#chat-connect-btn"); +const chatDisconnectBtn = document.querySelector("#chat-disconnect-btn"); +const chatInput = document.querySelector("#chat-input"); +const chatSendBtn = document.querySelector("#chat-send-btn"); +const chatPeers = document.querySelector("#chat-peers"); +const chatLogs = document.querySelector("#chat-logs"); +const chatMessages = document.querySelector("#chat-messages"); + if ( startAudioBtn === null || stopAudioBtn === null || @@ -29,7 +40,15 @@ if ( startAvBtn === null || stopAvBtn === null || avStatus === null || - previewElement === null + previewElement === null || + chatDisplayNameInput === null || + chatConnectBtn === null || + chatDisconnectBtn === null || + chatInput === null || + chatSendBtn === null || + chatPeers === null || + chatLogs === null || + chatMessages === null ) { throw new Error("missing UI elements"); } @@ -320,3 +339,89 @@ stopAvBtn.addEventListener("click", async () => { }); setCurrentMode("idle"); + +let signallingClient: LocalSignallingClient | null = null; + +function appendChatLog(line: string): void { + if (chatLogs) { + const current = chatLogs.textContent ?? ""; + chatLogs.textContent = `${current}\n${line}`.trim(); + } +} + +function appendChatMessage(line: string): void { + if (chatMessages) { + const current = chatMessages.textContent ?? ""; + chatMessages.textContent = `${current}\n${line}`.trim(); + } +} + +function updatePeerList(peers: PeerInfo[]): void { + if (chatPeers) + chatPeers.textContent = JSON.stringify(peers, null, 2); +} + +function setChatConnected(connected: boolean): void { + if (chatConnectBtn) + chatConnectBtn.disabled = connected; + if (chatDisconnectBtn) + chatDisconnectBtn.disabled = !connected; + if (chatSendBtn) + chatSendBtn.disabled = !connected; +} + +chatConnectBtn.addEventListener("click", async () => { + if (signallingClient !== null) { + appendChatLog("signalling client already exists"); + return; + } + + const displayName = (chatDisplayNameInput.value || "").trim() || "anonymous"; + + const client = new LocalSignallingClient( + displayName, + (line) => appendChatLog(line), + (peers) => updatePeerList(peers), + (line) => appendChatMessage(line), + ); + + try { + await client.connect(); + signallingClient = client; + setChatConnected(true); + } catch (error) { + appendChatLog(`connect failed: ${String(error)}`); + signallingClient = null; + setChatConnected(false); + } +}); + +chatDisconnectBtn.addEventListener("click", () => { + if (signallingClient === null) { + return; + } + + signallingClient.disconnect(); + signallingClient = null; + setChatConnected(false); + updatePeerList([]); +}); + +chatSendBtn.addEventListener("click", () => { + if (signallingClient === null) { + appendChatLog("not connected"); + return; + } + + const text = chatInput.value.trim(); + if (text.length === 0) { + appendChatLog("chat message is empty"); + return; + } + + signallingClient.sendChat(text); + chatInput.value = ""; +}); + +setChatConnected(false); + diff --git a/frontend/styles.css b/frontend/styles.css index f0bbafc..600abb9 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -71,3 +71,19 @@ pre { padding: 12px; white-space: pre-wrap; } + +.chat-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; +} + +input[type="text"] { + border: 1px solid #374151; + border-radius: 8px; + padding: 10px 12px; + background: #0b1220; + color: #f9fafb; + min-width: 220px; +} diff --git a/frontend/ts/bindings/ClientWsMessage.ts b/frontend/ts/bindings/ClientWsMessage.ts new file mode 100644 index 0000000..4558f7a --- /dev/null +++ b/frontend/ts/bindings/ClientWsMessage.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ClientWsMessage = { "type": "hello", display_name: string, } | { "type": "chat_send", text: string, } | { "type": "offer", target_peer_id: string, sdp: string, } | { "type": "answer", target_peer_id: string, sdp: string, } | { "type": "ice_candidate", target_peer_id: string, candidate: string, sdp_mid: string | null, sdp_mline_index: number | null, } | { "type": "ping" }; diff --git a/frontend/ts/bindings/PeerInfo.ts b/frontend/ts/bindings/PeerInfo.ts new file mode 100644 index 0000000..8a925c1 --- /dev/null +++ b/frontend/ts/bindings/PeerInfo.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PeerInfo = { peer_id: string, display_name: string, }; diff --git a/frontend/ts/bindings/ServerWsMessage.ts b/frontend/ts/bindings/ServerWsMessage.ts new file mode 100644 index 0000000..b7e4fd2 --- /dev/null +++ b/frontend/ts/bindings/ServerWsMessage.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PeerInfo } from "./PeerInfo"; + +export type ServerWsMessage = { "type": "welcome", peer_id: string, } | { "type": "peer_list", peers: Array, } | { "type": "peer_joined", peer: PeerInfo, } | { "type": "peer_left", peer_id: string, } | { "type": "chat_receive", from_peer_id: string, from_display_name: string, text: string, } | { "type": "offer", from_peer_id: string, sdp: string, } | { "type": "answer", from_peer_id: string, sdp: string, } | { "type": "ice_candidate", from_peer_id: string, candidate: string, sdp_mid: string | null, sdp_mline_index: number | null, } | { "type": "pong" } | { "type": "error", message: string, }; diff --git a/package.json b/package.json index 8416386..44be8c9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tauri-video03", "private": true, - "version": "0.1.0", + "version": "0.1.1", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,8 @@ }, "dependencies": { "@tauri-apps/api": "^2.10", - "@tauri-apps/plugin-opener": "^2.5" + "@tauri-apps/plugin-opener": "^2.5", + "@tauri-apps/plugin-websocket": "^2.4" }, "devDependencies": { "@tauri-apps/cli": "^2.10", diff --git a/src/lib.rs b/src/lib.rs index f8b252e..e8f93d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ pub fn run() { init_gstreamer(); let builder = tauri::Builder::default() + .plugin(tauri_plugin_websocket::init()) .plugin(tauri_plugin_opener::init()) .manage(app_state::AppState::new()) .invoke_handler(tauri::generate_handler![ diff --git a/tauri.conf.json b/tauri.conf.json index 7856253..f8275ab 100644 --- a/tauri.conf.json +++ b/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "tauri-video03", - "version": "0.1.0", + "version": "0.1.1", "identifier": "com.sinus.tauri-video03", "build": { "beforeDevCommand": "npm run dev", diff --git a/vite.config.ts b/vite.config.ts index 79531e8..c884456 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,6 +7,7 @@ const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/ export default defineConfig(async () => ({ + envPrefix: ['VITE_', 'TAURI_ENV_*'], // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent Vite from obscuring rust errors @@ -61,7 +62,7 @@ export default defineConfig(async () => ({ // 2. tauri expects a fixed port, fail if that port is not available server: { port: 1420, - strictPort: true, + strictPort: false, host: host || false, hmr: host ? {