219 lines
5.3 KiB
JavaScript
219 lines
5.3 KiB
JavaScript
import { setupVideo } from "./video.mjs?v=9";
|
|
import { setupChat, logEventToChat, updateViewerList } from "./chat.mjs?v=9";
|
|
|
|
/**
|
|
* @param {string} sessionId
|
|
* @param {string} nickname
|
|
* @returns {WebSocket}
|
|
*/
|
|
const createWebSocket = (sessionId, nickname, colour) => {
|
|
const wsUrl = new URL(
|
|
`/sess/${sessionId}/subscribe` +
|
|
`?nickname=${encodeURIComponent(nickname)}` +
|
|
`&colour=${encodeURIComponent(colour)}`,
|
|
window.location.href
|
|
);
|
|
wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol];
|
|
const socket = new WebSocket(wsUrl.toString());
|
|
|
|
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 {WebSocket} 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 {WebSocket} 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(),
|
|
},
|
|
})
|
|
);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} nickname
|
|
* @param {string} sessionId
|
|
*/
|
|
export const joinSession = async (nickname, sessionId, colour) => {
|
|
try {
|
|
window.location.hash = sessionId;
|
|
|
|
const { video_url, subtitle_tracks, current_time_ms, is_playing } =
|
|
await fetch(`/sess/${sessionId}`).then((r) => r.json());
|
|
|
|
const socket = createWebSocket(sessionId, nickname, colour);
|
|
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);
|
|
});
|
|
// TODO: Close listener ?
|
|
} catch (err) {
|
|
// TODO: Show an error on the screen
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @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}`;
|
|
};
|