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