diff options
-rw-r--r-- | Cargo.lock | 144 | ||||
-rw-r--r-- | server/Cargo.toml | 1 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 24 | ||||
-rw-r--r-- | server/src/routes/playersync.rs | 55 | ||||
-rw-r--r-- | server/src/routes/streamsync.rs | 11 | ||||
-rw-r--r-- | web/script/player/mod.ts | 33 | ||||
-rw-r--r-- | web/script/player/sync.ts | 92 | ||||
-rw-r--r-- | web/style/js-player.css | 38 |
8 files changed, 358 insertions, 40 deletions
@@ -428,7 +428,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ - "smallvec", + "smallvec 1.11.2", "target-lexicon", ] @@ -439,6 +439,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "chashmap" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff41a3c2c1e39921b9003de14bf0439c7b63a9039637c291e1a64925d8ddfa45" +dependencies = [ + "owning_ref", + "parking_lot 0.4.8", +] + +[[package]] name = "chrono" version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -547,7 +557,7 @@ dependencies = [ "base64", "hkdf", "percent-encoding", - "rand", + "rand 0.8.5", "sha2", "subtle", "time", @@ -633,7 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -775,7 +785,7 @@ dependencies = [ "lebe", "miniz_oxide", "rayon-core", - "smallvec", + "smallvec 1.11.2", "zune-inflate", ] @@ -858,6 +868,12 @@ dependencies = [ ] [[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] name = "futures" version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1312,7 +1328,7 @@ dependencies = [ "jellyclient", "jellycommon", "log", - "rand", + "rand 0.8.5", "redb", "serde", "serde_json", @@ -1416,6 +1432,7 @@ dependencies = [ "async-recursion", "base64", "bincode", + "chashmap", "chrono", "env_logger", "futures", @@ -1427,7 +1444,7 @@ dependencies = [ "jellytranscoder", "log", "markup", - "rand", + "rand 0.8.5", "rocket", "rocket_ws", "serde", @@ -1451,7 +1468,7 @@ dependencies = [ "jellyclient", "jellycommon", "log", - "rand", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -1658,6 +1675,12 @@ dependencies = [ ] [[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1936,13 +1959,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] +name = "owning_ref" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "parking_lot" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e" +dependencies = [ + "owning_ref", + "parking_lot_core 0.2.14", +] + +[[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db1a8ccf734a7bce794cc19b3df06ed87ab2f3907036b693c68f56b4d4537fa" +dependencies = [ + "libc", + "rand 0.4.6", + "smallvec 0.6.14", + "winapi", ] [[package]] @@ -1954,7 +2008,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec", + "smallvec 1.11.2", "windows-targets 0.48.5", ] @@ -1965,7 +2019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2113,13 +2167,26 @@ dependencies = [ [[package]] name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2129,11 +2196,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" @@ -2168,7 +2250,7 @@ dependencies = [ "num-traits", "once_cell", "paste", - "rand", + "rand 0.8.5", "rand_chacha", "rust_hawktracer", "rustc_version", @@ -2215,6 +2297,15 @@ dependencies = [ ] [[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] name = "redb" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2362,9 +2453,9 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", - "rand", + "rand 0.8.5", "ref-cast", "rocket_codegen", "rocket_http", @@ -2417,7 +2508,7 @@ dependencies = [ "pin-project-lite", "ref-cast", "serde", - "smallvec", + "smallvec 1.11.2", "stable-pattern", "state", "time", @@ -2681,6 +2772,15 @@ dependencies = [ [[package]] name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" @@ -2724,6 +2824,12 @@ dependencies = [ ] [[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] name = "state" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2924,7 +3030,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", @@ -3093,7 +3199,7 @@ dependencies = [ "once_cell", "regex", "sharded-slab", - "smallvec", + "smallvec 1.11.2", "thread_local", "tracing", "tracing-core", @@ -3118,7 +3224,7 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror", "url", diff --git a/server/Cargo.toml b/server/Cargo.toml index a124876..ca8315c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -21,6 +21,7 @@ rand = "0.8.5" base64 = "0.21.5" chrono = { version = "0.4.31", features = ["serde"] } vte = "0.13.0" +chashmap = "2.2.2" argon2 = "0.5.2" aes-gcm-siv = "0.11.1" diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index e4d05e5..a6a086f 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -10,8 +10,14 @@ use jellybase::{federation::Federation, CONF, SECRETS}; use log::warn; use rand::random; use rocket::{ - catchers, config::SecretKey, fairing::AdHoc, fs::FileServer, get, http::Header, routes, Build, - Config, Rocket, + catchers, + config::SecretKey, + fairing::AdHoc, + fs::FileServer, + get, + http::Header, + response::{self, Responder}, + routes, Build, Config, Request, Rocket, }; use std::fs::File; use stream::r_stream; @@ -37,11 +43,11 @@ use ui::{ }; use userdata::{r_node_userdata, r_player_progress, r_player_watched}; -use self::streamsync::r_streamsync; +use self::playersync::{r_streamsync, PlayersyncChannels}; pub mod api; pub mod stream; -pub mod streamsync; +pub mod playersync; pub mod ui; pub mod userdata; @@ -76,6 +82,7 @@ pub fn build_rocket(database: DataAcid, federation: Federation) -> Rocket<Build> }) .manage(database) .manage(federation) + .manage(PlayersyncChannels::default()) .attach(AdHoc::on_response("set server header", |_req, res| { res.set_header(Header::new("server", "jellything")); Box::pin(async {}) @@ -133,3 +140,12 @@ pub fn build_rocket(database: DataAcid, federation: Federation) -> Rocket<Build> fn r_favicon() -> MyResult<File> { Ok(File::open(CONF.asset_path.join("favicon.ico"))?) } + +pub struct Cors<T>(pub T); +impl<'r, T: Responder<'r, 'static>> Responder<'r, 'static> for Cors<T> { + fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { + let mut r = self.0.respond_to(request)?; + r.adjoin_header(Header::new("access-controll-allow-origin", "*")); + Ok(r) + } +} diff --git a/server/src/routes/playersync.rs b/server/src/routes/playersync.rs new file mode 100644 index 0000000..2a7d3f6 --- /dev/null +++ b/server/src/routes/playersync.rs @@ -0,0 +1,55 @@ +use super::Cors; +use chashmap::CHashMap; +use futures::{SinkExt, StreamExt}; +use log::warn; +use rocket::{get, State}; +use rocket_ws::{stream::DuplexStream, Channel, Message, WebSocket}; +use tokio::sync::broadcast::{self, Sender}; + +#[derive(Default)] +pub struct PlayersyncChannels { + channels: CHashMap<String, broadcast::Sender<Message>>, +} + +#[get("/playersync/<channel>")] +pub fn r_streamsync( + ws: WebSocket, + state: &State<PlayersyncChannels>, + channel: &str, +) -> Cors<Channel<'static>> { + let sender = state + .channels + .get(&channel.to_owned()) + .map(|x| x.to_owned()) + .unwrap_or_else(|| { + let ch = broadcast::channel(16).0; + state.channels.insert(channel.to_owned(), ch.clone()); + ch + }); + Cors(ws.channel(move |ws| { + Box::pin(async move { + if let Err(e) = handle_socket(sender, ws).await { + warn!("streamsync websocket error: {e:?}") + } + Ok(()) + }) + })) +} + +async fn handle_socket(broadcast: Sender<Message>, mut ws: DuplexStream) -> anyhow::Result<()> { + let mut sub = broadcast.subscribe(); + loop { + tokio::select! { + message = ws.next() => { + if let Some(message) = message { + broadcast.send(message?)?; + } else { + return Ok(()) + } + }, + message = sub.recv() => { + ws.send(message?).await?; + } + }; + } +} diff --git a/server/src/routes/streamsync.rs b/server/src/routes/streamsync.rs deleted file mode 100644 index d4a4d7e..0000000 --- a/server/src/routes/streamsync.rs +++ /dev/null @@ -1,11 +0,0 @@ -use rocket::get; -use rocket_ws::{Stream, WebSocket}; - -#[get("/streamsync")] -pub fn r_streamsync(ws: WebSocket) -> Stream!['static] { - Stream! { ws => - for await message in ws { - yield message?; - } - } -} diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index fdb4e4a..6aca36c 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -11,6 +11,7 @@ import { EncodingProfile } from "./jhls.d.ts"; import { TrackKind, get_track_kind } from "./mediacaps.ts"; import { Player } from "./player.ts"; import { Popup } from "./popup.ts"; +import { Playersync } from "./sync.ts" globalThis.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("player")) { @@ -35,10 +36,12 @@ function initialize_player(el: HTMLElement, node_id: string) { const logger = new Logger<string>(s => e("p", s)) const player = new Player(node_id, logger) const show_stats = new OVar(false); + const sync_state = new OVar<Playersync | undefined>(undefined) const toggle_playing = () => player.playing.value ? player.pause() : player.play() const pri_map = (v: number) => (v / player.duration.value * 100) + "%" + let pri_current: HTMLElement; let pri: HTMLElement; @@ -49,12 +52,14 @@ function initialize_player(el: HTMLElement, node_id: string) { const button = e("button", MEDIA_KIND_ICONS[kind][+enabled], { class: "icon", onclick: () => { + // sync_state.value = new Playersync(player, logger, "test") enabled = !enabled button.textContent = MEDIA_KIND_ICONS[kind][+enabled] } }) new Popup(button, popups, () => e("div", { class: "jsp-track-select-popup" }, + e("h3", `${kind[0].toUpperCase()}${kind.substring(1)}`), ...(player.tracks ?? []) .map((track, index) => ({ index, track })) .filter(({ track }) => get_track_kind(track.kind) == kind) @@ -74,6 +79,33 @@ function initialize_player(el: HTMLElement, node_id: string) { ) return button } + const settings_popup = () => { + const button = e("button", "settings", { class: "icon" }) + let channelname: HTMLInputElement; + new Popup(button, popups, () => e("div", { class: "jsp-settings-popup" }, + e("h2", "Settings"), + e("div", { class: "jsp-playersync-controls" }, + e("h3", "Playersync"), + sync_state.map(sync => sync + ? e("div", + e("span", "Sync enabled."), + e("button", "Disable", { + onclick: () => { sync_state.value?.destroy(); sync_state.value = undefined } + })) + : e("div", + channelname = e("input", { type: "text" }), + e("button", "Sync!", { + onclick: () => { + if (!channelname.value.length) return + sync_state.value?.destroy() + sync_state.value = new Playersync(player, logger, channelname.value) + } + })) + ) + ) + )) + return button; + } const controls = e("div", { class: "jsp-controls" }, player.playing.map(playing => @@ -105,6 +137,7 @@ function initialize_player(el: HTMLElement, node_id: string) { track_select("audio"), track_select("subtitles") ), + settings_popup(), e("button", "fullscreen", { class: "icon", onclick() { diff --git a/web/script/player/sync.ts b/web/script/player/sync.ts new file mode 100644 index 0000000..a2029ea --- /dev/null +++ b/web/script/player/sync.ts @@ -0,0 +1,92 @@ +import { Logger } from "../jshelper/src/log.ts"; +import { Player } from "./player.ts" + +function get_username() { + return document.querySelector("nav .account .username")?.textContent ?? "Unknown User" +} + +interface Packet { + time?: number, + playing?: boolean, + join?: string, + leave?: string, +} + +export class Playersync { + private ws: WebSocket + private on_destroy: (() => void)[] = [] + + private cancel_pers: undefined | (() => void) + set_pers(s?: string) { + if (this.cancel_pers) this.cancel_pers(), this.cancel_pers = undefined + if (s) this.cancel_pers = this.logger?.log_persistent(s) + } + + constructor(private player: Player, private logger: Logger<string>, private channel_name: string) { + this.set_pers("Playersync enabling...") + + let [localpart, remotepart] = channel_name.split(":") + if (!remotepart?.length) remotepart = window.location.host + + this.ws = new WebSocket(`${window.location.protocol.endsWith("s:") ? "wss" : "ws"}://${remotepart}/playersync/${encodeURIComponent(localpart)}`) + this.on_destroy.push(() => this.ws.close()) + + this.ws.onopen = () => { + this.set_pers() + this.logger.log(`Playersync connected.`) + this.send({ join: get_username() }) + } + this.ws.onerror = () => { + this.set_pers(`Playersync websocket error.`) + } + this.ws.onclose = () => { + this.set_pers(`Playersync websocket closed.`) + } + + let last_time = 0; + this.ws.onmessage = ev => { + const packet: Packet = JSON.parse(ev.data) + console.log("playersync recv", packet); + if (packet.time !== undefined) { + this.player.seek(packet.time) + last_time = packet.time + } + if (packet.playing === true) this.player.play() + if (packet.playing === false) this.player.pause() + if (packet.join) this.logger.log(`${packet.join} joined.`) + if (packet.leave) this.logger.log(`${packet.join} left.`) + } + + let cb: () => void + + player.video.addEventListener("play", cb = () => { + this.send({ playing: true }) + }) + this.on_destroy.push(() => player.video.removeEventListener("play", cb)) + + player.video.addEventListener("pause", cb = () => { + this.send({ playing: false }) + }) + this.on_destroy.push(() => player.video.removeEventListener("pause", cb)) + + player.video.addEventListener("seeking", cb = () => { + const time = this.player.video.currentTime + if (Math.abs(last_time - time) < 0.01) return + this.send({ time: this.player.video.currentTime }) + }) + this.on_destroy.push(() => player.video.removeEventListener("seeking", cb)) + } + + destroy() { + this.set_pers() + this.logger.log("Playersync disabled.") + this.on_destroy.forEach(f => f()) + this.send({ leave: get_username() }) + } + + send(p: Packet) { + console.log("playersync send", p); + this.ws.send(JSON.stringify(p)) + } +} + diff --git a/web/style/js-player.css b/web/style/js-player.css index 40455ed..8f047f1 100644 --- a/web/style/js-player.css +++ b/web/style/js-player.css @@ -52,10 +52,6 @@ .jsp-track-state:hover { background-color: rgba(113, 113, 113, 0.333); } -.jsp-track-select-popup { - background-color: #303a; - padding: 1em; -} .jsp-pri { position: relative; @@ -149,14 +145,16 @@ bottom: var(--csize); right: 0px; animation-name: popup-in; + animation-delay: 180ms; animation-duration: 100ms; - animation-fill-mode: forwards; + animation-fill-mode: both; animation-timing-function: ease-out; } .jsp-popup-out { animation-name: popup-out; + animation-delay: 0ms; animation-duration: 100ms; - animation-fill-mode: backwards; + animation-fill-mode: both; animation-timing-function: ease-in; } @keyframes popup-in { @@ -179,3 +177,31 @@ opacity: 0; } } + +.jsp-settings-popup { + padding: 1em; + min-width: 14em; + background-color: rgba(45, 24, 104, 0.548); +} +.jsp-track-select-popup { + min-width: 14em; + background-color: #303a; + padding: 1em; +} +.jsp-settings-popup h2, +.jsp-settings-popup h3 { + margin-top: 0.1em; + margin-bottom: 0.1em; +} + +.jsp-playersync-controls button { + background-color: black; + border: 2px solid var(--accent-light); + font-size: medium; + padding: 0.3em; + border-radius: 7px; +} +.jsp-playersync-controls { + padding: 1em; + background-color: #0005; +} |