From d48771e92189990ee14b8d5e5501e94f2f6403ff Mon Sep 17 00:00:00 2001 From: videogame hacker Date: Mon, 25 Oct 2021 02:59:52 +0100 Subject: [PATCH] Prepare for adding the chat box --- frontend/index.html | 11 +++++++ frontend/main.js | 64 ++++++++++++++++++++++++++++------------ frontend/styles.css | 18 +++++++++++ src/events.rs | 13 +++++++- src/main.rs | 18 +++++++---- src/viewer_connection.rs | 25 ++++++++++++++-- src/watch_session.rs | 2 ++ 7 files changed, 123 insertions(+), 28 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 11ba102..c075c69 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -16,6 +16,14 @@

Join a session

+ + + +
+
+ diff --git a/frontend/main.js b/frontend/main.js index 91f2964..5063b1a 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -3,8 +3,6 @@ * @param {{name: string, url: string}[]} subtitles */ const createVideoElement = (videoUrl, subtitles) => { - document.querySelector("#pre-join-controls").style["display"] = "none"; - const video = document.createElement("video"); video.controls = true; video.autoplay = false; @@ -35,6 +33,19 @@ const createVideoElement = (videoUrl, subtitles) => { let outgoingDebounce = false; let outgoingDebounceCallbackId = null; +const setDebounce = () => { + outgoingDebounce = true; + + if (outgoingDebounceCallbackId) { + cancelIdleCallback(outgoingDebounceCallbackId); + outgoingDebounceCallbackId = null; + } + + outgoingDebounceCallbackId = setTimeout(() => { + outgoingDebounce = false; + }, 500); +} + /** * @param {WebSocket} socket * @param {HTMLVideoElement} video @@ -53,10 +64,10 @@ const setupSocketEvents = (socket, video) => { const event = JSON.parse(messageEvent.data); console.log(event); - outgoingDebounce = true; - switch (event.op) { case "SetPlaying": + setDebounce(); + if (event.data.playing) { await video.play(); } else { @@ -67,21 +78,15 @@ const setupSocketEvents = (socket, video) => { break; case "SetTime": + setDebounce(); setVideoTime(event.data); break; + + // TODO: UserJoin, UserLeave, ChatMessage } } 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 */ const setupVideo = async (sessionId, videoUrl, subtitles, currentTime, playing, socket) => { + document.querySelector("#pre-join-controls").style["display"] = "none"; const video = createVideoElement(videoUrl, subtitles); - document.body.appendChild(video); + document.querySelector("#video-container").appendChild(video); video.currentTime = (currentTime / 1000.0); @@ -167,8 +173,20 @@ const setupVideo = async (sessionId, videoUrl, subtitles, currentTime, playing, 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 { window.location.hash = sessionId; @@ -177,11 +195,14 @@ const joinSession = async (sessionId) => { current_time_ms, is_playing } = 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]; const socket = new WebSocket(wsUrl.toString()); - setupVideo(sessionId, video_url, subtitle_tracks, current_time_ms, is_playing, socket); + socket.addEventListener("open", () => { + setupVideo(sessionId, video_url, subtitle_tracks, current_time_ms, is_playing, socket); + setupChat(sessionId, socket); + }); } catch (err) { // TODO: Show an error on the screen console.error(err); @@ -189,11 +210,16 @@ const joinSession = async (sessionId) => { } const main = () => { + document.querySelector("#join-session-nickname").value = localStorage.getItem("watch-party-nickname"); + document.querySelector("#join-session-form").addEventListener("submit", event => { event.preventDefault(); + const nickname = document.querySelector("#join-session-nickname").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\-]+/)) { diff --git a/frontend/styles.css b/frontend/styles.css index 9a50d74..a35d7dc 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -9,6 +9,15 @@ html { color: var(--fg); font-size: 1.125rem; font-family: sans-serif; + + overflow-y: scroll; + scrollbar-width: none; + -ms-overflow-style: none; +} + +::-webkit-scrollbar { + width: 0; + background: transparent; } html, @@ -17,8 +26,13 @@ body { } video { + display: block; + width: 100vw; height: auto; + + max-width: auto; + max-height: 100vh; } a { @@ -99,3 +113,7 @@ button.small-button { #join-session-form { margin-bottom: 4em; } + +#chatbox-container { + display: none; +} diff --git a/src/events.rs b/src/events.rs index 13eabe4..77584fa 100644 --- a/src/events.rs +++ b/src/events.rs @@ -3,6 +3,17 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize)] #[serde(tag = "op", content = "data")] pub enum WatchEvent { - SetPlaying { playing: bool, time: u64 }, + SetPlaying { + playing: bool, + time: u64, + }, SetTime(u64), + + UserJoin(String), + UserLeave(String), + ChatMessage { + #[serde(default = "String::new")] + user: String, + message: String, + }, } diff --git a/src/main.rs b/src/main.rs index b130574..786bbf9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,9 +19,14 @@ use crate::{ #[derive(Deserialize)] struct StartSessionBody { - pub video_url: String, + video_url: String, #[serde(default = "Vec::new")] - pub subtitle_tracks: Vec, + subtitle_tracks: Vec, +} + +#[derive(Deserialize)] +struct SubscribeQuery { + nickname: String, } #[tokio::main] @@ -112,12 +117,13 @@ async fn main() { ); let ws_subscribe_route = get_running_session - .and(warp::path!("subscribe")) - .and(warp::ws()) + .and(warb::path!("subscribe")) + .and(warb::query()) + .and(warb::ws()) .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 - .on_upgrade(move |ws| ws_subscribe(uuid, ws)) + .on_upgrade(move |ws| ws_subscribe(uuid, query.nickname, ws)) .into_response(), RequestedSession::Error(error_response) => error_response.into_response(), }, diff --git a/src/viewer_connection.rs b/src/viewer_connection.rs index 385eeb5..7a8b68a 100644 --- a/src/viewer_connection.rs +++ b/src/viewer_connection.rs @@ -29,7 +29,7 @@ pub struct ConnectedViewer { pub tx: UnboundedSender, } -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 (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 { - let event: WatchEvent = match message + let mut event: WatchEvent = match message .to_str() .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, }; + // 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, &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, None, WatchEvent::UserLeave(nickname.clone())).await; + CONNECTED_VIEWERS.write().await.remove(&viewer_id); } diff --git a/src/watch_session.rs b/src/watch_session.rs index 5579f6a..3ff1c17 100644 --- a/src/watch_session.rs +++ b/src/watch_session.rs @@ -84,6 +84,8 @@ pub fn handle_watch_event(uuid: Uuid, watch_session: &mut WatchSession, event: W WatchEvent::SetTime(time) => { watch_session.set_time_ms(time); } + + _ => {} }; let _ = SESSIONS.lock().unwrap().insert(uuid, watch_session.clone());