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"; const startAudioBtn = document.querySelector("#start-audio-btn"); const stopAudioBtn = document.querySelector("#stop-audio-btn"); const audioStatus = document.querySelector("#audio-status"); const startVideoBtn = document.querySelector("#start-video-btn"); const stopVideoBtn = document.querySelector("#stop-video-btn"); const videoStatus = document.querySelector("#video-status"); const startAvBtn = document.querySelector("#start-av-btn"); const stopAvBtn = document.querySelector("#stop-av-btn"); 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 || audioStatus === null || startVideoBtn === null || stopVideoBtn === null || videoStatus === null || startAvBtn === null || stopAvBtn === null || avStatus === 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"); } let currentMode: Mode = "idle"; let previewTimer: number | null = null; let previewRequestInFlight = false; let previewObjectUrl: string | null = null; function setAudioStatus(message: string): void { if (audioStatus) audioStatus.textContent = message; } function setVideoStatus(message: string): void { if (videoStatus) videoStatus.textContent = message; } function setAvStatus(message: string): void { if (avStatus) avStatus.textContent = message; } function setAudioButtons(isRecording: boolean): void { if (startAudioBtn) startAudioBtn.disabled = isRecording; if (stopAudioBtn) stopAudioBtn.disabled = !isRecording; } function setVideoButtons(isRecording: boolean): void { if (startVideoBtn) startVideoBtn.disabled = isRecording; if (stopVideoBtn) stopVideoBtn.disabled = !isRecording; } function setAvButtons(isRecording: boolean): void { if (startAvBtn) startAvBtn.disabled = isRecording; if (stopAvBtn) stopAvBtn.disabled = !isRecording; } function setAllButtonsForMode(mode: Mode): void { if (mode === "idle") { setAudioButtons(false); setVideoButtons(false); setAvButtons(false); return; } setAudioButtons(mode === "audio"); setVideoButtons(mode === "video"); setAvButtons(mode === "av"); if (mode !== "audio") { if (stopAudioBtn) stopAudioBtn.disabled = true; if (startAudioBtn) startAudioBtn.disabled = true; } if (mode !== "video") { if (stopVideoBtn) stopVideoBtn.disabled = true; if (startVideoBtn) startVideoBtn.disabled = true; } if (mode !== "av") { if (stopAvBtn) stopAvBtn.disabled = true; if (startAvBtn) startAvBtn.disabled = true; } } function setCurrentMode(mode: Mode): void { currentMode = mode; setAllButtonsForMode(mode); } function base64ToUint8Array(base64: string): Uint8Array { const binary = window.atob(base64); const bytes = new Uint8Array(binary.length); let index = 0; while (index < binary.length) { bytes[index] = binary.charCodeAt(index); index += 1; } return bytes; } function updatePreviewImageFromBase64(encoded: string): void { const bytes = base64ToUint8Array(encoded); const copied = new Uint8Array(bytes.byteLength); copied.set(bytes); const blob = new Blob([copied.buffer], { type: "image/jpeg" }); const objectUrl = URL.createObjectURL(blob); if (previewObjectUrl !== null) { URL.revokeObjectURL(previewObjectUrl); } previewObjectUrl = objectUrl; if (previewElement) previewElement.src = objectUrl; } async function refreshPreviewFrame(): Promise { if (previewRequestInFlight) { return; } if (currentMode !== "video" && currentMode !== "av") { return; } previewRequestInFlight = true; try { const encoded = await invoke("get_video_preview_frame_base64"); if (encoded !== null && encoded.length > 0) { updatePreviewImageFromBase64(encoded); } } catch (error) { console.error("preview refresh failed", error); } finally { previewRequestInFlight = false; } } function startPreviewPolling(): void { if (previewTimer !== null) { return; } window.setTimeout(() => { void refreshPreviewFrame(); }, 120); previewTimer = window.setInterval(() => { void refreshPreviewFrame(); }, 200); } function stopPreviewPolling(): void { if (previewTimer !== null) { window.clearInterval(previewTimer); previewTimer = null; } previewRequestInFlight = false; if (previewObjectUrl !== null) { URL.revokeObjectURL(previewObjectUrl); previewObjectUrl = null; } if (previewElement) previewElement.removeAttribute("src"); } startAudioBtn.addEventListener("click", async () => { if (currentMode !== "idle") { setAudioStatus("Another mode is already running."); return; } setCurrentMode("audio"); try { const path = await invoke("start_audio_recording"); setAudioStatus(`Audio recording started.\nOutput: ${path}`); } catch (error) { setAudioStatus(`Start audio failed.\n${String(error)}`); setCurrentMode("idle"); } }); stopAudioBtn.addEventListener("click", async () => { if (currentMode !== "audio") { setAudioStatus("Audio mode is not running."); return; } stopAudioBtn.disabled = true; try { const path = await invoke("stop_audio_recording"); setAudioStatus(`Audio recording stopped.\nSaved file: ${path}`); setCurrentMode("idle"); } catch (error) { setAudioStatus(`Stop audio failed.\n${String(error)}`); setCurrentMode("audio"); } }); startVideoBtn.addEventListener("click", async () => { if (currentMode !== "idle") { setVideoStatus("Another mode is already running."); return; } setCurrentMode("video"); try { const path = await invoke("start_video_recording"); setVideoStatus(`Video recording started.\nOutput: ${path}`); startPreviewPolling(); } catch (error) { setVideoStatus(`Start video failed.\n${String(error)}`); stopPreviewPolling(); setCurrentMode("idle"); } }); stopVideoBtn.addEventListener("click", async () => { if (currentMode !== "video") { setVideoStatus("Video mode is not running."); return; } stopVideoBtn.disabled = true; try { const path = await invoke("stop_video_recording"); setVideoStatus(`Video recording stopped.\nSaved file: ${path}`); stopPreviewPolling(); setCurrentMode("idle"); } catch (error) { setVideoStatus(`Stop video failed.\n${String(error)}`); setCurrentMode("video"); } }); startAvBtn.addEventListener("click", async () => { if (currentMode !== "idle") { setAvStatus("Another mode is already running."); return; } setCurrentMode("av"); try { const result = await invoke("start_av_recording"); setAvStatus( `AV recording started.\nAudio: ${result.audio_path}\nVideo: ${result.video_path}` ); startPreviewPolling(); } catch (error) { setAvStatus(`Start AV failed.\n${String(error)}`); stopPreviewPolling(); setCurrentMode("idle"); } }); stopAvBtn.addEventListener("click", async () => { if (currentMode !== "av") { setAvStatus("AV mode is not running."); return; } stopAvBtn.disabled = true; try { const result = await invoke("stop_av_recording"); setAvStatus( `AV recording stopped.\nAudio: ${result.audio_path}\nVideo: ${result.video_path}` ); stopPreviewPolling(); setCurrentMode("idle"); } catch (error) { setAvStatus(`Stop AV failed.\n${String(error)}`); setCurrentMode("av"); } }); 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);