forked from lavender/watch-party
Compare commits
No commits in common. "903fd535ce6d53ced972d441104f77e548283503" and "8da286fad9579659ee058b085f2e7418e4e7d94f" have entirely different histories.
903fd535ce
...
8da286fad9
13 changed files with 123 additions and 396 deletions
|
|
@ -1,52 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>watch party :D</title>
|
|
||||||
<link rel="stylesheet" href="/styles.css?v=3" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
This site will <em>not</em> work without JavaScript, and there's not
|
|
||||||
really any way around that :(
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id="create-controls">
|
|
||||||
<form id="create-session-form">
|
|
||||||
<h2>Create a session</h2>
|
|
||||||
|
|
||||||
<label for="create-session-video">Video:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="create-session-video"
|
|
||||||
placeholder="https://video.example.com/example.mp4"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- TODO: Ability to add multiple subtitles for different languages -->
|
|
||||||
<label for="create-session-subs">Subtitles:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="create-session-subs"
|
|
||||||
placeholder="https://video.example.com/example.vtt"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for="create-session-subs-name">Subtitle track name:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="create-session-subs-name"
|
|
||||||
placeholder="English"
|
|
||||||
/>
|
|
||||||
<button>Create</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Already have a session?
|
|
||||||
<a href="/">Join your session</a> instead.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="/create.mjs?v=1"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { setupCreateSessionForm } from "./lib/create-session.mjs";
|
|
||||||
|
|
||||||
const main = () => {
|
|
||||||
setupCreateSessionForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (document.readyState === "complete") {
|
|
||||||
main();
|
|
||||||
} else {
|
|
||||||
document.addEventListener("DOMContentLoaded", main);
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>watch party :D</title>
|
<title>watch party :D</title>
|
||||||
<link rel="stylesheet" href="/styles.css?v=3" />
|
<link rel="stylesheet" href="/styles.css?v=2" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -16,12 +16,7 @@
|
||||||
<form id="join-session-form">
|
<form id="join-session-form">
|
||||||
<h2>Join a session</h2>
|
<h2>Join a session</h2>
|
||||||
|
|
||||||
<p id="post-create-message">
|
<label for="nickname">Nickname:</label>
|
||||||
Your session has been created successfully. Copy the current url or
|
|
||||||
the Session ID below and share it with your friends. :)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label for="join-session-nickname">Nickname:</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="join-session-nickname"
|
id="join-session-nickname"
|
||||||
|
|
@ -29,7 +24,7 @@
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="join-session-id">Session ID:</label>
|
<label for="session-id">Session ID:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="join-session-id"
|
id="join-session-id"
|
||||||
|
|
@ -52,6 +47,6 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/main.mjs?v=2"></script>
|
<script type="module" src="/main.mjs?v=1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ const setupChatboxEvents = (socket) => {
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
op: "ChatMessage",
|
op: "ChatMessage",
|
||||||
data: content,
|
data: {
|
||||||
|
message: content,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -49,134 +51,51 @@ export const setupChat = async (socket) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToChat = (node) => {
|
const printToChat = (elem) => {
|
||||||
const chatbox = document.querySelector("#chatbox");
|
const chatbox = document.querySelector("#chatbox");
|
||||||
chatbox.appendChild(node);
|
chatbox.appendChild(elem);
|
||||||
chatbox.scrollTop = chatbox.scrollHeight;
|
chatbox.scrollTop = chatbox.scrollHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastTimeMs = null;
|
export const handleChatEvent = (event) => {
|
||||||
let lastPlaying = false;
|
|
||||||
|
|
||||||
const checkDebounce = (event) => {
|
|
||||||
let timeMs = null;
|
|
||||||
let playing = null;
|
|
||||||
if (event.op == "SetTime") {
|
|
||||||
timeMs = event.data;
|
|
||||||
} else if (event.op == "SetPlaying") {
|
|
||||||
timeMs = event.data.time;
|
|
||||||
playing = event.data.playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
let shouldIgnore = false;
|
|
||||||
|
|
||||||
if (timeMs != null) {
|
|
||||||
if (lastTimeMs && Math.abs(lastTimeMs - timeMs) < 500) {
|
|
||||||
shouldIgnore = true;
|
|
||||||
}
|
|
||||||
lastTimeMs = timeMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playing != null) {
|
|
||||||
if (lastPlaying != playing) {
|
|
||||||
shouldIgnore = false;
|
|
||||||
}
|
|
||||||
lastPlaying = playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return shouldIgnore;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} eventType
|
|
||||||
* @param {string?} user
|
|
||||||
* @param {Node?} content
|
|
||||||
*/
|
|
||||||
const printChatMessage = (eventType, user, content) => {
|
|
||||||
const chatMessage = document.createElement("div");
|
|
||||||
chatMessage.classList.add("chat-message");
|
|
||||||
chatMessage.classList.add(eventType);
|
|
||||||
|
|
||||||
if (user != null) {
|
|
||||||
const userName = document.createElement("strong");
|
|
||||||
userName.textContent = user;
|
|
||||||
chatMessage.appendChild(userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
chatMessage.appendChild(document.createTextNode(" "));
|
|
||||||
|
|
||||||
if (content != null) {
|
|
||||||
chatMessage.appendChild(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
addToChat(chatMessage);
|
|
||||||
|
|
||||||
return chatMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (ms) => {
|
|
||||||
const seconds = Math.floor((ms / 1000) % 60);
|
|
||||||
const minutes = Math.floor((ms / (60 * 1000)) % 60);
|
|
||||||
const hours = Math.floor((ms / (3600 * 1000)) % 3600);
|
|
||||||
return `${hours < 10 ? "0" + hours : hours}:${
|
|
||||||
minutes < 10 ? "0" + minutes : minutes
|
|
||||||
}:${seconds < 10 ? "0" + seconds : seconds}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const logEventToChat = (event) => {
|
|
||||||
if (checkDebounce(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.op) {
|
switch (event.op) {
|
||||||
case "UserJoin": {
|
case "UserJoin": {
|
||||||
printChatMessage(
|
// print something to the chat
|
||||||
"user-join",
|
const chatMessage = document.createElement("div");
|
||||||
event.user,
|
chatMessage.classList.add("chat-message");
|
||||||
document.createTextNode("joined")
|
chatMessage.classList.add("user-join");
|
||||||
);
|
const userName = document.createElement("strong");
|
||||||
|
userName.textContent = event.data;
|
||||||
|
chatMessage.appendChild(userName);
|
||||||
|
chatMessage.appendChild(document.createTextNode(" joined"));
|
||||||
|
printToChat(chatMessage);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "UserLeave": {
|
case "UserLeave": {
|
||||||
printChatMessage(
|
const chatMessage = document.createElement("div");
|
||||||
"user-leave",
|
chatMessage.classList.add("chat-message");
|
||||||
event.user,
|
chatMessage.classList.add("user-leave");
|
||||||
document.createTextNode("left")
|
const userName = document.createElement("strong");
|
||||||
);
|
userName.textContent = event.data;
|
||||||
|
chatMessage.appendChild(userName);
|
||||||
|
chatMessage.appendChild(document.createTextNode(" left"));
|
||||||
|
printToChat(chatMessage);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "ChatMessage": {
|
case "ChatMessage": {
|
||||||
|
const chatMessage = document.createElement("div");
|
||||||
|
chatMessage.classList.add("chat-message");
|
||||||
|
const userName = document.createElement("strong");
|
||||||
|
userName.innerText = event.data.user;
|
||||||
|
chatMessage.appendChild(userName);
|
||||||
|
chatMessage.appendChild(document.createTextNode(" "));
|
||||||
const messageContent = document.createElement("span");
|
const messageContent = document.createElement("span");
|
||||||
messageContent.classList.add("message-content");
|
messageContent.classList.add("message-content");
|
||||||
messageContent.textContent = event.data;
|
messageContent.textContent = event.data.message;
|
||||||
printChatMessage("chat-message", event.user, messageContent);
|
chatMessage.appendChild(messageContent);
|
||||||
break;
|
printToChat(chatMessage);
|
||||||
}
|
|
||||||
case "SetTime": {
|
|
||||||
const messageContent = document.createElement("span");
|
|
||||||
messageContent.appendChild(document.createTextNode("set the time to "));
|
|
||||||
|
|
||||||
messageContent.appendChild(
|
|
||||||
document.createTextNode(formatTime(event.data))
|
|
||||||
);
|
|
||||||
|
|
||||||
printChatMessage("set-time", event.user, messageContent);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "SetPlaying": {
|
|
||||||
const messageContent = document.createElement("span");
|
|
||||||
messageContent.appendChild(
|
|
||||||
document.createTextNode(
|
|
||||||
event.data.playing ? "started playing" : "paused"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
messageContent.appendChild(document.createTextNode(" at "));
|
|
||||||
messageContent.appendChild(
|
|
||||||
document.createTextNode(formatTime(event.data.time))
|
|
||||||
);
|
|
||||||
|
|
||||||
printChatMessage("set-playing", event.user, messageContent);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { createSession } from "./watch-session.mjs?v=3";
|
|
||||||
|
|
||||||
export const setupCreateSessionForm = () => {
|
|
||||||
const form = document.querySelector("#create-session-form");
|
|
||||||
const videoUrl = form.querySelector("#create-session-video");
|
|
||||||
const subsUrl = form.querySelector("#create-session-subs");
|
|
||||||
const subsName = form.querySelector("#create-session-subs-name");
|
|
||||||
|
|
||||||
form.addEventListener("submit", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let subs = [];
|
|
||||||
if (subsUrl.value) {
|
|
||||||
subs.push({ url: subsUrl.value, name: subsName.value || "default" });
|
|
||||||
}
|
|
||||||
createSession(videoUrl.value, subs);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { joinSession } from "./watch-session.mjs?v=3";
|
import { joinSession } from "./watch-session.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLInputElement} field
|
* @param {HTMLInputElement} field
|
||||||
|
|
@ -23,17 +23,7 @@ const saveNickname = (field) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayPostCreateMessage = () => {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
if (params.get("created") == "true") {
|
|
||||||
document.querySelector("#post-create-message").style["display"] = "block";
|
|
||||||
window.history.replaceState({}, document.title, `/${window.location.hash}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupJoinSessionForm = () => {
|
export const setupJoinSessionForm = () => {
|
||||||
displayPostCreateMessage();
|
|
||||||
|
|
||||||
const form = document.querySelector("#join-session-form");
|
const form = document.querySelector("#join-session-form");
|
||||||
const nickname = form.querySelector("#join-session-nickname");
|
const nickname = form.querySelector("#join-session-nickname");
|
||||||
const sessionId = form.querySelector("#join-session-id");
|
const sessionId = form.querySelector("#join-session-id");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { setupVideo } from "./video.mjs?v=2";
|
import { setupVideo } from "./video.mjs";
|
||||||
import { setupChat, logEventToChat } from "./chat.mjs?v=2";
|
import { setupChat, handleChatEvent } from "./chat.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} sessionId
|
* @param {string} sessionId
|
||||||
|
|
@ -50,29 +50,31 @@ const setupIncomingEvents = (video, socket) => {
|
||||||
socket.addEventListener("message", async (messageEvent) => {
|
socket.addEventListener("message", async (messageEvent) => {
|
||||||
try {
|
try {
|
||||||
const event = JSON.parse(messageEvent.data);
|
const event = JSON.parse(messageEvent.data);
|
||||||
|
// console.log(event);
|
||||||
|
|
||||||
if (!event.reflected) {
|
switch (event.op) {
|
||||||
switch (event.op) {
|
case "SetPlaying":
|
||||||
case "SetPlaying":
|
setDebounce();
|
||||||
setDebounce();
|
|
||||||
|
|
||||||
if (event.data.playing) {
|
if (event.data.playing) {
|
||||||
await video.play();
|
await video.play();
|
||||||
} else {
|
} else {
|
||||||
video.pause();
|
video.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
setVideoTime(event.data.time);
|
setVideoTime(event.data.time);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "SetTime":
|
case "SetTime":
|
||||||
setDebounce();
|
setDebounce();
|
||||||
setVideoTime(event.data);
|
setVideoTime(event.data);
|
||||||
break;
|
break;
|
||||||
}
|
case "UserJoin":
|
||||||
|
case "UserLeave":
|
||||||
|
case "ChatMessage":
|
||||||
|
handleChatEvent(event);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
logEventToChat(event);
|
|
||||||
} catch (_err) {}
|
} catch (_err) {}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -167,20 +169,3 @@ export const joinSession = async (nickname, sessionId) => {
|
||||||
console.error(err);
|
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}`;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { setupJoinSessionForm } from "./lib/join-session.mjs?v=2";
|
import { setupJoinSessionForm } from "./lib/join-session.mjs";
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
setupJoinSessionForm();
|
setupJoinSessionForm();
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ input[type="text"] {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
resize: none;
|
resize: none;
|
||||||
overflow-x: wrap;
|
overflow-x: wrap;
|
||||||
|
|
@ -83,7 +82,6 @@ button {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border: 1px solid rgba(0, 0, 0, 0);
|
border: 1px solid rgba(0, 0, 0, 0);
|
||||||
|
|
@ -106,50 +104,29 @@ button.small-button {
|
||||||
margin-right: 1ch !important;
|
margin-right: 1ch !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pre-join-controls,
|
#pre-join-controls {
|
||||||
#create-controls {
|
|
||||||
width: 60%;
|
width: 60%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-top: 4em;
|
margin-top: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#join-session-form,
|
#join-session-form {
|
||||||
#create-session-form {
|
|
||||||
margin-bottom: 4em;
|
margin-bottom: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#post-create-message {
|
|
||||||
display: none;
|
|
||||||
width: 500px;
|
|
||||||
max-width: 100%;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chatbox-container {
|
#chatbox-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-join,
|
||||||
|
.user-leave {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-message > strong {
|
.chat-message > strong {
|
||||||
color: rgb(126, 208, 255);
|
color: rgb(126, 208, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message.user-join,
|
|
||||||
.chat-message.user-leave {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message.set-time,
|
|
||||||
.chat-message.set-playing {
|
|
||||||
font-style: italic;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message.set-time > strong,
|
|
||||||
.chat-message.set-playing > strong {
|
|
||||||
color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chatbox {
|
#chatbox {
|
||||||
padding: 0.5em 2em;
|
padding: 0.5em 2em;
|
||||||
min-height: 8em;
|
min-height: 8em;
|
||||||
|
|
@ -167,30 +144,4 @@ button.small-button {
|
||||||
|
|
||||||
#chatbox-send > input {
|
#chatbox-send > input {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-aspect-ratio: 4/3) {
|
|
||||||
#video-container video {
|
|
||||||
width: calc(100vw - 400px);
|
|
||||||
position: absolute;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#video-container {
|
|
||||||
float: left;
|
|
||||||
height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chatbox-container {
|
|
||||||
float: right;
|
|
||||||
width: 400px;
|
|
||||||
height: 100vh !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chatbox {
|
|
||||||
height: calc(100vh - 5em) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,18 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "op", content = "data")]
|
#[serde(tag = "op", content = "data")]
|
||||||
pub enum WatchEventData {
|
pub enum WatchEvent {
|
||||||
SetPlaying { playing: bool, time: u64 },
|
SetPlaying {
|
||||||
|
playing: bool,
|
||||||
|
time: u64,
|
||||||
|
},
|
||||||
SetTime(u64),
|
SetTime(u64),
|
||||||
|
|
||||||
UserJoin,
|
UserJoin(String),
|
||||||
UserLeave,
|
UserLeave(String),
|
||||||
ChatMessage(String),
|
ChatMessage {
|
||||||
}
|
#[serde(default = "String::new")]
|
||||||
|
user: String,
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
message: String,
|
||||||
pub struct WatchEvent {
|
},
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user: Option<String>,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub data: WatchEventData,
|
|
||||||
#[serde(default)]
|
|
||||||
pub reflected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WatchEvent {
|
|
||||||
pub fn new(user: String, data: WatchEventData) -> Self {
|
|
||||||
WatchEvent {
|
|
||||||
user: Some(user),
|
|
||||||
data,
|
|
||||||
reflected: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
src/main.rs
32
src/main.rs
|
|
@ -12,9 +12,9 @@ mod watch_session;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::{WatchEvent, WatchEventData},
|
events::WatchEvent,
|
||||||
viewer_connection::{ws_publish, ws_subscribe},
|
viewer_connection::{ws_publish, ws_subscribe},
|
||||||
watch_session::{get_session, handle_watch_event_data, SubtitleTrack, WatchSession, SESSIONS},
|
watch_session::{get_session, handle_watch_event, SubtitleTrack, WatchSession, SESSIONS},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -85,21 +85,13 @@ async fn main() {
|
||||||
.and(warb::body::json())
|
.and(warb::body::json())
|
||||||
.map(|requested_session, playing: bool| match requested_session {
|
.map(|requested_session, playing: bool| match requested_session {
|
||||||
RequestedSession::Session(uuid, mut sess) => {
|
RequestedSession::Session(uuid, mut sess) => {
|
||||||
let data = WatchEventData::SetPlaying {
|
let event = WatchEvent::SetPlaying {
|
||||||
playing,
|
playing,
|
||||||
time: sess.get_time_ms(),
|
time: sess.get_time_ms(),
|
||||||
};
|
};
|
||||||
|
|
||||||
handle_watch_event_data(uuid, &mut sess, data.clone());
|
handle_watch_event(uuid, &mut sess, event.clone());
|
||||||
tokio::spawn(ws_publish(
|
tokio::spawn(ws_publish(uuid, None, event));
|
||||||
uuid,
|
|
||||||
None,
|
|
||||||
WatchEvent {
|
|
||||||
user: None,
|
|
||||||
data,
|
|
||||||
reflected: false,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK)
|
warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
@ -113,18 +105,10 @@ async fn main() {
|
||||||
.map(
|
.map(
|
||||||
|requested_session, current_time_ms: u64| match requested_session {
|
|requested_session, current_time_ms: u64| match requested_session {
|
||||||
RequestedSession::Session(uuid, mut sess) => {
|
RequestedSession::Session(uuid, mut sess) => {
|
||||||
let data = WatchEventData::SetTime(current_time_ms);
|
let event = WatchEvent::SetTime(current_time_ms);
|
||||||
|
|
||||||
handle_watch_event_data(uuid, &mut sess, data.clone());
|
handle_watch_event(uuid, &mut sess, event.clone());
|
||||||
tokio::spawn(ws_publish(
|
tokio::spawn(ws_publish(uuid, None, event));
|
||||||
uuid,
|
|
||||||
None,
|
|
||||||
WatchEvent {
|
|
||||||
user: None,
|
|
||||||
data,
|
|
||||||
reflected: false,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK)
|
warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ use uuid::Uuid;
|
||||||
use warp::ws::{Message, WebSocket};
|
use warp::ws::{Message, WebSocket};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::{WatchEvent, WatchEventData},
|
events::WatchEvent,
|
||||||
watch_session::{get_session, handle_watch_event_data},
|
watch_session::{get_session, handle_watch_event},
|
||||||
};
|
};
|
||||||
|
|
||||||
static CONNECTED_VIEWERS: Lazy<RwLock<HashMap<usize, ConnectedViewer>>> =
|
static CONNECTED_VIEWERS: Lazy<RwLock<HashMap<usize, ConnectedViewer>>> =
|
||||||
|
|
@ -27,7 +27,6 @@ pub struct ConnectedViewer {
|
||||||
pub session: Uuid,
|
pub session: Uuid,
|
||||||
pub viewer_id: usize,
|
pub viewer_id: usize,
|
||||||
pub tx: UnboundedSender<WatchEvent>,
|
pub tx: UnboundedSender<WatchEvent>,
|
||||||
pub nickname: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) {
|
pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) {
|
||||||
|
|
@ -54,19 +53,13 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) {
|
||||||
viewer_id,
|
viewer_id,
|
||||||
session: session_uuid,
|
session: session_uuid,
|
||||||
tx,
|
tx,
|
||||||
nickname: Some(nickname.clone()),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ws_publish(
|
ws_publish(session_uuid, None, WatchEvent::UserJoin(nickname.clone())).await;
|
||||||
session_uuid,
|
|
||||||
None,
|
|
||||||
WatchEvent::new(nickname.clone(), WatchEventData::UserJoin),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
while let Some(Ok(message)) = viewer_ws_rx.next().await {
|
while let Some(Ok(message)) = viewer_ws_rx.next().await {
|
||||||
let event: WatchEventData = match message
|
let mut event: WatchEvent = match message
|
||||||
.to_str()
|
.to_str()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| serde_json::from_str(s).ok())
|
.and_then(|s| serde_json::from_str(s).ok())
|
||||||
|
|
@ -75,39 +68,47 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) {
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
handle_watch_event_data(
|
// Make sure people don't spoof their nicknames to pretend to be others
|
||||||
|
// If a nickname change is required, I guess reconnect idk
|
||||||
|
if let WatchEvent::ChatMessage { user: _, message } = event {
|
||||||
|
event = WatchEvent::ChatMessage {
|
||||||
|
user: nickname.clone(),
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't pass through the viewer_id because we want the chat message
|
||||||
|
// to be reflected to the user.
|
||||||
|
ws_publish(session_uuid, None, event).await;
|
||||||
|
|
||||||
|
// We don't need to handle() chat messages,
|
||||||
|
// and we are already publishing them ourselves.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_watch_event(
|
||||||
session_uuid,
|
session_uuid,
|
||||||
&mut get_session(session_uuid).unwrap(),
|
&mut get_session(session_uuid).unwrap(),
|
||||||
event.clone(),
|
event.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
ws_publish(
|
ws_publish(session_uuid, Some(viewer_id), event).await;
|
||||||
session_uuid,
|
|
||||||
Some(viewer_id),
|
|
||||||
WatchEvent::new(nickname.clone(), event),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ws_publish(
|
ws_publish(session_uuid, None, WatchEvent::UserLeave(nickname.clone())).await;
|
||||||
session_uuid,
|
|
||||||
None,
|
|
||||||
WatchEvent::new(nickname.clone(), WatchEventData::UserLeave),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
CONNECTED_VIEWERS.write().await.remove(&viewer_id);
|
CONNECTED_VIEWERS.write().await.remove(&viewer_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ws_publish(session_uuid: Uuid, skip_viewer_id: Option<usize>, event: WatchEvent) {
|
pub async fn ws_publish(session_uuid: Uuid, viewer_id: Option<usize>, event: WatchEvent) {
|
||||||
for viewer in CONNECTED_VIEWERS.read().await.values() {
|
for viewer in CONNECTED_VIEWERS.read().await.values() {
|
||||||
|
if viewer_id == Some(viewer.viewer_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if viewer.session != session_uuid {
|
if viewer.session != session_uuid {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = viewer.tx.send(WatchEvent {
|
let _ = viewer.tx.send(event.clone());
|
||||||
reflected: skip_viewer_id == Some(viewer.viewer_id),
|
|
||||||
..event.clone()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, sync::Mutex, time::Instant};
|
use std::{collections::HashMap, sync::Mutex, time::Instant};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::events::WatchEventData;
|
use crate::events::WatchEvent;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct SubtitleTrack {
|
pub struct SubtitleTrack {
|
||||||
|
|
@ -75,17 +75,13 @@ pub fn get_session(uuid: Uuid) -> Option<WatchSession> {
|
||||||
SESSIONS.lock().unwrap().get(&uuid).cloned()
|
SESSIONS.lock().unwrap().get(&uuid).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_watch_event_data(
|
pub fn handle_watch_event(uuid: Uuid, watch_session: &mut WatchSession, event: WatchEvent) {
|
||||||
uuid: Uuid,
|
|
||||||
watch_session: &mut WatchSession,
|
|
||||||
event: WatchEventData,
|
|
||||||
) {
|
|
||||||
match event {
|
match event {
|
||||||
WatchEventData::SetPlaying { playing, time } => {
|
WatchEvent::SetPlaying { playing, time } => {
|
||||||
watch_session.set_playing(playing, time);
|
watch_session.set_playing(playing, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchEventData::SetTime(time) => {
|
WatchEvent::SetTime(time) => {
|
||||||
watch_session.set_time_ms(time);
|
watch_session.set_time_ms(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue