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

271 lines
6.4 KiB
JavaScript

import { setupVideo } from "./video.mjs?v=4b61c4";
import {
setupChat,
logEventToChat,
updateViewerList,
printChatMessage,
} from "./chat.mjs?v=4b61c4";
import ReconnectingWebSocket from "./reconnecting-web-socket.mjs";
import { state } from "./state.mjs";
let player;
/**
* @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) => {
const timeSecs = time / 1000.0;
if (Math.abs(player.currentTime - timeSecs) > 0.5) {
player.currentTime = timeSecs;
}
};
export const setPlaying = async (playing) => {
if (playing) {
await player.play();
} else {
player.pause();
}
};
/**
* @param {HTMLVideoElement} video
* @param {ReconnectingWebSocket} socket
*/
const setupIncomingEvents = (player, 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 player.play();
} else {
player.pause();
}
setVideoTime(event.data.time);
break;
case "SetTime":
setDebounce();
setVideoTime(event.data);
break;
case "UpdateViewerList":
updateViewerList(event.data);
break;
}
}
logEventToChat(event);
} catch (_err) {}
});
};
/**
* @param {Plyr} player
* @param {ReconnectingWebSocket} socket
*/
const setupOutgoingEvents = (player, socket) => {
const currentVideoTime = () => (player.currentTime * 1000) | 0;
player.on("pause", async () => {
if (outgoingDebounce || player.elements.inputs.seek.disabled) {
return;
}
// don't send a pause event for the video ending
if (player.currentTime == player.duration) {
return;
}
socket.send(
JSON.stringify({
op: "SetPlaying",
data: {
playing: false,
time: currentVideoTime(),
},
})
);
});
player.on("play", () => {
if (outgoingDebounce || player.elements.inputs.seek.disabled) {
return;
}
socket.send(
JSON.stringify({
op: "SetPlaying",
data: {
playing: true,
time: currentVideoTime(),
},
})
);
});
let firstSeekComplete = false;
player.on("seeked", async (event) => {
if (!firstSeekComplete) {
// The first seeked event is performed by the browser when the video is loading
firstSeekComplete = true;
return;
}
if (outgoingDebounce || player.elements.inputs.seek.disabled) {
return;
}
socket.send(
JSON.stringify({
op: "SetTime",
data: {
to: currentVideoTime(),
},
})
);
});
};
export const joinSession = async (created) => {
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 () => {
player = await setupVideo(
video_url,
subtitle_tracks,
current_time_ms,
is_playing,
created
);
player.on("canplay", () => {
sync();
});
setupOutgoingEvents(player, socket);
setupIncomingEvents(player, 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}`;
};
export const sync = async () => {
setDebounce();
await setPlaying(false);
const { current_time_ms, is_playing } = await fetch(
`/sess/${state().sessionId}`
).then((r) => r.json());
setDebounce();
setVideoTime(current_time_ms);
if (is_playing) await setPlaying(is_playing);
};