// @ts-check
"use strict";
import * as Types from './types.js';
///// Status message /////
/** @typedef {"error" | "success" | "progress"} StatusType */
/** @type {HTMLElement | null} */
let statusElementVal = null;
/** @returns {HTMLElement} */
function getStatusElement() {
if (statusElementVal === null) {
const r = document.getElementById("statusMessage");
if (!(r instanceof HTMLElement)) {
throw new Error("Can't find status message area");
}
statusElementVal = r;
statusElementVal.innerText = "Скрипт загружен впервые";
}
return statusElementVal;
}
/**
* @param {string} msg
* @param {StatusType} type
* @returns {null}
*/
function logStatus(msg, type) {
console.log(msg);
const e = getStatusElement();
e.innerText = msg;
switch (type) {
case "error":
e.style.backgroundColor = "#ffaaaa";
e.style.borderColor = "#7f5555";
return null;
case "success":
e.style.backgroundColor = "#aaffaa";
e.style.borderColor = "#557f55";
return null;
case "progress":
e.style.backgroundColor = "#ffffaa";
e.style.borderColor = "#7f7f55";
return null;
}
}
/**
* @param {string} s
* @returns {never}
*/
function panic(s) {
logStatus(s, "error");
throw new Error("Panic: " + s);
}
/**
* @template A
* @param {A | null} x
* @param {string} s - error message
* @returns {A}
*/
function expect(x, s) {
if (x == null) {
panic("unwrap: " + s);
}
return x;
}
///// Main logic /////
async function enterFullscreen() {
if (document.fullscreenElement === null) {
const videoPlayer = document.getElementById("videoPlayer");
if (!(videoPlayer instanceof HTMLElement)) {
panic("Unexpected element");
}
await videoPlayer.requestFullscreen();
} else {
await document.exitFullscreen();
}
}
/** @ts-ignore */
window.enterFullscreen = enterFullscreen;
/** Open a connection to a websocket server and wait for the connection to
* succeed
*
* @param {string} url
* @returns {Promise<WebSocket>}
*/
function connectWs(url) {
return new Promise(resolve => {
const ws = new WebSocket(url);
/** @param {Event} _ev */
function onOpen(_ev) {
ws.removeEventListener("open", onOpen);
resolve(ws);
}
ws.addEventListener("open", onOpen);
});
}
/** Receive one message from the websocket
* @param {WebSocket} ws
* @returns {Promise<MessageEvent>}
*/
function recv(ws) {
return new Promise(resolve => {
/** @param {MessageEvent} msg */
function onMsg(msg) {
ws.removeEventListener("message", onMsg);
resolve(msg);
}
ws.addEventListener("message", onMsg);
});
}
/**
* @param {string} url
* @returns {Promise<{ws: WebSocket, roomId: string, myId: string}>}
*/
async function connectToServer(url) {
logStatus("Подключение к серверу..", "progress");
const ws = await connectWs(url);
logStatus("Подключение установлено, ожидание инициализации..", "progress");
const msgEvent = await recv(ws);
const firstMsg = Types.decodeServerMessage(msgEvent.data);
if (firstMsg === null || firstMsg.body.t != "identity") {
panic("Ошибка инициализации: некорректное сообщение от сервера");
}
logStatus("Соединение с сервером установлено", "success");
return {
ws: ws,
roomId: firstMsg.body.roomId,
myId: firstMsg.senderId,
};
}
/**
* @param {string} roomId
*/
async function awaitCall(roomId) {
const video = document.getElementById("video");
if (!(video instanceof HTMLVideoElement)) {
panic("Ошибка при поиске видеоэлемента на странице");
}
// Create connection to signaling server
const urlPort = window.location.port === "" ? "" : ":" + window.location.port;
const initMessage = await connectToServer("wss://" + window.location.hostname + urlPort);
const serverConnection = initMessage.ws;
serverConnection.send(Types.encodeServerMessage({
targetId: null,
body: {
t: "joinRoom",
roomId,
},
}));
const roomMessageEvent = await recv(serverConnection);
const roomMessage = Types.decodeServerMessage(roomMessageEvent.data);
if (roomMessage === null || roomMessage.body.t != "joinRoom") {
panic("Некорректное сообщение от сервера; невозможно подключиться к комнате");
}
if (roomMessage.body.status != "success") {
panic(`Невозможно подключиться к комнате. Ошибка: ${roomMessage.body.status}`);
}
logStatus("Подключение к комнате успешно, создание видеосвязи", "progress");
// Create webrtc connection
const configuration = {"iceServers": [{"urls": "stun:stun.l.google.com:19302"}]};
const peerConnection = new RTCPeerConnection(configuration);
peerConnection.onicecandidate = ev => {
if (ev.candidate != null) {
serverConnection.send(Types.encodeServerMessage({
targetId: null,
body: {
t: "ice",
ice: ev.candidate,
}
}));
}
};
peerConnection.onconnectionstatechange = ev => {
console.log(ev);
if (peerConnection.connectionState === "connected") {
logStatus("Видеосоединение установлено", "success");
}
};
peerConnection.ontrack = ev => {
// Set local video to whatever track we get
const firstStream = ev.streams[0];
if (firstStream != undefined) {
video.srcObject = firstStream;
}
}
// Handle signaling events
serverConnection.onmessage = async message => {
const signal = Types.decodeServerMessage(message.data);
if (!signal) {
console.error("Malformed message");
return;
}
if (!signal.senderId) {
console.error("Malformed message");
return;
}
if (signal.body.t === "sdp") {
console.debug("initiator sdp signal");
await peerConnection.setRemoteDescription(new RTCSessionDescription(signal.body.sdp));
if (signal.body.sdp.type !== "offer") {
// We're the responder, we're only interested in offers
return
}
// Create answer
const desc = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(desc);
serverConnection.send(Types.encodeServerMessage({
targetId: null,
body: {
t: "sdp",
sdp: expect(peerConnection.localDescription, "localDescription not set"),
},
}));
} else if (signal.body.t === "ice") {
console.debug("initiator ice signal");
peerConnection.addIceCandidate(new RTCIceCandidate(signal.body.ice));
} else if (signal.body.t === "leaveRoom") {
logStatus("Трансляция закончена", "success");
const liveIndicator = document.getElementById("liveIndicator");
if (!(liveIndicator instanceof HTMLElement) || liveIndicator === null) {
console.error("Ошибка при получении индикатора трансляции")
return;
}
liveIndicator.innerText = "Конец трансляции";
}
}
}
window.onload = async () => {
const query = new URLSearchParams(window.location.search);
const roomId = query.get("room");
if (roomId === null) {
panic("Комната не указана");
}
const roomElement = document.getElementById("roomName");
if (!(roomElement instanceof HTMLElement)) {
console.error("Ошибка при поиске подписи комнаты на странице");
} else {
roomElement.innerText = roomId;
}
await awaitCall(roomId);
}