Prepare for adding the chat box

experimental
Charlotte Som 2021-10-25 02:59:52 +01:00
parent 7796d8e5f0
commit d48771e921
7 changed files with 123 additions and 28 deletions

View File

@ -16,6 +16,14 @@
<form id="join-session-form"> <form id="join-session-form">
<h2>Join a session</h2> <h2>Join a session</h2>
<label for="nickname">Nickname:</label>
<input
type="text"
id="join-session-nickname"
placeholder="Nickname"
required
/>
<label for="session-id">Session ID:</label> <label for="session-id">Session ID:</label>
<input <input
type="text" type="text"
@ -31,6 +39,9 @@
</p> </p>
</div> </div>
<div id="video-container"></div>
<div id="chatbox-container"></div>
<script src="/main.js"></script> <script src="/main.js"></script>
</body> </body>
</html> </html>

View File

@ -3,8 +3,6 @@
* @param {{name: string, url: string}[]} subtitles * @param {{name: string, url: string}[]} subtitles
*/ */
const createVideoElement = (videoUrl, subtitles) => { const createVideoElement = (videoUrl, subtitles) => {
document.querySelector("#pre-join-controls").style["display"] = "none";
const video = document.createElement("video"); const video = document.createElement("video");
video.controls = true; video.controls = true;
video.autoplay = false; video.autoplay = false;
@ -35,6 +33,19 @@ const createVideoElement = (videoUrl, subtitles) => {
let outgoingDebounce = false; let outgoingDebounce = false;
let outgoingDebounceCallbackId = null; let outgoingDebounceCallbackId = null;
const setDebounce = () => {
outgoingDebounce = true;
if (outgoingDebounceCallbackId) {
cancelIdleCallback(outgoingDebounceCallbackId);
outgoingDebounceCallbackId = null;
}
outgoingDebounceCallbackId = setTimeout(() => {
outgoingDebounce = false;
}, 500);
}
/** /**
* @param {WebSocket} socket * @param {WebSocket} socket
* @param {HTMLVideoElement} video * @param {HTMLVideoElement} video
@ -53,10 +64,10 @@ const setupSocketEvents = (socket, video) => {
const event = JSON.parse(messageEvent.data); const event = JSON.parse(messageEvent.data);
console.log(event); console.log(event);
outgoingDebounce = true;
switch (event.op) { switch (event.op) {
case "SetPlaying": case "SetPlaying":
setDebounce();
if (event.data.playing) { if (event.data.playing) {
await video.play(); await video.play();
} else { } else {
@ -67,21 +78,15 @@ const setupSocketEvents = (socket, video) => {
break; break;
case "SetTime": case "SetTime":
setDebounce();
setVideoTime(event.data); setVideoTime(event.data);
break; break;
// TODO: UserJoin, UserLeave, ChatMessage
} }
} catch (_err) { } catch (_err) {
} }
if (outgoingDebounceCallbackId) {
cancelIdleCallback(outgoingDebounceCallbackId);
outgoingDebounceCallbackId = null;
}
outgoingDebounceCallbackId = setTimeout(() => {
outgoingDebounce = false;
}, 500);
}); });
} }
@ -148,8 +153,9 @@ const setupVideoEvents = (sessionId, video, socket) => {
* @param {WebSocket} socket * @param {WebSocket} socket
*/ */
const setupVideo = async (sessionId, videoUrl, subtitles, currentTime, playing, socket) => { const setupVideo = async (sessionId, videoUrl, subtitles, currentTime, playing, socket) => {
document.querySelector("#pre-join-controls").style["display"] = "none";
const video = createVideoElement(videoUrl, subtitles); const video = createVideoElement(videoUrl, subtitles);
document.body.appendChild(video); document.querySelector("#video-container").appendChild(video);
video.currentTime = (currentTime / 1000.0); video.currentTime = (currentTime / 1000.0);
@ -167,8 +173,20 @@ const setupVideo = async (sessionId, videoUrl, subtitles, currentTime, playing,
setupVideoEvents(sessionId, video, socket); setupVideoEvents(sessionId, video, socket);
} }
/** @param {string} sessionId */ /**
const joinSession = async (sessionId) => { * @param {string} sessionId
* @param {WebSocket} socket
*/
const setupChat = async (sessionId, socket) => {
document.querySelector("#chatbox-container").style["display"] = "initial";
// TODO
}
/**
* @param {string} nickname
* @param {string} sessionId
*/
const joinSession = async (nickname, sessionId) => {
try { try {
window.location.hash = sessionId; window.location.hash = sessionId;
@ -177,11 +195,14 @@ const joinSession = async (sessionId) => {
current_time_ms, is_playing current_time_ms, is_playing
} = await fetch(`/sess/${sessionId}`).then(r => r.json()); } = await fetch(`/sess/${sessionId}`).then(r => r.json());
const wsUrl = new URL(`/sess/${sessionId}/subscribe`, window.location.href); const wsUrl = new URL(`/sess/${sessionId}/subscribe?nickname=${encodeURIComponent(nickname)}`, window.location.href);
wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol];
const socket = new WebSocket(wsUrl.toString()); const socket = new WebSocket(wsUrl.toString());
socket.addEventListener("open", () => {
setupVideo(sessionId, video_url, subtitle_tracks, current_time_ms, is_playing, socket); setupVideo(sessionId, video_url, subtitle_tracks, current_time_ms, is_playing, socket);
setupChat(sessionId, socket);
});
} catch (err) { } catch (err) {
// TODO: Show an error on the screen // TODO: Show an error on the screen
console.error(err); console.error(err);
@ -189,11 +210,16 @@ const joinSession = async (sessionId) => {
} }
const main = () => { const main = () => {
document.querySelector("#join-session-nickname").value = localStorage.getItem("watch-party-nickname");
document.querySelector("#join-session-form").addEventListener("submit", event => { document.querySelector("#join-session-form").addEventListener("submit", event => {
event.preventDefault(); event.preventDefault();
const nickname = document.querySelector("#join-session-nickname").value;
const sessionId = document.querySelector("#join-session-id").value; const sessionId = document.querySelector("#join-session-id").value;
joinSession(sessionId);
localStorage.setItem("watch-party-nickname", nickname);
joinSession(nickname, sessionId);
}); });
if (window.location.hash.match(/#[0-9a-f\-]+/)) { if (window.location.hash.match(/#[0-9a-f\-]+/)) {

View File

@ -9,6 +9,15 @@ html {
color: var(--fg); color: var(--fg);
font-size: 1.125rem; font-size: 1.125rem;
font-family: sans-serif; font-family: sans-serif;
overflow-y: scroll;
scrollbar-width: none;
-ms-overflow-style: none;
}
::-webkit-scrollbar {
width: 0;
background: transparent;
} }
html, html,
@ -17,8 +26,13 @@ body {
} }
video { video {
display: block;
width: 100vw; width: 100vw;
height: auto; height: auto;
max-width: auto;
max-height: 100vh;
} }
a { a {
@ -99,3 +113,7 @@ button.small-button {
#join-session-form { #join-session-form {
margin-bottom: 4em; margin-bottom: 4em;
} }
#chatbox-container {
display: none;
}

View File

@ -3,6 +3,17 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "op", content = "data")] #[serde(tag = "op", content = "data")]
pub enum WatchEvent { pub enum WatchEvent {
SetPlaying { playing: bool, time: u64 }, SetPlaying {
playing: bool,
time: u64,
},
SetTime(u64), SetTime(u64),
UserJoin(String),
UserLeave(String),
ChatMessage {
#[serde(default = "String::new")]
user: String,
message: String,
},
} }

View File

@ -19,9 +19,14 @@ use crate::{
#[derive(Deserialize)] #[derive(Deserialize)]
struct StartSessionBody { struct StartSessionBody {
pub video_url: String, video_url: String,
#[serde(default = "Vec::new")] #[serde(default = "Vec::new")]
pub subtitle_tracks: Vec<SubtitleTrack>, subtitle_tracks: Vec<SubtitleTrack>,
}
#[derive(Deserialize)]
struct SubscribeQuery {
nickname: String,
} }
#[tokio::main] #[tokio::main]
@ -112,12 +117,13 @@ async fn main() {
); );
let ws_subscribe_route = get_running_session let ws_subscribe_route = get_running_session
.and(warp::path!("subscribe")) .and(warb::path!("subscribe"))
.and(warp::ws()) .and(warb::query())
.and(warb::ws())
.map( .map(
|requested_session, ws: warb::ws::Ws| match requested_session { |requested_session, query: SubscribeQuery, ws: warb::ws::Ws| match requested_session {
RequestedSession::Session(uuid, _) => ws RequestedSession::Session(uuid, _) => ws
.on_upgrade(move |ws| ws_subscribe(uuid, ws)) .on_upgrade(move |ws| ws_subscribe(uuid, query.nickname, ws))
.into_response(), .into_response(),
RequestedSession::Error(error_response) => error_response.into_response(), RequestedSession::Error(error_response) => error_response.into_response(),
}, },

View File

@ -29,7 +29,7 @@ pub struct ConnectedViewer {
pub tx: UnboundedSender<WatchEvent>, pub tx: UnboundedSender<WatchEvent>,
} }
pub async fn ws_subscribe(session_uuid: Uuid, ws: WebSocket) { pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) {
let viewer_id = NEXT_VIEWER_ID.fetch_add(1, Ordering::Relaxed); let viewer_id = NEXT_VIEWER_ID.fetch_add(1, Ordering::Relaxed);
let (mut viewer_ws_tx, mut viewer_ws_rx) = ws.split(); let (mut viewer_ws_tx, mut viewer_ws_rx) = ws.split();
@ -56,8 +56,10 @@ pub async fn ws_subscribe(session_uuid: Uuid, ws: WebSocket) {
}, },
); );
ws_publish(session_uuid, None, WatchEvent::UserJoin(nickname.clone())).await;
while let Some(Ok(message)) = viewer_ws_rx.next().await { while let Some(Ok(message)) = viewer_ws_rx.next().await {
let event: WatchEvent = 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())
@ -66,6 +68,23 @@ pub async fn ws_subscribe(session_uuid: Uuid, ws: WebSocket) {
None => continue, None => continue,
}; };
// 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( handle_watch_event(
session_uuid, session_uuid,
&mut get_session(session_uuid).unwrap(), &mut get_session(session_uuid).unwrap(),
@ -75,6 +94,8 @@ pub async fn ws_subscribe(session_uuid: Uuid, ws: WebSocket) {
ws_publish(session_uuid, Some(viewer_id), event).await; ws_publish(session_uuid, Some(viewer_id), event).await;
} }
ws_publish(session_uuid, None, WatchEvent::UserLeave(nickname.clone())).await;
CONNECTED_VIEWERS.write().await.remove(&viewer_id); CONNECTED_VIEWERS.write().await.remove(&viewer_id);
} }

View File

@ -84,6 +84,8 @@ pub fn handle_watch_event(uuid: Uuid, watch_session: &mut WatchSession, event: W
WatchEvent::SetTime(time) => { WatchEvent::SetTime(time) => {
watch_session.set_time_ms(time); watch_session.set_time_ms(time);
} }
_ => {}
}; };
let _ = SESSIONS.lock().unwrap().insert(uuid, watch_session.clone()); let _ = SESSIONS.lock().unwrap().insert(uuid, watch_session.clone());