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

279 lines
7.1 KiB
JavaScript
Raw Normal View History

2022-06-10 19:57:36 -07:00
import { setupVideo } from "./video.mjs?v=bfdcf21";
2022-02-15 21:53:54 -08:00
import {
setupChat,
logEventToChat,
updateViewerList,
printChatMessage,
2022-06-10 19:57:36 -07:00
} from "./chat.mjs?v=bfdcf21";
2022-02-15 16:30:22 -08:00
import ReconnectingWebSocket from "./reconnecting-web-socket.mjs";
2022-03-30 06:02:08 -07:00
import { state } from "./state.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
*/
2022-03-30 06:02:08 -07:00
const createWebSocket = () => {
2021-11-09 05:21:14 -08:00
const wsUrl = new URL(
2022-03-30 06:02:08 -07:00
`/sess/${state().sessionId}/subscribe` +
`?nickname=${encodeURIComponent(state().nickname)}` +
`&colour=${encodeURIComponent(state().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
})
);
});
};
2022-03-30 06:02:08 -07:00
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));
2022-03-30 06:02:08 -07:00
printChatMessage("join-session", "watch-party", "#fffff", messageContent);
}
2022-03-30 06:02:08 -07:00
state().activeSession = state().sessionId;
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-03-30 06:02:08 -07:00
window.location.hash = state().sessionId;
2022-02-15 14:19:48 -08:00
let response, video_url, subtitle_tracks, current_time_ms, is_playing;
2021-11-09 05:21:14 -08:00
try {
2022-03-30 06:02:08 -07:00
response = await fetch(`/sess/${state().sessionId}`);
2022-02-15 14:19:48 -08:00
} 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
2022-03-30 06:02:08 -07:00
if (state().socket) {
state().socket.close();
state().socket = null;
}
const socket = createWebSocket();
state().socket = socket;
2022-02-15 14:19:48 -08:00
socket.addEventListener("open", async () => {
2022-03-30 06:02:08 -07:00
const video = await setupVideo(
2022-02-15 14:19:48 -08:00
video_url,
subtitle_tracks,
current_time_ms,
is_playing
);
2022-04-29 15:28:11 -07:00
// TODO: Allow the user to set this somewhere
let defaultAllowControls = false;
try {
defaultAllowControls = localStorage.getItem(
"watch-party-default-allow-controls"
);
} catch (_err) {}
2022-02-15 14:19:48 -08:00
// 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.
2022-04-29 15:28:11 -07:00
if (current_time_ms != 0 || !defaultAllowControls) {
2022-02-15 14:19:48 -08:00
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
/**
2022-06-10 19:57:36 -07:00
* @param {string} sessionId
2021-11-11 09:26:30 -08:00
* @param {string} videoUrl
* @param {Array} subtitleTracks
*/
2022-06-10 19:57:36 -07:00
export const createSession = async (sessionId, videoUrl, subtitleTracks) => {
2021-11-11 09:26:30 -08:00
const { id } = await fetch("/start_session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
2022-06-10 19:57:36 -07:00
session_id: sessionId,
2021-11-11 09:26:30 -08:00
video_url: videoUrl,
subtitle_tracks: subtitleTracks,
}),
}).then((r) => r.json());
window.location = `/?created=true#${id}`;
};