diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..664db10 --- /dev/null +++ b/.ignore @@ -0,0 +1,2 @@ +*.png +*.svg diff --git a/index.html b/index.html index 0e56156..0d082be 100644 --- a/index.html +++ b/index.html @@ -5,9 +5,7 @@ - BiliBili ShadowReplay -
diff --git a/live_index.html b/live_index.html new file mode 100644 index 0000000..b9f9db2 --- /dev/null +++ b/live_index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + +
+
+ +
+
+ + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6ccf9ad..c19cd87 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -217,7 +217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" dependencies = [ "atk-sys", - "bitflags", + "bitflags 1.3.2", "glib", "libc", ] @@ -271,9 +271,11 @@ dependencies = [ "async-std", "chrono", "custom_error", + "dashmap", "felgens", "ffmpeg-sidecar", "futures", + "hyper", "m3u8-rs", "md5", "notify-rust", @@ -297,6 +299,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "block" version = "0.1.6" @@ -411,7 +419,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "glib", "libc", @@ -508,7 +516,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", "cocoa-foundation", "core-foundation", @@ -524,7 +532,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", "core-foundation", "core-graphics-types", @@ -596,7 +604,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types", @@ -609,7 +617,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "foreign-types", "libc", @@ -794,6 +802,20 @@ dependencies = [ "syn 2.0.76", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -1051,7 +1073,7 @@ checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "windows-sys 0.45.0", ] @@ -1072,7 +1094,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1234,7 +1256,7 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk-sys", @@ -1250,7 +1272,7 @@ version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -1365,7 +1387,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -1395,7 +1417,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", @@ -1483,7 +1505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" dependencies = [ "atk", - "bitflags", + "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1851,7 +1873,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "glib", "javascriptcore-rs-sys", ] @@ -2031,9 +2053,9 @@ checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2218,7 +2240,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jni-sys", "ndk-sys", "num_enum", @@ -2252,7 +2274,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.7.1", @@ -2406,9 +2428,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" @@ -2426,7 +2448,7 @@ version = "0.10.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b277f87dacc05a6b709965d1cbafac4649d6ce9f3ce9ceb88508b5666dfec9" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -2487,7 +2509,7 @@ version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", "glib", "libc", "once_cell", @@ -2524,15 +2546,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.3", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.52.6", ] [[package]] @@ -2737,7 +2759,7 @@ version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "flate2", "miniz_oxide", @@ -2750,7 +2772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa" dependencies = [ "autocfg", - "bitflags", + "bitflags 1.3.2", "cfg-if", "concurrent-queue", "libc", @@ -2946,7 +2968,16 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", ] [[package]] @@ -2956,7 +2987,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom 0.2.8", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] @@ -3064,7 +3095,7 @@ version = "0.36.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fe885c3a125aa45213b68cc1472a49880cb5923dc23f522ad2791b882228778" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes", "libc", @@ -3078,7 +3109,7 @@ version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b24138615de35e32031d041a09032ef3487a616d901ca4db224e7d557efae2" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno 0.3.0", "io-lifetimes", "libc", @@ -3163,7 +3194,7 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -3186,7 +3217,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", @@ -3423,7 +3454,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gio", "glib", "libc", @@ -3437,7 +3468,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gio-sys", "glib-sys", "gobject-sys", @@ -3603,7 +3634,7 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "575c856fc21e551074869dcfaad8f706412bd5b803dfa0fbf6881c4ff4bfafab" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "cc", "cocoa", @@ -3880,7 +3911,7 @@ checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.2.16", "rustix 0.36.10", "windows-sys 0.42.0", ] @@ -4482,7 +4513,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk", "gdk-sys", @@ -4507,7 +4538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" dependencies = [ "atk-sys", - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d1b94b5..daccbef 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,6 +33,8 @@ platform-dirs = "0.3.0" pct-str = "1.2.0" md5 = "0.7.0" notify-rust = "4.8.0" +hyper = { version = "0.14", features = ["full"] } +dashmap = "6.1.0" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7302df0..2abaee2 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,16 +2,17 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod recorder; +mod recorder_manager; use custom_error::custom_error; +use futures::executor::block_on; use recorder::bilibili::errors::BiliClientError; use recorder::bilibili::{BiliClient, QrInfo, QrStatus}; -use recorder::BiliRecorder; -use std::collections::HashMap; +use recorder_manager::{ RecorderManager, Summary }; use std::process::Command; use std::sync::Arc; -use tauri::{CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem}; +use tauri::{CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, Theme}; use tauri::{Manager, WindowEvent}; use tokio::sync::{Mutex, RwLock}; @@ -74,7 +75,6 @@ fn show_in_folder(path: String) { pub struct Config { rooms: Vec, admin_uid: Vec, - max_len: u64, cache: String, output: String, login: bool, @@ -94,7 +94,6 @@ impl Config { let config = Config { rooms: Vec::new(), admin_uid: Vec::new(), - max_len: 300, cache: app_dirs .cache_dir .join("cache") @@ -158,14 +157,6 @@ impl Config { self.save(); } - pub fn set_max_len(&mut self, mut len: u64) { - if len < 30 { - len = 30; - } - self.max_len = len; - self.save(); - } - pub fn set_cookies(&mut self, cookies: &str) { self.cookies = cookies.to_string(); // match(/DedeUserID=(\d+)/)[1 @@ -212,58 +203,13 @@ fn copy_dir_all( struct State { client: Arc>, config: Arc>, - recorders: Arc>>>>, -} - -#[derive(serde::Deserialize, serde::Serialize, Clone)] -struct Summary { - pub count: usize, - pub rooms: Vec, -} - -#[derive(serde::Deserialize, serde::Serialize, Clone)] -struct RoomInfo { - pub room_id: u64, - pub room_title: String, - pub room_cover: String, - pub room_keyframe: String, - pub user_id: u64, - pub user_name: String, - pub user_sign: String, - pub user_avatar: String, - pub total_length: f64, - pub max_len: u64, - pub live_status: bool, + recorder_manager: Arc, + app_handle: tauri::AppHandle, } impl State { pub async fn get_summary(&self) -> Summary { - let recorders = self.recorders.lock().await; - let mut summary = Summary { - count: recorders.len(), - rooms: Vec::new(), - }; - - for (_, recorder) in recorders.iter() { - let recorder = recorder.read().await; - let room_info = RoomInfo { - room_id: recorder.room_id, - room_title: recorder.room_title.clone(), - room_cover: recorder.room_cover.clone(), - room_keyframe: recorder.room_keyframe.clone(), - user_id: recorder.user_id, - user_name: recorder.user_name.clone(), - user_sign: recorder.user_sign.clone(), - user_avatar: recorder.user_avatar.clone(), - total_length: *recorder.ts_length.read().await, - max_len: self.config.read().await.max_len, - live_status: *recorder.live_status.read().await, - }; - summary.rooms.push(room_info); - } - - summary.rooms.sort_by(|a, b| a.room_id.cmp(&b.room_id)); - summary + self.recorder_manager.get_summary().await } pub async fn get_qr(client: Arc>) -> Result { @@ -274,37 +220,16 @@ impl State { self.client.lock().await.get_qr_status(key).await } - pub async fn add_recorder(&self, room_id: u64) -> Result<(), StateError> { - if self.recorders.lock().await.get(&room_id).is_some() { - return Err(StateError::RecorderAlreadyExists); - } - match BiliRecorder::new(room_id, self.config.clone()).await { - Ok(recorder) => { - recorder.run().await; - let recorder = Arc::new(RwLock::new(recorder)); - self.recorders.lock().await.insert(room_id, recorder); - self.config.write().await.add(room_id); - Ok(()) - } - Err(e) => { - println!("create recorder failed: {:?}", e); - Err(StateError::RecorderCreateError) - } - } + pub async fn add_recorder(&self, room_id: u64) -> Result<(), String> { + self.recorder_manager.add_recorder(room_id).await } pub async fn remove_recorder(&self, room_id: u64) { - let mut recorders = self.recorders.lock().await; - let recorder = recorders.get_mut(&room_id).unwrap(); - recorder.read().await.stop().await; - recorders.remove(&room_id); - self.config.write().await.remove(room_id); + let _ = self.recorder_manager.remove_recorder(room_id).await; } pub async fn clip(&self, room_id: u64, len: f64) -> Result { - let recorders = self.recorders.lock().await; - let recorder = recorders.get(&room_id).unwrap().clone(); - if let Ok(file) = recorder.clone().read().await.clip(room_id, len).await { + if let Ok(file) = self.recorder_manager.clip(room_id, len).await { Ok(file) } else { Err("Clip error".to_string()) @@ -358,13 +283,6 @@ async fn get_config(state: tauri::State<'_, State>) -> Result { Ok(state.config.read().await.clone()) } -#[tauri::command] -async fn set_max_len(state: tauri::State<'_, State>, len: u64) -> Result<(), ()> { - let mut config = state.config.write().await; - config.set_max_len(len); - Ok(()) -} - #[tauri::command] async fn set_cache_path(state: tauri::State<'_, State>, cache_path: String) -> Result<(), ()> { let mut config = state.config.write().await; @@ -400,31 +318,13 @@ async fn clip(state: tauri::State<'_, State>, room_id: u64, len: f64) -> Result< state.clip(room_id, len).await } -#[tauri::command] -async fn init_recorders(state: tauri::State<'_, State>) -> Result<(), ()> { - println!("[invoke]init recorders"); - let cookies = state.config.read().await.cookies.clone(); - let rooms = state.config.read().await.rooms.clone(); - let mut client = state.client.lock().await; - client.set_cookies(&cookies); - for room_id in rooms.iter() { - if let Err(e) = state.add_recorder(*room_id).await { - println!("init recorder failed: {:?}", e); - } - } - Ok(()) -} - #[tauri::command] async fn set_cookies(state: tauri::State<'_, State>, cookies: String) -> Result<(), String> { let mut client = state.client.lock().await; let mut config = state.config.write().await; config.set_cookies(&cookies); client.set_cookies(&cookies); - let recorders = state.recorders.lock().await; - for (_, recorder) in recorders.iter() { - recorder.write().await.update_cookies(&cookies).await; - } + state.recorder_manager.update_cookies(&cookies).await; Ok(()) } @@ -437,6 +337,24 @@ async fn logout(state: tauri::State<'_, State>) -> Result<(), String> { Ok(()) } +#[tauri::command] +async fn open_live(state: tauri::State<'_, State>, room_id: u64) -> Result<(), String> { + let addr = state.recorder_manager.get_hls_server_addr().await.unwrap(); + let window = tauri::WindowBuilder::new(&state.app_handle.clone(), room_id.to_string(), tauri::WindowUrl::App(format!("live_index.html?port={}&room_id={}", addr.port(), room_id).into())) + .title(format!("Live {}", room_id)) + .theme(Some(Theme::Dark)) + .build() + .unwrap(); + let window_clone = window.clone(); + window_clone.on_window_event(move |event| { + if let tauri::WindowEvent::CloseRequested { .. } = event { + // close window + window.close().unwrap(); + } + }); + Ok(()) +} + fn main() -> Result<(), Box> { // Setup ffmpeg ffmpeg_sidecar::download::auto_download().unwrap(); @@ -449,16 +367,35 @@ fn main() -> Result<(), Box> { .add_item(quit); let tray = SystemTray::new().with_menu(tray_menu); - // Setup initial state - let state = State { - client: Arc::new(Mutex::new(BiliClient::new()?)), - config: Arc::new(RwLock::new(Config::load())), - recorders: Arc::new(Mutex::new(HashMap::new())), - }; + let config = Arc::new(RwLock::new(Config::load())); + let recorder_manager = Arc::new(RecorderManager::new(config.clone())); + + // Start recorder manager in tokio runtime + // create a new tokio runtime + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(4) + .enable_all() + .build() + .unwrap(); + let recorder_manager_clone = recorder_manager.clone(); + rt.block_on(async move { + recorder_manager_clone.init().await; + recorder_manager_clone.run().await; + }); // Tauri part tauri::Builder::default() - .manage(state) + .setup(move |app| { + let client = Arc::new(Mutex::new(BiliClient::new().unwrap())); + let state = State { + client, + config: config.clone(), + recorder_manager: recorder_manager.clone(), + app_handle: app.handle().clone(), + }; + app.manage(state); + Ok(()) + }) .system_tray(tray) .on_window_event(|event| { if let WindowEvent::CloseRequested { api, .. } = event.event() { @@ -467,12 +404,10 @@ fn main() -> Result<(), Box> { } }) .invoke_handler(tauri::generate_handler![ - init_recorders, get_summary, add_recorder, remove_recorder, get_config, - set_max_len, set_cache_path, set_output_path, set_admins, @@ -482,6 +417,7 @@ fn main() -> Result<(), Box> { get_qr_status, set_cookies, logout, + open_live, ]) .on_system_tray_event(|app, event| match event { SystemTrayEvent::LeftClick { diff --git a/src-tauri/src/recorder.rs b/src-tauri/src/recorder.rs index 8b190b4..e62d7ce 100644 --- a/src-tauri/src/recorder.rs +++ b/src-tauri/src/recorder.rs @@ -12,7 +12,6 @@ use notify_rust::Notification; use regex::Regex; use std::sync::Arc; use std::thread; - use felgens::{ws_socket_object, FelgensError, WsStreamMessageType}; use tokio::sync::mpsc::{self, UnboundedReceiver}; use tokio::sync::{Mutex, RwLock}; @@ -70,7 +69,8 @@ impl BiliRecorder { stream_type = stream_type_now; } } - Ok(Self { + + let recorder = Self { client: Arc::new(RwLock::new(client)), config, room_id, @@ -89,10 +89,12 @@ impl BiliRecorder { quit: Arc::new(Mutex::new(false)), header: Arc::new(RwLock::new(None)), stream_type: Arc::new(RwLock::new(stream_type)), - }) + }; + println!("Recorder for room {} created.", room_id); + Ok(recorder) } - pub async fn update_cookies(&mut self, cookies: &str) { + pub async fn update_cookies(&self, cookies: &str) { self.client.write().await.set_cookies(cookies); } @@ -139,6 +141,7 @@ impl BiliRecorder { println!("update entries error: {}", e); break; } + thread::sleep(std::time::Duration::from_secs(1)); } } // Every 10s check live status. @@ -245,63 +248,6 @@ impl BiliRecorder { Ok(header_url) } - // { - // "format_name": "ts", - // "codec": [ - // { - // "codec_name": "avc", - // "current_qn": 10000, - // "accept_qn": [ - // 10000, - // 400, - // 250, - // 150 - // ], - // "base_url": "/live-bvc/738905/live_51628309_47731828_bluray.m3u8?", - // "url_info": [ - // { - // "host": "https://cn-jsyz-ct-03-51.bilivideo.com", - // "extra": "expires=1680532720&len=0&oi=3664564898&pt=h5&qn=10000&trid=100352dbcd4ec5494d6083d4a9a3d9f91aa7&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha01&sign=829e59d93ef9ffff8e2aa3bb090f1280&sk=4207df3de646838b084f14f252be3aff94df00e145e0110c92421700c186a851&p2p_type=0&sl=6&free_type=0&mid=475210&sid=cn-jsyz-ct-03-51&chash=1&sche=ban&score=13&pp=rtmp&source=onetier&trace=a0c&site=c66c7195b197c2cf30e5715dbf2922b8&order=1", - // "stream_ttl": 3600 - // } - // ], - // "hdr_qn": null, - // "dolby_type": 0, - // "attr_name": "" - // } - // ] - // } - // { - // "format_name": "fmp4", - // "codec": [ - // { - // "codec_name": "avc", - // "current_qn": 10000, - // "accept_qn": [ - // 10000, - // 400, - // 250, - // 150 - // ], - // "base_url": "/live-bvc/738905/live_51628309_47731828_bluray/index.m3u8?", - // "url_info": [ - // { - // "host": "https://cn-jsyz-ct-03-51.bilivideo.com", - // "extra": "expires=1680532720&len=0&oi=3664564898&pt=h5&qn=10000&trid=100752dbcd4ec5494d6083d4a9a3d9f91aa7&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha01&sign=3d0930160c5870021ebbb457e4630fcf&sk=5bf07b9bbe6df2e0a6bc476fe3d9a642c8e387f5b7e5df7fa9e1b9d0abc8bd13&flvsk=4207df3de646838b084f14f252be3aff94df00e145e0110c92421700c186a851&p2p_type=0&sl=6&free_type=0&mid=475210&sid=cn-jsyz-ct-03-51&chash=1&sche=ban&bvchls=1&score=13&pp=rtmp&source=onetier&trace=a0c&site=c66c7195b197c2cf30e5715dbf2922b8&order=1", - // "stream_ttl": 3600 - // }, - // { - // "host": "https://d1--cn-gotcha208.bilivideo.com", - // "extra": "expires=1680532720&len=0&oi=3664564898&pt=h5&qn=10000&trid=100752dbcd4ec5494d6083d4a9a3d9f91aa7&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha208&sign=b63815ac70b18420c64a661465f92962&sk=5bf07b9bbe6df2e0a6bc476fe3d9a642c8e387f5b7e5df7fa9e1b9d0abc8bd13&p2p_type=0&sl=6&free_type=0&mid=475210&pp=rtmp&source=onetier&trace=4&site=c66c7195b197c2cf30e5715dbf2922b8&order=2", - // "stream_ttl": 3600 - // } - // ], - // "hdr_qn": null, - // "dolby_type": 0, - // "attr_name": "" - // } - // ] - // } async fn ts_url(&self, ts_url: &String) -> Result { // Construct url for ts and fmp4 stream. match *self.stream_type.read().await { @@ -399,20 +345,6 @@ impl BiliRecorder { *self.latest_sequence.lock().await = sequence; let mut total_length = self.ts_length.write().await; *total_length += ts.duration as f64; - while *total_length > self.config.read().await.max_len as f64 { - *total_length -= entries[0].length; - if let Err(e) = std::fs::remove_file( - BiliClient::url_to_file_name( - &self.config.read().await.cache, - room_id, - &entries[0].url, - ) - .1, - ) { - println!("remove file failed: {}", e); - } - entries.remove(0); - } sequence += 1; } join_all(handles).await.into_iter().for_each(|e| { @@ -482,4 +414,30 @@ impl BiliRecorder { }); Ok(file_name) } + + pub async fn generate_m3u8(&self) -> String { + let mut m3u8_content = "#EXTM3U\n".to_string(); + m3u8_content += "#EXT-X-VERSION:6\n"; + m3u8_content += "#EXT-X-TARGETDURATION:1\n"; + m3u8_content += "#EXT-X-PLAYLIST-TYPE:EVENT\n"; // 修改为 EVENT 模式以支持 DVR + + // initial segment for fmp4, info from self.header + if let Some(header) = self.header.read().await.as_ref() { + let file_name = header.url.split('/').last().unwrap(); + let local_url = format!("/{}/{}", self.room_id, file_name); + m3u8_content += &format!("#EXT-X-MAP:URI=\"{}\"\n", local_url); + } + let entries = self.ts_entries.lock().await.clone(); + for entry in entries.iter() { + m3u8_content += &format!("#EXTINF:{:.3},\n", entry.length); + let file_name = entry.url.split('/').last().unwrap(); + let local_url = format!("/{}/{}", self.room_id, file_name); + m3u8_content += &format!("{}\n", local_url); + } + m3u8_content + } + + pub async fn get_ts_file_path(&self, ts_file: &str) -> String { + format!("{}/{}/{}", self.config.read().await.cache, self.room_id, ts_file) + } } diff --git a/src-tauri/src/recorder_manager.rs b/src-tauri/src/recorder_manager.rs new file mode 100644 index 0000000..e97da15 --- /dev/null +++ b/src-tauri/src/recorder_manager.rs @@ -0,0 +1,232 @@ +use crate::recorder::BiliRecorder; +use crate::Config; +use dashmap::DashMap; +use hyper::{ + service::{make_service_fn, service_fn}, + Body, Request, Response, Server, +}; +use std::net::SocketAddr; +use std::{convert::Infallible, sync::Arc}; +use tokio::{ + net::TcpListener, + sync::RwLock, +}; + +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] +pub struct Summary { + pub count: usize, + pub rooms: Vec, +} + +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] +pub struct RoomInfo { + pub room_id: u64, + pub room_title: String, + pub room_cover: String, + pub room_keyframe: String, + pub user_id: u64, + pub user_name: String, + pub user_sign: String, + pub user_avatar: String, + pub total_length: f64, + pub live_status: bool, +} + +pub struct RecorderManager { + config: Arc>, + recorders: Arc>, + hls_server_addr: Arc>>, +} + +impl RecorderManager { + pub fn new(config: Arc>) -> RecorderManager { + RecorderManager { + config, + recorders: Arc::new(DashMap::new()), + hls_server_addr: Arc::new(RwLock::new(None)), + } + } + + pub async fn init(&self) { + let config = self.config.read().await.clone(); + for room_id in config.rooms.iter() { + let recorder = BiliRecorder::new(*room_id, self.config.clone()).await.unwrap(); + // run recorder + recorder.run().await; + self.recorders.insert(*room_id, recorder); + } + println!("RecorderManager initialized"); + } + + pub async fn run(&self) { + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = TcpListener::bind(&addr).await.unwrap(); + + let server_addr = self.start_hls_server(listener).await.unwrap(); + println!("HLS server started on http://{}", server_addr); + self.hls_server_addr.write().await.replace(server_addr); + } + + pub async fn add_recorder(&self, room_id: u64) -> Result<(), String> { + // check existing recorder + if self.recorders.contains_key(&room_id) { + return Err(format!("Recorder {} already exists", room_id)); + } + let recorder = BiliRecorder::new(room_id, self.config.clone()).await.unwrap(); + self.recorders.insert(room_id, recorder); + // update config + { + let mut config = self.config.write().await; + config.rooms.push(room_id); + config.save(); + } + // run recorder + let recorder = self.recorders.get(&room_id).unwrap(); + recorder.value().run().await; + Ok(()) + } + + pub async fn remove_recorder(&self, room_id: u64) -> Result<(), String> { + let recorder = self.recorders.remove(&room_id); + if recorder.is_none() { + return Err(format!("Recorder {} not found", room_id)); + } + Ok(()) + } + + pub async fn clip(&self, room_id: u64, d: f64) -> Result { + let recorder = self.recorders.get(&room_id); + if recorder.is_none() { + return Err(format!("Recorder {} not found", room_id)); + } + let recorder = recorder.unwrap(); + match recorder.value().clip(room_id, d).await { + Ok(f) => Ok(f), + Err(e) => Err(e.to_string()), + } + } + + pub async fn get_summary(&self) -> Summary { + let mut summary = Summary { + count: self.recorders.len(), + rooms: Vec::new(), + }; + + for recorder in self.recorders.iter() { + let recorder = recorder.value(); + let room_info = RoomInfo { + room_id: recorder.room_id, + room_title: recorder.room_title.clone(), + room_cover: recorder.room_cover.clone(), + room_keyframe: recorder.room_keyframe.clone(), + user_id: recorder.user_id, + user_name: recorder.user_name.clone(), + user_sign: recorder.user_sign.clone(), + user_avatar: recorder.user_avatar.clone(), + total_length: *recorder.ts_length.read().await, + live_status: *recorder.live_status.read().await, + }; + summary.rooms.push(room_info); + } + + summary.rooms.sort_by(|a, b| a.room_id.cmp(&b.room_id)); + + summary + } + + pub async fn update_cookies(&self, cookies: &str) { + // update cookies for all recorders + for recorder in self.recorders.iter() { + recorder.value().update_cookies(cookies).await; + } + } + + async fn start_hls_server(&self, listener: TcpListener) -> Result { + let recorders = self.recorders.clone(); + let make_svc = make_service_fn(move |_conn| { + let recorders = recorders.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |req: Request| { + let recorders = recorders.clone(); + async move { + let path = req.uri().path(); + let path_segs: Vec<&str> = path.split('/').collect(); + // path_segs should at lease with size 3: /21484828/playlist.m3u8 + if path_segs.len() != 3 { + return Ok::<_, Infallible>(Response::builder() + .status(404) + .body(Body::from("Path Not Found")) + .unwrap()); + } + // parse room id + let room_id = path_segs[1].parse::().unwrap(); + // if path is /room_id/playlist.m3u8 + if path_segs[2] == "playlist.m3u8" { + // get recorder + let recorder = recorders.get(&room_id); + if recorder.is_none() { + return Ok::<_, Infallible>(Response::builder() + .status(404) + .body(Body::from("Recorder Not Found")) + .unwrap()); + } + let recorder = recorder.unwrap(); + // response with recorder generated m3u8, which contains ts entries that cached in local + let m3u8_content = recorder.value().generate_m3u8().await; + Ok::<_, Infallible>(Response::builder() + .status(200) + .header("Content-Type", "application/vnd.apple.mpegurl") + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, OPTIONS") + .body(Body::from(m3u8_content)) + .unwrap()) + } else { + // try to find requested ts file in recorder's cache + // cache files are stored in {cache_dir}/{room_id}/{ts_file} + let ts_file = path_segs[2]; + let recorder = recorders.get(&room_id); + if recorder.is_none() { + return Ok::<_, Infallible>(Response::builder() + .status(404) + .body(Body::from("Recorder Not Found")) + .unwrap()); + } + let recorder = recorder.unwrap(); + let ts_file_path = recorder.value().get_ts_file_path(ts_file).await; + let ts_file_content = tokio::fs::read(ts_file_path).await; + if ts_file_content.is_err() { + return Ok::<_, Infallible>(Response::builder() + .status(404) + .body(Body::from("TS File Not Found")) + .unwrap()); + } + let ts_file_content = ts_file_content.unwrap(); + Ok::<_, Infallible>(Response::builder() + .status(200) + .header("Content-Type", "video/MP2T") + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "GET, OPTIONS") + .body(Body::from(ts_file_content)) + .unwrap()) + } + } + })) + } + }); + + let server = Server::from_tcp(listener.into_std().unwrap())?.serve(make_svc); + let addr = server.local_addr(); + tokio::spawn(async move { + if let Err(e) = server.await { + eprintln!("HLS server error: {}", e); + } + }); + + println!("HLS server running on http://{}", addr); + Ok(addr) + } + + pub async fn get_hls_server_addr(&self) -> Option { + *self.hls_server_addr.read().await + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index fd14e7e..5a79810 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -57,7 +57,7 @@ "icons/icon.icns", "icons/icon.ico" ], - "identifier": "cn.vjoi.bilishadowplay", + "identifier": "cn.vjoi.bilishadowreplay", "targets": "all" }, "security": { @@ -68,12 +68,13 @@ }, "windows": [ { + "label": "main", "fullscreen": false, "resizable": true, "title": "BiliBili ShadowReplay", "width": 800, "height": 600, - "theme": "Light" + "theme": "Dark" } ] } diff --git a/src/lib/RoomList.svelte b/src/lib/RoomList.svelte index f95bda1..3ec29c9 100644 --- a/src/lib/RoomList.svelte +++ b/src/lib/RoomList.svelte @@ -18,13 +18,11 @@ user_avatar: string; live_status: boolean; total_length: number; - max_len: number; }[]; } let summary: Summary; async function setup() { console.log("setup"); - await invoke("init_recorders"); await update_summary(); await get_config(); setInterval(async () => { @@ -46,6 +44,9 @@ } async function getImage(url) { + if (!url) { + return ""; + } const response = await fetch(url, { method: "GET", timeout: 30, @@ -220,7 +221,7 @@
{Number(room.total_length).toFixed(1)}s
{/if}
-