forked from lavender/watch-party
Prepare for adding the chat box
parent
7796d8e5f0
commit
d48771e921
|
@ -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>
|
||||||
|
|
|
@ -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\-]+/)) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
18
src/main.rs
18
src/main.rs
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
Loading…
Reference in New Issue