forked from lavender/watch-party
		
	fuck it, converting the rest to unix line endings
This commit is contained in:
		
							parent
							
								
									1bd7071cec
								
							
						
					
					
						commit
						f3ee2ecc83
					
				
					 4 changed files with 436 additions and 436 deletions
				
			
		
							
								
								
									
										104
									
								
								src/events.rs
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								src/events.rs
									
									
									
									
									
								
							|  | @ -1,52 +1,52 @@ | |||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct Viewer { | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub nickname: Option<String>, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub colour: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| #[serde(tag = "op", content = "data")] | ||||
| pub enum WatchEventData { | ||||
|     SetPlaying { | ||||
|         playing: bool, | ||||
|         time: u64, | ||||
|     }, | ||||
|     SetTime { | ||||
|         #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|         from: Option<u64>, | ||||
|         to: u64, | ||||
|     }, | ||||
| 
 | ||||
|     UserJoin, | ||||
|     UserLeave, | ||||
|     ChatMessage(String), | ||||
|     Ping(String), | ||||
|     UpdateViewerList(Vec<Viewer>), | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct WatchEvent { | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub user: Option<String>, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub colour: Option<String>, | ||||
|     #[serde(flatten)] | ||||
|     pub data: WatchEventData, | ||||
|     #[serde(default)] | ||||
|     pub reflected: bool, | ||||
| } | ||||
| 
 | ||||
| impl WatchEvent { | ||||
|     pub fn new(user: String, colour: String, data: WatchEventData) -> Self { | ||||
|         WatchEvent { | ||||
|             user: Some(user), | ||||
|             colour: Some(colour), | ||||
|             data, | ||||
|             reflected: false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct Viewer { | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub nickname: Option<String>, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub colour: Option<String>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| #[serde(tag = "op", content = "data")] | ||||
| pub enum WatchEventData { | ||||
|     SetPlaying { | ||||
|         playing: bool, | ||||
|         time: u64, | ||||
|     }, | ||||
|     SetTime { | ||||
|         #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|         from: Option<u64>, | ||||
|         to: u64, | ||||
|     }, | ||||
| 
 | ||||
|     UserJoin, | ||||
|     UserLeave, | ||||
|     ChatMessage(String), | ||||
|     Ping(String), | ||||
|     UpdateViewerList(Vec<Viewer>), | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| pub struct WatchEvent { | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub user: Option<String>, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub colour: Option<String>, | ||||
|     #[serde(flatten)] | ||||
|     pub data: WatchEventData, | ||||
|     #[serde(default)] | ||||
|     pub reflected: bool, | ||||
| } | ||||
| 
 | ||||
| impl WatchEvent { | ||||
|     pub fn new(user: String, colour: String, data: WatchEventData) -> Self { | ||||
|         WatchEvent { | ||||
|             user: Some(user), | ||||
|             colour: Some(colour), | ||||
|             data, | ||||
|             reflected: false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										264
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										264
									
								
								src/main.rs
									
									
									
									
									
								
							|  | @ -1,132 +1,132 @@ | |||
| use serde_json::json; | ||||
| use std::net::IpAddr; | ||||
| use uuid::Uuid; | ||||
| 
 | ||||
| use warb::{hyper::StatusCode, Filter, Reply}; | ||||
| use warp as warb; // i think it's funny
 | ||||
| 
 | ||||
| mod events; | ||||
| mod utils; | ||||
| mod viewer_connection; | ||||
| mod watch_session; | ||||
| 
 | ||||
| use serde::Deserialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     viewer_connection::ws_subscribe, | ||||
|     watch_session::{get_session, SubtitleTrack, WatchSession, SESSIONS}, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| struct StartSessionBody { | ||||
|     video_url: String, | ||||
|     #[serde(default = "Vec::new")] | ||||
|     subtitle_tracks: Vec<SubtitleTrack>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| struct SubscribeQuery { | ||||
|     nickname: String, | ||||
|     colour: String, | ||||
| } | ||||
| 
 | ||||
| async fn get_emoji_list() -> Result<impl warb::Reply, warb::Rejection> { | ||||
|     use tokio_stream::{wrappers::ReadDirStream, StreamExt}; | ||||
| 
 | ||||
|     let dir = tokio::fs::read_dir("frontend/emojis") | ||||
|         .await | ||||
|         .expect("Couldn't read emojis directory!"); | ||||
| 
 | ||||
|     let files = ReadDirStream::new(dir) | ||||
|         .filter_map(|r| r.ok()) | ||||
|         .map(|e| e.file_name().to_string_lossy().to_string()) | ||||
|         .collect::<Vec<_>>() | ||||
|         .await; | ||||
| 
 | ||||
|     Ok(warb::reply::json(&files)) | ||||
| } | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     let start_session_route = warb::path!("start_session") | ||||
|         .and(warb::path::end()) | ||||
|         .and(warb::post()) | ||||
|         .and(warb::body::json()) | ||||
|         .map(|body: StartSessionBody| { | ||||
|             let mut sessions = SESSIONS.lock().unwrap(); | ||||
|             let session_uuid = Uuid::new_v4(); | ||||
|             let session = WatchSession::new(body.video_url, body.subtitle_tracks); | ||||
|             let session_view = session.view(); | ||||
|             sessions.insert(session_uuid, session); | ||||
| 
 | ||||
|             warb::reply::json(&json!({ "id": session_uuid.to_string(), "session": session_view })) | ||||
|         }); | ||||
| 
 | ||||
|     let get_emoji_route = warb::path!("emojos").and_then(get_emoji_list); | ||||
| 
 | ||||
|     enum RequestedSession { | ||||
|         Session(Uuid, WatchSession), | ||||
|         Error(warb::reply::WithStatus<warb::reply::Json>), | ||||
|     } | ||||
| 
 | ||||
|     let get_running_session = warb::path::path("sess") | ||||
|         .and(warb::path::param::<String>()) | ||||
|         .map(|session_id: String| { | ||||
|             if let Ok(uuid) = Uuid::parse_str(&session_id) { | ||||
|                 get_session(uuid) | ||||
|                     .map(|sess| RequestedSession::Session(uuid, sess)) | ||||
|                     .unwrap_or_else(|| { | ||||
|                         RequestedSession::Error(warb::reply::with_status( | ||||
|                             warb::reply::json(&json!({ "error": "session does not exist" })), | ||||
|                             StatusCode::NOT_FOUND, | ||||
|                         )) | ||||
|                     }) | ||||
|             } else { | ||||
|                 RequestedSession::Error(warb::reply::with_status( | ||||
|                     warb::reply::json(&json!({ "error": "invalid session UUID" })), | ||||
|                     StatusCode::BAD_REQUEST, | ||||
|                 )) | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|     let get_status_route = get_running_session | ||||
|         .and(warb::path::end()) | ||||
|         .map(|requested_session| match requested_session { | ||||
|             RequestedSession::Session(_, sess) => { | ||||
|                 warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK) | ||||
|             } | ||||
|             RequestedSession::Error(e) => e, | ||||
|         }); | ||||
| 
 | ||||
|     let ws_subscribe_route = get_running_session | ||||
|         .and(warb::path!("subscribe")) | ||||
|         .and(warb::query()) | ||||
|         .and(warb::ws()) | ||||
|         .map( | ||||
|             |requested_session, query: SubscribeQuery, ws: warb::ws::Ws| match requested_session { | ||||
|                 RequestedSession::Session(uuid, _) => ws | ||||
|                     .on_upgrade(move |ws| ws_subscribe(uuid, query.nickname, query.colour, ws)) | ||||
|                     .into_response(), | ||||
|                 RequestedSession::Error(error_response) => error_response.into_response(), | ||||
|             }, | ||||
|         ); | ||||
| 
 | ||||
|     let routes = start_session_route | ||||
|         .or(get_status_route) | ||||
|         .or(ws_subscribe_route) | ||||
|         .or(get_emoji_route) | ||||
|         .or(warb::path::end().and(warb::fs::file("frontend/index.html"))) | ||||
|         .or(warb::fs::dir("frontend")); | ||||
| 
 | ||||
|     let ip = std::env::var("IP") | ||||
|         .ok() | ||||
|         .and_then(|s| s.parse::<IpAddr>().ok()) | ||||
|         .unwrap_or_else(|| [127, 0, 0, 1].into()); | ||||
|     let port = std::env::var("PORT") | ||||
|         .ok() | ||||
|         .and_then(|s| s.parse::<u16>().ok()) | ||||
|         .unwrap_or(3000); | ||||
| 
 | ||||
|     println!("Listening at http://{}:{} ...", &ip, &port); | ||||
|     warb::serve(routes).run((ip, port)).await; | ||||
| } | ||||
| use serde_json::json; | ||||
| use std::net::IpAddr; | ||||
| use uuid::Uuid; | ||||
| 
 | ||||
| use warb::{hyper::StatusCode, Filter, Reply}; | ||||
| use warp as warb; // i think it's funny
 | ||||
| 
 | ||||
| mod events; | ||||
| mod utils; | ||||
| mod viewer_connection; | ||||
| mod watch_session; | ||||
| 
 | ||||
| use serde::Deserialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     viewer_connection::ws_subscribe, | ||||
|     watch_session::{get_session, SubtitleTrack, WatchSession, SESSIONS}, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| struct StartSessionBody { | ||||
|     video_url: String, | ||||
|     #[serde(default = "Vec::new")] | ||||
|     subtitle_tracks: Vec<SubtitleTrack>, | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| struct SubscribeQuery { | ||||
|     nickname: String, | ||||
|     colour: String, | ||||
| } | ||||
| 
 | ||||
| async fn get_emoji_list() -> Result<impl warb::Reply, warb::Rejection> { | ||||
|     use tokio_stream::{wrappers::ReadDirStream, StreamExt}; | ||||
| 
 | ||||
|     let dir = tokio::fs::read_dir("frontend/emojis") | ||||
|         .await | ||||
|         .expect("Couldn't read emojis directory!"); | ||||
| 
 | ||||
|     let files = ReadDirStream::new(dir) | ||||
|         .filter_map(|r| r.ok()) | ||||
|         .map(|e| e.file_name().to_string_lossy().to_string()) | ||||
|         .collect::<Vec<_>>() | ||||
|         .await; | ||||
| 
 | ||||
|     Ok(warb::reply::json(&files)) | ||||
| } | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     let start_session_route = warb::path!("start_session") | ||||
|         .and(warb::path::end()) | ||||
|         .and(warb::post()) | ||||
|         .and(warb::body::json()) | ||||
|         .map(|body: StartSessionBody| { | ||||
|             let mut sessions = SESSIONS.lock().unwrap(); | ||||
|             let session_uuid = Uuid::new_v4(); | ||||
|             let session = WatchSession::new(body.video_url, body.subtitle_tracks); | ||||
|             let session_view = session.view(); | ||||
|             sessions.insert(session_uuid, session); | ||||
| 
 | ||||
|             warb::reply::json(&json!({ "id": session_uuid.to_string(), "session": session_view })) | ||||
|         }); | ||||
| 
 | ||||
|     let get_emoji_route = warb::path!("emojos").and_then(get_emoji_list); | ||||
| 
 | ||||
|     enum RequestedSession { | ||||
|         Session(Uuid, WatchSession), | ||||
|         Error(warb::reply::WithStatus<warb::reply::Json>), | ||||
|     } | ||||
| 
 | ||||
|     let get_running_session = warb::path::path("sess") | ||||
|         .and(warb::path::param::<String>()) | ||||
|         .map(|session_id: String| { | ||||
|             if let Ok(uuid) = Uuid::parse_str(&session_id) { | ||||
|                 get_session(uuid) | ||||
|                     .map(|sess| RequestedSession::Session(uuid, sess)) | ||||
|                     .unwrap_or_else(|| { | ||||
|                         RequestedSession::Error(warb::reply::with_status( | ||||
|                             warb::reply::json(&json!({ "error": "session does not exist" })), | ||||
|                             StatusCode::NOT_FOUND, | ||||
|                         )) | ||||
|                     }) | ||||
|             } else { | ||||
|                 RequestedSession::Error(warb::reply::with_status( | ||||
|                     warb::reply::json(&json!({ "error": "invalid session UUID" })), | ||||
|                     StatusCode::BAD_REQUEST, | ||||
|                 )) | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|     let get_status_route = get_running_session | ||||
|         .and(warb::path::end()) | ||||
|         .map(|requested_session| match requested_session { | ||||
|             RequestedSession::Session(_, sess) => { | ||||
|                 warb::reply::with_status(warb::reply::json(&sess.view()), StatusCode::OK) | ||||
|             } | ||||
|             RequestedSession::Error(e) => e, | ||||
|         }); | ||||
| 
 | ||||
|     let ws_subscribe_route = get_running_session | ||||
|         .and(warb::path!("subscribe")) | ||||
|         .and(warb::query()) | ||||
|         .and(warb::ws()) | ||||
|         .map( | ||||
|             |requested_session, query: SubscribeQuery, ws: warb::ws::Ws| match requested_session { | ||||
|                 RequestedSession::Session(uuid, _) => ws | ||||
|                     .on_upgrade(move |ws| ws_subscribe(uuid, query.nickname, query.colour, ws)) | ||||
|                     .into_response(), | ||||
|                 RequestedSession::Error(error_response) => error_response.into_response(), | ||||
|             }, | ||||
|         ); | ||||
| 
 | ||||
|     let routes = start_session_route | ||||
|         .or(get_status_route) | ||||
|         .or(ws_subscribe_route) | ||||
|         .or(get_emoji_route) | ||||
|         .or(warb::path::end().and(warb::fs::file("frontend/index.html"))) | ||||
|         .or(warb::fs::dir("frontend")); | ||||
| 
 | ||||
|     let ip = std::env::var("IP") | ||||
|         .ok() | ||||
|         .and_then(|s| s.parse::<IpAddr>().ok()) | ||||
|         .unwrap_or_else(|| [127, 0, 0, 1].into()); | ||||
|     let port = std::env::var("PORT") | ||||
|         .ok() | ||||
|         .and_then(|s| s.parse::<u16>().ok()) | ||||
|         .unwrap_or(3000); | ||||
| 
 | ||||
|     println!("Listening at http://{}:{} ...", &ip, &port); | ||||
|     warb::serve(routes).run((ip, port)).await; | ||||
| } | ||||
|  |  | |||
|  | @ -1,156 +1,156 @@ | |||
| use once_cell::sync::Lazy; | ||||
| use std::{ | ||||
|     collections::HashMap, | ||||
|     sync::atomic::{AtomicUsize, Ordering}, | ||||
| }; | ||||
| 
 | ||||
| use futures::{SinkExt, StreamExt, TryFutureExt}; | ||||
| use tokio::sync::{ | ||||
|     mpsc::{self, UnboundedSender}, | ||||
|     RwLock, | ||||
| }; | ||||
| use tokio_stream::wrappers::UnboundedReceiverStream; | ||||
| 
 | ||||
| use uuid::Uuid; | ||||
| use warp::ws::{Message, WebSocket}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     events::{Viewer, WatchEvent, WatchEventData}, | ||||
|     utils::truncate_str, | ||||
|     watch_session::{get_session, handle_watch_event_data}, | ||||
| }; | ||||
| 
 | ||||
| static CONNECTED_VIEWERS: Lazy<RwLock<HashMap<usize, ConnectedViewer>>> = | ||||
|     Lazy::new(|| RwLock::new(HashMap::new())); | ||||
| static NEXT_VIEWER_ID: AtomicUsize = AtomicUsize::new(1); | ||||
| 
 | ||||
| pub struct ConnectedViewer { | ||||
|     pub session: Uuid, | ||||
|     pub viewer_id: usize, | ||||
|     pub tx: UnboundedSender<WatchEvent>, | ||||
|     pub nickname: Option<String>, | ||||
|     pub colour: Option<String>, | ||||
| } | ||||
| 
 | ||||
| pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, colour: 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(); | ||||
| 
 | ||||
|     let (tx, rx) = mpsc::unbounded_channel::<WatchEvent>(); | ||||
|     let mut rx = UnboundedReceiverStream::new(rx); | ||||
| 
 | ||||
|     tokio::task::spawn(async move { | ||||
|         while let Some(event) = rx.next().await { | ||||
|             viewer_ws_tx | ||||
|                 .send(Message::text( | ||||
|                     serde_json::to_string(&event).expect("couldn't convert WatchEvent into JSON"), | ||||
|                 )) | ||||
|                 .unwrap_or_else(|e| eprintln!("ws send error: {}", e)) | ||||
|                 .await; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     let mut colour = colour; | ||||
|     if colour.len() != 6 || !colour.chars().all(|x| x.is_ascii_hexdigit()) { | ||||
|         colour = String::from("7ed0ff"); | ||||
|     } | ||||
|     let nickname = truncate_str(&nickname, 50).to_string(); | ||||
| 
 | ||||
|     CONNECTED_VIEWERS.write().await.insert( | ||||
|         viewer_id, | ||||
|         ConnectedViewer { | ||||
|             viewer_id, | ||||
|             session: session_uuid, | ||||
|             tx, | ||||
|             nickname: Some(nickname.clone()), | ||||
|             colour: Some(colour.clone()), | ||||
|         }, | ||||
|     ); | ||||
| 
 | ||||
|     ws_publish( | ||||
|         session_uuid, | ||||
|         None, | ||||
|         WatchEvent::new(nickname.clone(), colour.clone(), WatchEventData::UserJoin), | ||||
|     ) | ||||
|     .await; | ||||
| 
 | ||||
|     update_viewer_list(session_uuid).await; | ||||
| 
 | ||||
|     while let Some(Ok(message)) = viewer_ws_rx.next().await { | ||||
|         let event: WatchEventData = match message | ||||
|             .to_str() | ||||
|             .ok() | ||||
|             .and_then(|s| serde_json::from_str(s).ok()) | ||||
|         { | ||||
|             Some(e) => e, | ||||
|             None => continue, | ||||
|         }; | ||||
| 
 | ||||
|         let session = &mut get_session(session_uuid).unwrap(); | ||||
| 
 | ||||
|         // server side event modification where neccessary
 | ||||
|         let event: WatchEventData = match event { | ||||
|             WatchEventData::SetTime { from: _, to } => WatchEventData::SetTime { | ||||
|                 from: Some(session.get_time_ms()), | ||||
|                 to, | ||||
|             }, | ||||
|             _ => event, | ||||
|         }; | ||||
| 
 | ||||
|         handle_watch_event_data(session_uuid, session, event.clone()); | ||||
| 
 | ||||
|         ws_publish( | ||||
|             session_uuid, | ||||
|             Some(viewer_id), | ||||
|             WatchEvent::new(nickname.clone(), colour.clone(), event), | ||||
|         ) | ||||
|         .await; | ||||
|     } | ||||
| 
 | ||||
|     ws_publish( | ||||
|         session_uuid, | ||||
|         None, | ||||
|         WatchEvent::new(nickname.clone(), colour.clone(), WatchEventData::UserLeave), | ||||
|     ) | ||||
|     .await; | ||||
| 
 | ||||
|     CONNECTED_VIEWERS.write().await.remove(&viewer_id); | ||||
|     update_viewer_list(session_uuid).await; | ||||
| } | ||||
| 
 | ||||
| pub async fn ws_publish(session_uuid: Uuid, skip_viewer_id: Option<usize>, event: WatchEvent) { | ||||
|     for viewer in CONNECTED_VIEWERS.read().await.values() { | ||||
|         if viewer.session != session_uuid { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         let _ = viewer.tx.send(WatchEvent { | ||||
|             reflected: skip_viewer_id == Some(viewer.viewer_id), | ||||
|             ..event.clone() | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async fn update_viewer_list(session_uuid: Uuid) { | ||||
|     let mut viewers = Vec::new(); | ||||
| 
 | ||||
|     for viewer in CONNECTED_VIEWERS.read().await.values() { | ||||
|         if viewer.session == session_uuid { | ||||
|             viewers.push(Viewer { | ||||
|                 nickname: viewer.nickname.clone(), | ||||
|                 colour: viewer.colour.clone(), | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ws_publish( | ||||
|         session_uuid, | ||||
|         None, | ||||
|         WatchEvent::new( | ||||
|             String::from("server"), | ||||
|             String::from(""), | ||||
|             WatchEventData::UpdateViewerList(viewers), | ||||
|         ), | ||||
|     ) | ||||
|     .await; | ||||
| } | ||||
| use once_cell::sync::Lazy; | ||||
| use std::{ | ||||
|     collections::HashMap, | ||||
|     sync::atomic::{AtomicUsize, Ordering}, | ||||
| }; | ||||
| 
 | ||||
| use futures::{SinkExt, StreamExt, TryFutureExt}; | ||||
| use tokio::sync::{ | ||||
|     mpsc::{self, UnboundedSender}, | ||||
|     RwLock, | ||||
| }; | ||||
| use tokio_stream::wrappers::UnboundedReceiverStream; | ||||
| 
 | ||||
| use uuid::Uuid; | ||||
| use warp::ws::{Message, WebSocket}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     events::{Viewer, WatchEvent, WatchEventData}, | ||||
|     utils::truncate_str, | ||||
|     watch_session::{get_session, handle_watch_event_data}, | ||||
| }; | ||||
| 
 | ||||
| static CONNECTED_VIEWERS: Lazy<RwLock<HashMap<usize, ConnectedViewer>>> = | ||||
|     Lazy::new(|| RwLock::new(HashMap::new())); | ||||
| static NEXT_VIEWER_ID: AtomicUsize = AtomicUsize::new(1); | ||||
| 
 | ||||
| pub struct ConnectedViewer { | ||||
|     pub session: Uuid, | ||||
|     pub viewer_id: usize, | ||||
|     pub tx: UnboundedSender<WatchEvent>, | ||||
|     pub nickname: Option<String>, | ||||
|     pub colour: Option<String>, | ||||
| } | ||||
| 
 | ||||
| pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, colour: 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(); | ||||
| 
 | ||||
|     let (tx, rx) = mpsc::unbounded_channel::<WatchEvent>(); | ||||
|     let mut rx = UnboundedReceiverStream::new(rx); | ||||
| 
 | ||||
|     tokio::task::spawn(async move { | ||||
|         while let Some(event) = rx.next().await { | ||||
|             viewer_ws_tx | ||||
|                 .send(Message::text( | ||||
|                     serde_json::to_string(&event).expect("couldn't convert WatchEvent into JSON"), | ||||
|                 )) | ||||
|                 .unwrap_or_else(|e| eprintln!("ws send error: {}", e)) | ||||
|                 .await; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     let mut colour = colour; | ||||
|     if colour.len() != 6 || !colour.chars().all(|x| x.is_ascii_hexdigit()) { | ||||
|         colour = String::from("7ed0ff"); | ||||
|     } | ||||
|     let nickname = truncate_str(&nickname, 50).to_string(); | ||||
| 
 | ||||
|     CONNECTED_VIEWERS.write().await.insert( | ||||
|         viewer_id, | ||||
|         ConnectedViewer { | ||||
|             viewer_id, | ||||
|             session: session_uuid, | ||||
|             tx, | ||||
|             nickname: Some(nickname.clone()), | ||||
|             colour: Some(colour.clone()), | ||||
|         }, | ||||
|     ); | ||||
| 
 | ||||
|     ws_publish( | ||||
|         session_uuid, | ||||
|         None, | ||||
|         WatchEvent::new(nickname.clone(), colour.clone(), WatchEventData::UserJoin), | ||||
|     ) | ||||
|     .await; | ||||
| 
 | ||||
|     update_viewer_list(session_uuid).await; | ||||
| 
 | ||||
|     while let Some(Ok(message)) = viewer_ws_rx.next().await { | ||||
|         let event: WatchEventData = match message | ||||
|             .to_str() | ||||
|             .ok() | ||||
|             .and_then(|s| serde_json::from_str(s).ok()) | ||||
|         { | ||||
|             Some(e) => e, | ||||
|             None => continue, | ||||
|         }; | ||||
| 
 | ||||
|         let session = &mut get_session(session_uuid).unwrap(); | ||||
| 
 | ||||
|         // server side event modification where neccessary
 | ||||
|         let event: WatchEventData = match event { | ||||
|             WatchEventData::SetTime { from: _, to } => WatchEventData::SetTime { | ||||
|                 from: Some(session.get_time_ms()), | ||||
|                 to, | ||||
|             }, | ||||
|             _ => event, | ||||
|         }; | ||||
| 
 | ||||
|         handle_watch_event_data(session_uuid, session, event.clone()); | ||||
| 
 | ||||
|         ws_publish( | ||||
|             session_uuid, | ||||
|             Some(viewer_id), | ||||
|             WatchEvent::new(nickname.clone(), colour.clone(), event), | ||||
|         ) | ||||
|         .await; | ||||
|     } | ||||
| 
 | ||||
|     ws_publish( | ||||
|         session_uuid, | ||||
|         None, | ||||
|         WatchEvent::new(nickname.clone(), colour.clone(), WatchEventData::UserLeave), | ||||
|     ) | ||||
|     .await; | ||||
| 
 | ||||
|     CONNECTED_VIEWERS.write().await.remove(&viewer_id); | ||||
|     update_viewer_list(session_uuid).await; | ||||
| } | ||||
| 
 | ||||
| pub async fn ws_publish(session_uuid: Uuid, skip_viewer_id: Option<usize>, event: WatchEvent) { | ||||
|     for viewer in CONNECTED_VIEWERS.read().await.values() { | ||||
|         if viewer.session != session_uuid { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         let _ = viewer.tx.send(WatchEvent { | ||||
|             reflected: skip_viewer_id == Some(viewer.viewer_id), | ||||
|             ..event.clone() | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async fn update_viewer_list(session_uuid: Uuid) { | ||||
|     let mut viewers = Vec::new(); | ||||
| 
 | ||||
|     for viewer in CONNECTED_VIEWERS.read().await.values() { | ||||
|         if viewer.session == session_uuid { | ||||
|             viewers.push(Viewer { | ||||
|                 nickname: viewer.nickname.clone(), | ||||
|                 colour: viewer.colour.clone(), | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ws_publish( | ||||
|         session_uuid, | ||||
|         None, | ||||
|         WatchEvent::new( | ||||
|             String::from("server"), | ||||
|             String::from(""), | ||||
|             WatchEventData::UpdateViewerList(viewers), | ||||
|         ), | ||||
|     ) | ||||
|     .await; | ||||
| } | ||||
|  |  | |||
|  | @ -1,96 +1,96 @@ | |||
| use once_cell::sync::Lazy; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::{collections::HashMap, sync::Mutex, time::Instant}; | ||||
| use uuid::Uuid; | ||||
| 
 | ||||
| use crate::events::WatchEventData; | ||||
| 
 | ||||
| #[derive(Serialize, Deserialize, Clone)] | ||||
| pub struct SubtitleTrack { | ||||
|     pub url: String, | ||||
|     pub name: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct WatchSession { | ||||
|     pub video_url: String, | ||||
|     pub subtitle_tracks: Vec<SubtitleTrack>, | ||||
| 
 | ||||
|     is_playing: bool, | ||||
|     playing_from_timestamp: u64, | ||||
|     playing_from_instant: Instant, | ||||
| } | ||||
| 
 | ||||
| #[derive(Serialize)] | ||||
| pub struct WatchSessionView { | ||||
|     pub video_url: String, | ||||
|     pub subtitle_tracks: Vec<SubtitleTrack>, | ||||
|     pub current_time_ms: u64, | ||||
|     pub is_playing: bool, | ||||
| } | ||||
| 
 | ||||
| impl WatchSession { | ||||
|     pub fn new(video_url: String, subtitle_tracks: Vec<SubtitleTrack>) -> Self { | ||||
|         WatchSession { | ||||
|             video_url, | ||||
|             subtitle_tracks, | ||||
|             is_playing: false, | ||||
|             playing_from_timestamp: 0, | ||||
|             playing_from_instant: Instant::now(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn view(&self) -> WatchSessionView { | ||||
|         WatchSessionView { | ||||
|             video_url: self.video_url.clone(), | ||||
|             subtitle_tracks: self.subtitle_tracks.clone(), | ||||
|             current_time_ms: self.get_time_ms() as u64, | ||||
|             is_playing: self.is_playing, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_time_ms(&self) -> u64 { | ||||
|         if !self.is_playing { | ||||
|             return self.playing_from_timestamp; | ||||
|         } | ||||
| 
 | ||||
|         self.playing_from_timestamp + self.playing_from_instant.elapsed().as_millis() as u64 | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_time_ms(&mut self, time_ms: u64) { | ||||
|         self.playing_from_timestamp = time_ms; | ||||
|         self.playing_from_instant = Instant::now(); | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_playing(&mut self, playing: bool, time_ms: u64) { | ||||
|         self.set_time_ms(time_ms); | ||||
|         self.is_playing = playing; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub static SESSIONS: Lazy<Mutex<HashMap<Uuid, WatchSession>>> = | ||||
|     Lazy::new(|| Mutex::new(HashMap::new())); | ||||
| 
 | ||||
| pub fn get_session(uuid: Uuid) -> Option<WatchSession> { | ||||
|     SESSIONS.lock().unwrap().get(&uuid).cloned() | ||||
| } | ||||
| 
 | ||||
| pub fn handle_watch_event_data( | ||||
|     uuid: Uuid, | ||||
|     watch_session: &mut WatchSession, | ||||
|     event: WatchEventData, | ||||
| ) { | ||||
|     match event { | ||||
|         WatchEventData::SetPlaying { playing, time } => { | ||||
|             watch_session.set_playing(playing, time); | ||||
|         } | ||||
| 
 | ||||
|         WatchEventData::SetTime { from: _, to } => { | ||||
|             watch_session.set_time_ms(to); | ||||
|         } | ||||
| 
 | ||||
|         _ => {} | ||||
|     }; | ||||
| 
 | ||||
|     let _ = SESSIONS.lock().unwrap().insert(uuid, watch_session.clone()); | ||||
| } | ||||
| use once_cell::sync::Lazy; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::{collections::HashMap, sync::Mutex, time::Instant}; | ||||
| use uuid::Uuid; | ||||
| 
 | ||||
| use crate::events::WatchEventData; | ||||
| 
 | ||||
| #[derive(Serialize, Deserialize, Clone)] | ||||
| pub struct SubtitleTrack { | ||||
|     pub url: String, | ||||
|     pub name: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct WatchSession { | ||||
|     pub video_url: String, | ||||
|     pub subtitle_tracks: Vec<SubtitleTrack>, | ||||
| 
 | ||||
|     is_playing: bool, | ||||
|     playing_from_timestamp: u64, | ||||
|     playing_from_instant: Instant, | ||||
| } | ||||
| 
 | ||||
| #[derive(Serialize)] | ||||
| pub struct WatchSessionView { | ||||
|     pub video_url: String, | ||||
|     pub subtitle_tracks: Vec<SubtitleTrack>, | ||||
|     pub current_time_ms: u64, | ||||
|     pub is_playing: bool, | ||||
| } | ||||
| 
 | ||||
| impl WatchSession { | ||||
|     pub fn new(video_url: String, subtitle_tracks: Vec<SubtitleTrack>) -> Self { | ||||
|         WatchSession { | ||||
|             video_url, | ||||
|             subtitle_tracks, | ||||
|             is_playing: false, | ||||
|             playing_from_timestamp: 0, | ||||
|             playing_from_instant: Instant::now(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn view(&self) -> WatchSessionView { | ||||
|         WatchSessionView { | ||||
|             video_url: self.video_url.clone(), | ||||
|             subtitle_tracks: self.subtitle_tracks.clone(), | ||||
|             current_time_ms: self.get_time_ms() as u64, | ||||
|             is_playing: self.is_playing, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_time_ms(&self) -> u64 { | ||||
|         if !self.is_playing { | ||||
|             return self.playing_from_timestamp; | ||||
|         } | ||||
| 
 | ||||
|         self.playing_from_timestamp + self.playing_from_instant.elapsed().as_millis() as u64 | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_time_ms(&mut self, time_ms: u64) { | ||||
|         self.playing_from_timestamp = time_ms; | ||||
|         self.playing_from_instant = Instant::now(); | ||||
|     } | ||||
| 
 | ||||
|     pub fn set_playing(&mut self, playing: bool, time_ms: u64) { | ||||
|         self.set_time_ms(time_ms); | ||||
|         self.is_playing = playing; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub static SESSIONS: Lazy<Mutex<HashMap<Uuid, WatchSession>>> = | ||||
|     Lazy::new(|| Mutex::new(HashMap::new())); | ||||
| 
 | ||||
| pub fn get_session(uuid: Uuid) -> Option<WatchSession> { | ||||
|     SESSIONS.lock().unwrap().get(&uuid).cloned() | ||||
| } | ||||
| 
 | ||||
| pub fn handle_watch_event_data( | ||||
|     uuid: Uuid, | ||||
|     watch_session: &mut WatchSession, | ||||
|     event: WatchEventData, | ||||
| ) { | ||||
|     match event { | ||||
|         WatchEventData::SetPlaying { playing, time } => { | ||||
|             watch_session.set_playing(playing, time); | ||||
|         } | ||||
| 
 | ||||
|         WatchEventData::SetTime { from: _, to } => { | ||||
|             watch_session.set_time_ms(to); | ||||
|         } | ||||
| 
 | ||||
|         _ => {} | ||||
|     }; | ||||
| 
 | ||||
|     let _ = SESSIONS.lock().unwrap().insert(uuid, watch_session.clone()); | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue