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

253 lines
6.1 KiB
JavaScript
Raw Normal View History

2022-02-16 22:25:54 -08:00
import { setupVideo } from "./video.mjs?v=19ef791";
2022-02-15 21:53:54 -08:00
import {
setupChat,
logEventToChat,
updateViewerList,
2022-02-16 22:25:54 -08:00
} from "./chat.mjs?v=19ef791";
2022-02-15 16:30:22 -08:00
import ReconnectingWebSocket from "./reconnecting-web-socket.mjs";
2021-11-09 05:21:14 -08:00
/**
* @param {string} sessionId
* @param {string} nickname
2022-02-15 14:19:48 -08:00
* @returns {ReconnectingWebSocket}
2021-11-09 05:21:14 -08:00
*/
const createWebSocket = (sessionId, nickname, colour) => {
2021-11-09 05:21:14 -08:00
const wsUrl = new URL(
`/sess/${sessionId}/subscribe` +
`?nickname=${encodeURIComponent(nickname)}` +
`&colour=${encodeURIComponent(colour)}`,
2021-11-09 05:21:14 -08:00
window.location.href
);
2022-02-15 14:19:48 -08:00
wsUrl.protocol = "ws" + window.location.protocol.slice(4);
const socket = new ReconnectingWebSocket(wsUrl);
2021-11-09 05:21:14 -08:00
return socket;
};
let outgoingDebounce = false;
let outgoingDebounceCallbackId = null;
export const setDebounce = () => {
2021-11-09 05:21:14 -08:00
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();
}
};
2021-11-09 05:21:14 -08:00
/**
* @param {HTMLVideoElement} video
2022-02-15 14:19:48 -08:00
* @param {ReconnectingWebSocket} socket
2021-11-09 05:21:14 -08:00
*/
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();
2021-11-09 05:21:14 -08:00
if (event.data.playing) {
await video.play();
} else {
video.pause();
}
2021-11-09 05:21:14 -08:00
setVideoTime(event.data.time, video);
break;
case "SetTime":
setDebounce();
setVideoTime(event.data, video);
break;
2022-02-13 09:23:20 -08:00
case "UpdateViewerList":
updateViewerList(event.data);
break;
}
2021-11-09 05:21:14 -08:00
}
logEventToChat(event);
} catch (_err) {}
2021-11-09 05:21:14 -08:00
});
};
/**
* @param {HTMLVideoElement} video
2022-02-15 14:19:48 -08:00
* @param {ReconnectingWebSocket} socket
2021-11-09 05:21:14 -08:00
*/
const setupOutgoingEvents = (video, socket) => {
const currentVideoTime = () => (video.currentTime * 1000) | 0;
video.addEventListener("pause", async (event) => {
if (outgoingDebounce || !video.controls) {
2021-11-09 05:21:14 -08:00
return;
}
// don't send a pause event for the video ending
if (video.currentTime == video.duration) {
return;
}
2021-11-09 05:21:14 -08:00
socket.send(
JSON.stringify({
op: "SetPlaying",
data: {
playing: false,
time: currentVideoTime(),
},
})
);
});
video.addEventListener("play", (event) => {
if (outgoingDebounce || !video.controls) {
2021-11-09 05:21:14 -08:00
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) {
2021-11-09 05:21:14 -08:00
return;
}
socket.send(
JSON.stringify({
op: "SetTime",
2022-02-13 08:58:31 -08:00
data: {
to: currentVideoTime(),
},
2021-11-09 05:21:14 -08:00
})
);
});
};
/**
* @param {string} nickname
* @param {string} sessionId
*/
export const joinSession = async (nickname, sessionId, colour) => {
2022-02-15 14:19:48 -08:00
// try { // we are handling errors in the join form.
2022-02-15 16:30:22 -08:00
const genericConnectionError = new Error(
"There was an issue getting the session information."
);
2022-02-15 14:19:48 -08:00
window.location.hash = sessionId;
let response, video_url, subtitle_tracks, current_time_ms, is_playing;
2021-11-09 05:21:14 -08:00
try {
2022-02-15 14:19:48 -08:00
response = await fetch(`/sess/${sessionId}`);
} catch (e) {
console.error(e);
throw genericConnectionError;
}
2022-02-15 16:30:22 -08:00
if (!response.ok) {
2022-02-15 14:19:48 -08:00
let error;
try {
({ error } = await response.json());
2022-02-15 16:30:22 -08:00
if (!error) throw new Error();
2022-02-15 14:19:48 -08:00
} catch (e) {
console.error(e);
throw genericConnectionError;
}
2022-02-15 16:30:22 -08:00
throw new Error(error);
2022-02-15 14:19:48 -08:00
}
try {
2022-02-15 16:30:22 -08:00
({ video_url, subtitle_tracks, current_time_ms, is_playing } =
await response.json());
2022-02-15 14:19:48 -08:00
} catch (e) {
console.error(e);
throw genericConnectionError;
2021-11-09 05:21:14 -08:00
}
2022-02-15 14:19:48 -08:00
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);
});
2022-02-15 16:30:22 -08:00
socket.addEventListener("reconnecting", (e) => {
2022-02-15 14:19:48 -08:00
console.log("Reconnecting...");
});
2022-02-15 16:30:22 -08:00
socket.addEventListener("reconnected", (e) => {
2022-02-15 14:19:48 -08:00
console.log("Reconnected.");
});
//} catch (e) {
// alert(e.message)
//}
2021-11-09 05:21:14 -08:00
};
2021-11-11 09:26:30 -08:00
/**
* @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}`;
};