/** * @param {string} videoUrl * @param {{name: string, url: string}[]} subtitles */ const createVideoElement = (videoUrl, subtitles) => { document.querySelector("#pre-join-controls").style["display"] = "none"; const video = document.createElement("video"); video.controls = true; video.autoplay = false; const source = document.createElement("source"); source.src = videoUrl; video.appendChild(source); let first = true; for (const { name, url } of subtitles) { const track = document.createElement("track"); track.label = name; track.src = url; track.kind = "captions"; if (first) { track.default = true; first = false; } video.appendChild(track); } return video; } /** * @param {WebSocket} socket * @param {HTMLVideoElement} video */ const setupSocketEvents = (socket, video) => { const setVideoTime = time => { const timeSecs = time / 1000.0; if (Math.abs(video.currentTime - timeSecs) > 0.5) { video.currentTime = timeSecs; } } socket.addEventListener("message", async messageEvent => { try { const event = JSON.parse(messageEvent.data); console.log(event); switch (event.op) { case "SetPlaying": if (event.data.playing) { await video.play(); } else { video.pause(); } setVideoTime(event.data.time); break; case "SetTime": setVideoTime(event.data); break; } } catch (_err) { } }); } /** * @param {string} videoUrl * @param {{name: string, url: string}[]} subtitles * @param {number} currentTime * @param {boolean} playing * @param {WebSocket} socket */ const setupVideo = async (sessionId, videoUrl, subtitles, currentTime, playing, socket) => { const video = createVideoElement(videoUrl, subtitles); document.body.appendChild(video); video.currentTime = (currentTime / 1000.0); try { if (playing) { await video.play() } else { video.pause() } } catch (err) { // Auto-play is probably disabled, we should uhhhhhhh do something about it } video.addEventListener("pause", async event => { await fetch(`/sess/${sessionId}/playing`, { method: "PUT", body: JSON.stringify(false), headers: { "Content-Type": "application/json" }, }); }); video.addEventListener("play", async event => { await fetch(`/sess/${sessionId}/playing`, { method: "PUT", body: JSON.stringify(true), headers: { "Content-Type": "application/json" }, }); }); let firstSeekComplete = false; video.addEventListener("seeked", async event => { if (!firstSeekComplete) { // The first seeked event is performed by the browser when the video is loading firstSeekComplete = true; return; } await fetch(`/sess/${sessionId}/current_time`, { method: "PUT", body: JSON.stringify((video.currentTime * 1000) | 0), headers: { "Content-Type": "application/json" }, }); }); setupSocketEvents(socket, video); } /** @param {string} sessionId */ const joinSession = async (sessionId) => { try { window.location.hash = sessionId; const { video_url, subtitle_tracks, current_time_ms, is_playing } = await fetch(`/sess/${sessionId}`).then(r => r.json()); const wsUrl = new URL(`/sess/${sessionId}/subscribe`, window.location.href); wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; const socket = new WebSocket(wsUrl.toString()); setupVideo(sessionId, video_url, subtitle_tracks, current_time_ms, is_playing, socket); } catch (err) { // TODO: Show an error on the screen console.error(err); } } const main = () => { document.querySelector("#join-session-form").addEventListener("submit", event => { event.preventDefault(); const sessionId = document.querySelector("#join-session-id").value; joinSession(sessionId); }); if (window.location.hash.match(/#[0-9a-f\-]+/)) { document.querySelector("#join-session-id").value = window.location.hash.substring(1); } }; if (document.readyState === "complete") { main(); } else { document.addEventListener("DOMContentLoaded", main); }