Files
linearvoid/frontend/src/main.ts
T

197 lines
5.5 KiB
TypeScript

import "./style.css";
type HlsClass = (typeof import("hls.js"))["default"];
declare global {
interface Window {
switchRoom: (room: string) => void;
sendMessage: () => void;
togglePrivateDM: () => void;
toggleChatPanel: () => void;
}
}
let hls: InstanceType<HlsClass> | null = null;
let ws: WebSocket | null = null;
let currentRoom = "main";
let username = "viewer" + Math.floor(Math.random() * 1000);
let chatVisible = true;
function resolveWebSocketBaseUrl() {
const configuredUrl = import.meta.env.VITE_PUBLIC_WS_URL?.trim();
if (configuredUrl) {
return configuredUrl;
}
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${window.location.host}/ws`;
}
function resolveManifestUrl() {
const configuredUrl = import.meta.env.VITE_PUBLIC_HLS_URL?.trim();
const cacheBust = `stream=${Date.now()}`;
const sameOriginManifestUrl = new URL(
"/hls/playlist.m3u8",
window.location.origin,
);
sameOriginManifestUrl.searchParams.set("stream", cacheBust);
const allowCrossOriginManifest =
import.meta.env.VITE_PUBLIC_HLS_CROSS_ORIGIN?.trim().toLowerCase() ===
"true";
if (configuredUrl) {
const manifestUrl = new URL(configuredUrl);
manifestUrl.searchParams.set("stream", cacheBust);
if (
!allowCrossOriginManifest &&
manifestUrl.origin !== window.location.origin
) {
return sameOriginManifestUrl.toString();
}
return manifestUrl.toString();
}
return sameOriginManifestUrl.toString();
}
function connectWebSocket(room: string) {
if (ws) ws.close();
const socketUrl = new URL(resolveWebSocketBaseUrl());
socketUrl.searchParams.set("username", username);
socketUrl.searchParams.set("room", room);
ws = new WebSocket(socketUrl);
ws.onopen = () => {
console.log(`Connected to chat room: ${room}`);
addMessage("system", "Connected to " + room);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp);
};
ws.onclose = () => {
console.log("Chat disconnected. Reconnecting in 3s...");
setTimeout(() => connectWebSocket(room), 3000);
};
}
function addMessage(
type: string,
text: string,
sender = "",
timestamp = new Date(),
) {
const messages = document.getElementById("messages")!;
const div = document.createElement("div");
div.className = "message-row";
if (type === "system") {
div.classList.add("message-system-row");
div.innerHTML = `<div class="message-system">${text}</div>`;
} else {
div.classList.add(
sender === username ? "message-self-row" : "message-other-row",
);
div.innerHTML = `
<article class="message-bubble ${sender === username ? "message-self" : "message-other"}">
${sender && sender !== username ? `<div class="message-sender">${sender}</div>` : ""}
<div class="message-text">${text}</div>
<time class="message-time">${new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</time>
</article>
`;
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
async function initPlayerAndChat() {
const video = document.getElementById("player") as HTMLVideoElement;
const manifestUrl = resolveManifestUrl();
if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = manifestUrl;
video.addEventListener("loadedmetadata", () => video.play());
} else {
try {
const { default: Hls } = await import("hls.js");
if (Hls.isSupported()) {
hls = new Hls();
hls.loadSource(manifestUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play());
}
} catch (error) {
console.error("Failed to load HLS player", error);
addMessage("system", "Unable to initialize video player.");
}
}
connectWebSocket("main");
console.log(
"Linear Void Frontend initialized - HLS Player + Chat Sidebar ready",
);
}
function toggleChatPanel() {
const shell = document.querySelector(".viewer-shell");
const toggle = document.getElementById("chat-toggle");
if (!shell || !toggle) return;
chatVisible = !chatVisible;
shell.classList.toggle("chat-collapsed", !chatVisible);
toggle.textContent = chatVisible ? "Hide Chat" : "Show Chat";
toggle.setAttribute("aria-expanded", String(chatVisible));
}
function sendMessage() {
const input = document.getElementById("message-input") as HTMLInputElement;
const text = input.value.trim();
if (!text || !ws) return;
const msg = {
room: currentRoom,
username: username,
text: text,
type: currentRoom.startsWith("dm:") ? "private" : "public",
};
ws.send(JSON.stringify(msg));
input.value = "";
}
function switchRoom(room: string) {
currentRoom = room;
document.getElementById("room-name")!.textContent =
room === "main"
? "Main Channel"
: room.charAt(0).toUpperCase() + room.slice(1) + " Room";
addMessage("system", `Switched to ${room} room`);
connectWebSocket(room);
}
function togglePrivateDM() {
const target = prompt("Enter username to start private chat with:");
if (target && target !== username) {
const dmRoom = `dm:${[username, target].sort().join("-")}`;
switchRoom(dmRoom);
}
}
window.switchRoom = switchRoom;
window.sendMessage = sendMessage;
window.togglePrivateDM = togglePrivateDM;
window.toggleChatPanel = toggleChatPanel;
document.addEventListener("DOMContentLoaded", () => {
void initPlayerAndChat();
});