From be5a05e0fd5c437bf9f76f08904b7127c0beed12 Mon Sep 17 00:00:00 2001 From: videogame hacker Date: Wed, 10 Nov 2021 14:29:52 +0000 Subject: [PATCH] Big changes: All events are reported to chat, new layout options --- frontend/index.html | 2 +- frontend/lib/chat.mjs | 149 +++++++++++++++++++++++++-------- frontend/lib/join-session.mjs | 2 +- frontend/lib/watch-session.mjs | 42 +++++----- frontend/main.mjs | 2 +- frontend/styles.css | 48 ++++++++++- src/events.rs | 37 +++++--- src/main.rs | 32 +++++-- src/viewer_connection.rs | 59 +++++++------ src/watch_session.rs | 12 ++- 10 files changed, 268 insertions(+), 117 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e1c2e6b..2e1efb0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -47,6 +47,6 @@ - + diff --git a/frontend/lib/chat.mjs b/frontend/lib/chat.mjs index 1a6ee1d..bc06473 100644 --- a/frontend/lib/chat.mjs +++ b/frontend/lib/chat.mjs @@ -15,9 +15,7 @@ const setupChatboxEvents = (socket) => { socket.send( JSON.stringify({ op: "ChatMessage", - data: { - message: content, - }, + data: content, }) ); } @@ -51,51 +49,134 @@ export const setupChat = async (socket) => { }); }; -const printToChat = (elem) => { +const addToChat = (node) => { const chatbox = document.querySelector("#chatbox"); - chatbox.appendChild(elem); + chatbox.appendChild(node); chatbox.scrollTop = chatbox.scrollHeight; }; -export const handleChatEvent = (event) => { +let lastTimeMs = null; +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) { case "UserJoin": { - // print something to the chat - const chatMessage = document.createElement("div"); - chatMessage.classList.add("chat-message"); - chatMessage.classList.add("user-join"); - const userName = document.createElement("strong"); - userName.textContent = event.data; - chatMessage.appendChild(userName); - chatMessage.appendChild(document.createTextNode(" joined")); - printToChat(chatMessage); - + printChatMessage( + "user-join", + event.user, + document.createTextNode("joined") + ); break; } case "UserLeave": { - const chatMessage = document.createElement("div"); - chatMessage.classList.add("chat-message"); - chatMessage.classList.add("user-leave"); - const userName = document.createElement("strong"); - userName.textContent = event.data; - chatMessage.appendChild(userName); - chatMessage.appendChild(document.createTextNode(" left")); - printToChat(chatMessage); - + printChatMessage( + "user-leave", + event.user, + document.createTextNode("left") + ); break; } 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"); messageContent.classList.add("message-content"); - messageContent.textContent = event.data.message; - chatMessage.appendChild(messageContent); - printToChat(chatMessage); + messageContent.textContent = event.data; + printChatMessage("chat-message", event.user, messageContent); + break; + } + 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; } } diff --git a/frontend/lib/join-session.mjs b/frontend/lib/join-session.mjs index 9174167..11c3039 100644 --- a/frontend/lib/join-session.mjs +++ b/frontend/lib/join-session.mjs @@ -1,4 +1,4 @@ -import { joinSession } from "./watch-session.mjs"; +import { joinSession } from "./watch-session.mjs?v=2"; /** * @param {HTMLInputElement} field diff --git a/frontend/lib/watch-session.mjs b/frontend/lib/watch-session.mjs index 614b43e..da6898b 100644 --- a/frontend/lib/watch-session.mjs +++ b/frontend/lib/watch-session.mjs @@ -1,5 +1,5 @@ -import { setupVideo } from "./video.mjs"; -import { setupChat, handleChatEvent } from "./chat.mjs"; +import { setupVideo } from "./video.mjs?v=2"; +import { setupChat, logEventToChat } from "./chat.mjs?v=2"; /** * @param {string} sessionId @@ -50,31 +50,29 @@ const setupIncomingEvents = (video, socket) => { socket.addEventListener("message", async (messageEvent) => { try { const event = JSON.parse(messageEvent.data); - // console.log(event); - switch (event.op) { - case "SetPlaying": - setDebounce(); + if (!event.reflected) { + switch (event.op) { + case "SetPlaying": + setDebounce(); - if (event.data.playing) { - await video.play(); - } else { - video.pause(); - } + if (event.data.playing) { + await video.play(); + } else { + video.pause(); + } - setVideoTime(event.data.time); + setVideoTime(event.data.time); - break; - case "SetTime": - setDebounce(); - setVideoTime(event.data); - break; - case "UserJoin": - case "UserLeave": - case "ChatMessage": - handleChatEvent(event); - break; + break; + case "SetTime": + setDebounce(); + setVideoTime(event.data); + break; + } } + + logEventToChat(event); } catch (_err) {} }); }; diff --git a/frontend/main.mjs b/frontend/main.mjs index ec9d888..a0bec35 100644 --- a/frontend/main.mjs +++ b/frontend/main.mjs @@ -1,4 +1,4 @@ -import { setupJoinSessionForm } from "./lib/join-session.mjs"; +import { setupJoinSessionForm } from "./lib/join-session.mjs?v=2"; const main = () => { setupJoinSessionForm(); diff --git a/frontend/styles.css b/frontend/styles.css index 42240a8..265fa9f 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -61,6 +61,7 @@ input[type="text"] { font-family: sans-serif; font-size: 1em; width: 500px; + max-width: 100%; resize: none; overflow-x: wrap; @@ -82,6 +83,7 @@ button { font-family: sans-serif; font-size: 1em; width: 500px; + max-width: 100%; user-select: none; border: 1px solid rgba(0, 0, 0, 0); @@ -118,13 +120,25 @@ button.small-button { display: none; } -.user-join, -.user-leave { +.chat-message > strong { + color: rgb(126, 208, 255); +} + +.chat-message.user-join, +.chat-message.user-leave { font-style: italic; } -.chat-message > strong { - color: rgb(126, 208, 255); +.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 { @@ -144,4 +158,30 @@ button.small-button { #chatbox-send > input { 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; + } } diff --git a/src/events.rs b/src/events.rs index 77584fa..910b3a9 100644 --- a/src/events.rs +++ b/src/events.rs @@ -2,18 +2,31 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize)] #[serde(tag = "op", content = "data")] -pub enum WatchEvent { - SetPlaying { - playing: bool, - time: u64, - }, +pub enum WatchEventData { + SetPlaying { playing: bool, time: u64 }, SetTime(u64), - UserJoin(String), - UserLeave(String), - ChatMessage { - #[serde(default = "String::new")] - user: String, - message: String, - }, + UserJoin, + UserLeave, + ChatMessage(String), +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct WatchEvent { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user: Option, + #[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, + } + } } diff --git a/src/main.rs b/src/main.rs index 786bbf9..cbda565 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,9 +12,9 @@ mod watch_session; use serde::Deserialize; use crate::{ - events::WatchEvent, + events::{WatchEvent, WatchEventData}, viewer_connection::{ws_publish, ws_subscribe}, - watch_session::{get_session, handle_watch_event, SubtitleTrack, WatchSession, SESSIONS}, + watch_session::{get_session, handle_watch_event_data, SubtitleTrack, WatchSession, SESSIONS}, }; #[derive(Deserialize)] @@ -85,13 +85,21 @@ async fn main() { .and(warb::body::json()) .map(|requested_session, playing: bool| match requested_session { RequestedSession::Session(uuid, mut sess) => { - let event = WatchEvent::SetPlaying { + let data = WatchEventData::SetPlaying { playing, time: sess.get_time_ms(), }; - handle_watch_event(uuid, &mut sess, event.clone()); - tokio::spawn(ws_publish(uuid, None, event)); + handle_watch_event_data(uuid, &mut sess, data.clone()); + tokio::spawn(ws_publish( + uuid, + None, + WatchEvent { + user: None, + data, + reflected: false, + }, + )); warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK) } @@ -105,10 +113,18 @@ async fn main() { .map( |requested_session, current_time_ms: u64| match requested_session { RequestedSession::Session(uuid, mut sess) => { - let event = WatchEvent::SetTime(current_time_ms); + let data = WatchEventData::SetTime(current_time_ms); - handle_watch_event(uuid, &mut sess, event.clone()); - tokio::spawn(ws_publish(uuid, None, event)); + handle_watch_event_data(uuid, &mut sess, data.clone()); + tokio::spawn(ws_publish( + uuid, + None, + WatchEvent { + user: None, + data, + reflected: false, + }, + )); warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK) } diff --git a/src/viewer_connection.rs b/src/viewer_connection.rs index 7a8b68a..7ceb490 100644 --- a/src/viewer_connection.rs +++ b/src/viewer_connection.rs @@ -15,8 +15,8 @@ use uuid::Uuid; use warp::ws::{Message, WebSocket}; use crate::{ - events::WatchEvent, - watch_session::{get_session, handle_watch_event}, + events::{WatchEvent, WatchEventData}, + watch_session::{get_session, handle_watch_event_data}, }; static CONNECTED_VIEWERS: Lazy>> = @@ -27,6 +27,7 @@ pub struct ConnectedViewer { pub session: Uuid, pub viewer_id: usize, pub tx: UnboundedSender, + pub nickname: Option, } pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) { @@ -53,13 +54,19 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) { viewer_id, session: session_uuid, tx, + nickname: Some(nickname.clone()), }, ); - ws_publish(session_uuid, None, WatchEvent::UserJoin(nickname.clone())).await; + ws_publish( + session_uuid, + None, + WatchEvent::new(nickname.clone(), WatchEventData::UserJoin), + ) + .await; while let Some(Ok(message)) = viewer_ws_rx.next().await { - let mut event: WatchEvent = match message + let event: WatchEventData = match message .to_str() .ok() .and_then(|s| serde_json::from_str(s).ok()) @@ -68,47 +75,39 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, 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( + handle_watch_event_data( session_uuid, &mut get_session(session_uuid).unwrap(), event.clone(), ); - ws_publish(session_uuid, Some(viewer_id), event).await; + ws_publish( + session_uuid, + Some(viewer_id), + WatchEvent::new(nickname.clone(), event), + ) + .await; } - ws_publish(session_uuid, None, WatchEvent::UserLeave(nickname.clone())).await; + ws_publish( + session_uuid, + None, + WatchEvent::new(nickname.clone(), WatchEventData::UserLeave), + ) + .await; CONNECTED_VIEWERS.write().await.remove(&viewer_id); } -pub async fn ws_publish(session_uuid: Uuid, viewer_id: Option, event: WatchEvent) { +pub async fn ws_publish(session_uuid: Uuid, skip_viewer_id: Option, event: WatchEvent) { for viewer in CONNECTED_VIEWERS.read().await.values() { - if viewer_id == Some(viewer.viewer_id) { - continue; - } - if viewer.session != session_uuid { continue; } - let _ = viewer.tx.send(event.clone()); + let _ = viewer.tx.send(WatchEvent { + reflected: skip_viewer_id == Some(viewer.viewer_id), + ..event.clone() + }); } } diff --git a/src/watch_session.rs b/src/watch_session.rs index 3ff1c17..56b6634 100644 --- a/src/watch_session.rs +++ b/src/watch_session.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Mutex, time::Instant}; use uuid::Uuid; -use crate::events::WatchEvent; +use crate::events::WatchEventData; #[derive(Serialize, Deserialize, Clone)] pub struct SubtitleTrack { @@ -75,13 +75,17 @@ pub fn get_session(uuid: Uuid) -> Option { SESSIONS.lock().unwrap().get(&uuid).cloned() } -pub fn handle_watch_event(uuid: Uuid, watch_session: &mut WatchSession, event: WatchEvent) { +pub fn handle_watch_event_data( + uuid: Uuid, + watch_session: &mut WatchSession, + event: WatchEventData, +) { match event { - WatchEvent::SetPlaying { playing, time } => { + WatchEventData::SetPlaying { playing, time } => { watch_session.set_playing(playing, time); } - WatchEvent::SetTime(time) => { + WatchEventData::SetTime(time) => { watch_session.set_time_ms(time); }