watch-party/frontend/lib/watch-session.mjs

269 lines
6.7 KiB
JavaScript

import { setupVideo } from "./video.mjs?v=bfdcf2";
import {
setupChat,
logEventToChat,
updateViewerList,
printChatMessage,
} from "./chat.mjs?v=bfdcf2";
import ReconnectingWebSocket from "./reconnecting-web-socket.mjs";
import { state } from "./state.mjs";
/**
* @param {string} sessionId
* @param {string} nickname
* @returns {ReconnectingWebSocket}
*/
const createWebSocket = () => {
const wsUrl = new URL(
`/sess/${state().sessionId}/subscribe` +
`?nickname=${encodeURIComponent(state().nickname)}` +
`&colour=${encodeURIComponent(state().colour)}`,
window.location.href
);
wsUrl.protocol = "ws" + window.location.protocol.slice(4);
const socket = new ReconnectingWebSocket(wsUrl);
return socket;
};
let outgoingDebounce = false;
let outgoingDebounceCallbackId = null;
export const setDebounce = () => {
outgoingDebounce = true;
if (outgoingDebounceCallbackId) {
cancelIdleCallback(outgoingDebounceCallbackId);
outgoingDebounceCallbackId = null;
}
outgoingDebounceCallbackId = setTimeout(() => {
outgoingDebounce = false;
}, 500);
};
export const setVideoTime = (time, video = null) => {
if (video == null) {
video = document.querySelector("video");
}
const timeSecs = time / 1000.0;
if (Math.abs(video.currentTime - timeSecs) > 0.5) {
video.currentTime = timeSecs;
}
};
export const setPlaying = async (playing, video = null) => {
if (video == null) {
video = document.querySelector("video");
}
if (playing) {
await video.play();
} else {
video.pause();
}
};
/**
* @param {HTMLVideoElement} video
* @param {ReconnectingWebSocket} socket
*/
const setupIncomingEvents = (video, socket) => {
socket.addEventListener("message", async (messageEvent) => {
try {
const event = JSON.parse(messageEvent.data);
if (!event.reflected) {
switch (event.op) {
case "SetPlaying":
setDebounce();
if (event.data.playing) {
await video.play();
} else {
video.pause();
}
setVideoTime(event.data.time, video);
break;
case "SetTime":
setDebounce();
setVideoTime(event.data, video);
break;
case "UpdateViewerList":
updateViewerList(event.data);
break;
}
}
logEventToChat(event);
} catch (_err) {}
});
};
/**
* @param {HTMLVideoElement} video
* @param {ReconnectingWebSocket} socket
*/
const setupOutgoingEvents = (video, socket) => {
const currentVideoTime = () => (video.currentTime * 1000) | 0;
video.addEventListener("pause", async (event) => {
if (outgoingDebounce || !video.controls) {
return;
}
// don't send a pause event for the video ending
if (video.currentTime == video.duration) {
return;
}
socket.send(
JSON.stringify({
op: "SetPlaying",
data: {
playing: false,
time: currentVideoTime(),
},
})
);
});
video.addEventListener("play", (event) => {
if (outgoingDebounce || !video.controls) {
return;
}
socket.send(
JSON.stringify({
op: "SetPlaying",
data: {
playing: true,
time: currentVideoTime(),
},
})
);
});
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;
}
if (outgoingDebounce || !video.controls) {
return;
}
socket.send(
JSON.stringify({
op: "SetTime",
data: {
to: currentVideoTime(),
},
})
);
});
};
export const joinSession = async () => {
if (state().activeSession) {
if (state().activeSession === state().sessionId) {
// we are already in this session, dont rejoin
return;
}
// we are joining a new session from an existing session
const messageContent = document.createElement("span");
messageContent.appendChild(document.createTextNode("joining new session "));
messageContent.appendChild(document.createTextNode(state().sessionId));
printChatMessage("join-session", "watch-party", "#fffff", messageContent);
}
state().activeSession = state().sessionId;
// try { // we are handling errors in the join form.
const genericConnectionError = new Error(
"There was an issue getting the session information."
);
window.location.hash = state().sessionId;
let response, video_url, subtitle_tracks, current_time_ms, is_playing;
try {
response = await fetch(`/sess/${state().sessionId}`);
} catch (e) {
console.error(e);
throw genericConnectionError;
}
if (!response.ok) {
let error;
try {
({ error } = await response.json());
if (!error) throw new Error();
} catch (e) {
console.error(e);
throw genericConnectionError;
}
throw new Error(error);
}
try {
({ video_url, subtitle_tracks, current_time_ms, is_playing } =
await response.json());
} catch (e) {
console.error(e);
throw genericConnectionError;
}
if (state().socket) {
state().socket.close();
state().socket = null;
}
const socket = createWebSocket();
state().socket = socket;
socket.addEventListener("open", async () => {
const video = await setupVideo(
video_url,
subtitle_tracks,
current_time_ms,
is_playing
);
// By default, we should disable video controls if the video is already playing.
// This solves an issue where Safari users join and seek to 00:00:00 because of
// outgoing events.
if (current_time_ms != 0) {
video.controls = false;
}
setupOutgoingEvents(video, socket);
setupIncomingEvents(video, socket);
setupChat(socket);
});
socket.addEventListener("reconnecting", (e) => {
console.log("Reconnecting...");
});
socket.addEventListener("reconnected", (e) => {
console.log("Reconnected.");
});
//} catch (e) {
// alert(e.message)
//}
};
/**
* @param {string} videoUrl
* @param {Array} subtitleTracks
*/
export const createSession = async (videoUrl, subtitleTracks) => {
const { id } = await fetch("/start_session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
video_url: videoUrl,
subtitle_tracks: subtitleTracks,
}),
}).then((r) => r.json());
window.location = `/?created=true#${id}`;
};