WIP: DVR implement

This commit is contained in:
Xinrea
2024-09-09 02:46:42 +08:00
parent a1c6caece1
commit 5f547e593a
13 changed files with 832 additions and 256 deletions

2
.ignore Normal file
View File

@@ -0,0 +1,2 @@
*.png
*.svg

View File

@@ -5,9 +5,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BiliBili ShadowReplay</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>

33
live_index.html Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.10.0/controls.css"/>
<link rel="stylesheet" href="src/youtube-theme.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.10.0/shaka-player.compiled.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.10.0/shaka-player.ui.js"></script>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
video {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<section id="wrap">
<div class="youtube-theme" data-shaka-player-container style="width: 100vw; height: 100vh;">
<video autoplay data-shaka-player id="video"></video>
</div>
</section>
<script type="module" src="src/live_main.ts"></script>
</body>
</html>

111
src-tauri/Cargo.lock generated
View File

@@ -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",

View File

@@ -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

View File

@@ -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<u64>,
admin_uid: Vec<u64>,
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<Mutex<BiliClient>>,
config: Arc<RwLock<Config>>,
recorders: Arc<Mutex<HashMap<u64, Arc<RwLock<BiliRecorder>>>>>,
}
#[derive(serde::Deserialize, serde::Serialize, Clone)]
struct Summary {
pub count: usize,
pub rooms: Vec<RoomInfo>,
}
#[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<RecorderManager>,
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<Mutex<BiliClient>>) -> Result<QrInfo, BiliClientError> {
@@ -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<String, String> {
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<Config, ()> {
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<dyn std::error::Error>> {
// Setup ffmpeg
ffmpeg_sidecar::download::auto_download().unwrap();
@@ -449,16 +367,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.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<dyn std::error::Error>> {
}
})
.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<dyn std::error::Error>> {
get_qr_status,
set_cookies,
logout,
open_live,
])
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick {

View File

@@ -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<String, BiliClientError> {
// 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)
}
}

View File

@@ -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<RoomInfo>,
}
#[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<RwLock<Config>>,
recorders: Arc<DashMap<u64, BiliRecorder>>,
hls_server_addr: Arc<RwLock<Option<SocketAddr>>>,
}
impl RecorderManager {
pub fn new(config: Arc<RwLock<Config>>) -> 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<String, String> {
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<SocketAddr, hyper::Error> {
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<Body>| {
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::<u64>().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<SocketAddr> {
*self.hls_server_addr.read().await
}
}

View File

@@ -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"
}
]
}

View File

@@ -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<Uint8Array>(url, {
method: "GET",
timeout: 30,
@@ -220,7 +221,7 @@
<td class="text-center"
><div
class="radial-progress bg-primary text-primary-content border-4 border-primary"
style="--value:{(room.total_length * 100) / room.max_len};"
style="--value:{100};"
>
{Number(room.total_length).toFixed(1)}s
</div></td
@@ -235,7 +236,7 @@
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div tabindex="0" class="btn m-1 btn-square btn-sm">
<svg
class="stroke-info"
class="stroke-info w-full h-full"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@@ -263,11 +264,19 @@
tabindex="0"
class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
>
<li>
<a
href={"#"}
on:click={async (_) => {
await invoke("open_live", { roomId: room.room_id });
}}>查看直播流</a
>
</li>
<li>
<a
href={"#"}
on:click={(_) => {
clip_model.max_len = room.max_len;
clip_model.max_len = room.total_length;
clip_model.room = room.room_id;
clip_model.title = room.room_title;
clip_model.video = false;
@@ -541,16 +550,6 @@
</div>
{/if}
<div class="divider"></div>
<label class="flex items-center my-2" for=""
>缓存时长:<input
type="number"
class="input input-sm input-bordered"
bind:value={setting_model.cach_len}
on:change={() => {
setting_model.changed = true;
}}
/></label
>
<label class="flex items-center my-2" for=""
>缓存目录:
<button

98
src/live_main.ts Normal file
View File

@@ -0,0 +1,98 @@
import { invoke } from "@tauri-apps/api/tauri";
const urlParams = new URLSearchParams(window.location.search);
const port = urlParams.get("port");
const room_id = urlParams.get("room_id");
let x_offset = 0;
let y_offset = 0;
async function init() {
const video = document.getElementById("video");
const ui = video["ui"];
const controls = ui.getControls();
const player = controls.getPlayer();
const config = {
seekBarColors: {
base: "rgba(255,255,255,.2)",
buffered: "rgba(255,255,255,.4)",
played: "rgb(255,0,0)",
},
};
ui.configure(config);
console.log(player);
// Attach player and UI to the window to make it easy to access in the JS console.
window.player = player;
window.ui = ui;
try {
await player.load(`http://127.0.0.1:${port}/${room_id}/playlist.m3u8`);
// This runs if the asynchronous load is successful.
console.log("The video has now been loaded!");
} catch (error) {
console.error("Error code", error.code, "object", error);
}
document.getElementsByClassName("shaka-overflow-menu-button")[0].remove();
document.querySelector(
".shaka-back-to-overflow-button .material-icons-round"
).innerHTML = "arrow_back_ios_new";
// add self-defined element in shaka-bottom-controls.shaka-no-propagation (second seekbar)
const shakaBottomControls = document.querySelector(
".shaka-bottom-controls.shaka-no-propagation"
);
const selfSeekbar = document.createElement("div");
selfSeekbar.className = "shaka-seek-bar shaka-no-propagation";
selfSeekbar.innerHTML = `
<div class="shaka-seek-bar-container self-defined" style="background-color: gray; margin: 4px 10px 4px 10px;">
<div class="shaka-seek-bar shaka-no-propagation">
<div class="shaka-seek-bar-buffered" style="width: 0%;"></div>
<div class="shaka-seek-bar-played" style="width: 0%;"></div>
<div class="shaka-seek-bar-hover" style="transform: translateX(0px);"></div>
<div class="shaka-seek-bar-hit-target"></div>
</div>
</div>
`;
shakaBottomControls.appendChild(selfSeekbar);
// add keydown event listener for '[' and ']' to control range
document.addEventListener("keydown", (e) => {
if (e.key === "[") {
// get current player time
const start = player.getPresentationStartTimeAsDate();
x_offset = (player.getPlayheadTimeAsDate() - start) / 1000;
if (y_offset < x_offset) {
y_offset = (Date.now() - start) / 1000;
}
} else if (e.key === "]") {
const start = player.getPresentationStartTimeAsDate();
y_offset = (player.getPlayheadTimeAsDate() - start) / 1000;
if (x_offset > y_offset) {
x_offset = 0;
}
}
// if enter key is pressed, send x_offset and y_offset to tauri
if (e.key === "Enter" && y_offset > 0) {
invoke("clip_range", { x: x_offset, y: y_offset });
}
console.log(`x_offset: ${x_offset}, y_offset: ${y_offset}`);
});
setInterval(() => {
const start = player.getPresentationStartTimeAsDate();
const total = (Date.now() - start) / 1000;
const first_point = x_offset / total;
const second_point = y_offset / total;
// set background color for self-defined seekbar between first_point and second_point using linear-gradient
// example: linear-gradient(to right, rgb(255, 0, 0) 28.495542%, rgb(255, 0, 0) 28.495542%, rgb(255, 0, 0) 28.63019%, rgba(255, 255, 255, 0.4) 28.63019%, rgba(255, 255, 255, 0.4) 29.285618%, rgba(255, 255, 255, 0.2) 29.285618%)
const seekbarContainer = selfSeekbar.querySelector(
".shaka-seek-bar-container.self-defined"
);
seekbarContainer.style.background = `linear-gradient(to right, rgba(255, 255, 255, 0.4) ${
first_point * 100
}%, rgb(0, 255, 0) ${first_point * 100}%, rgb(0, 255, 0) ${
second_point * 100
}%, rgba(255, 255, 255, 0.4) ${
second_point * 100
}%, rgba(255, 255, 255, 0.4) ${
first_point * 100
}%, rgba(255, 255, 255, 0.2) ${first_point * 100}%)`;
}, 500);
}
// receive tauri emit
document.addEventListener("shaka-ui-loaded", init);

286
src/youtube-theme.css Normal file
View File

@@ -0,0 +1,286 @@
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Me5Q.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmEU9vAw.ttf) format('truetype');
}
.youtube-theme {
font-family: 'Roboto', sans-serif;
}
.youtube-theme .shaka-bottom-controls {
width: 100%;
padding: 0;
padding-bottom: 0;
z-index: 1;
}
.youtube-theme .shaka-bottom-controls {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.youtube-theme .shaka-ad-controls {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
.youtube-theme .shaka-controls-button-panel {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2;
height: 40px;
padding: 0 10px;
}
.youtube-theme .shaka-range-container {
margin: 4px 10px 4px 10px;
top: 0;
}
.youtube-theme .shaka-small-play-button {
-webkit-box-ordinal-group: -2;
-ms-flex-order: -3;
order: -3;
}
.youtube-theme .shaka-mute-button {
-webkit-box-ordinal-group: -1;
-ms-flex-order: -2;
order: -2;
}
.youtube-theme .shaka-controls-button-panel > * {
margin: 0;
padding: 3px 8px;
color: #EEE;
height: 40px;
}
.youtube-theme .shaka-controls-button-panel > *:focus {
outline: none;
-webkit-box-shadow: inset 0 0 0 2px rgba(27, 127, 204, 0.8);
box-shadow: inset 0 0 0 2px rgba(27, 127, 204, 0.8);
color: #FFF;
}
.youtube-theme .shaka-controls-button-panel > *:hover {
color: #FFF;
}
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container {
position: relative;
z-index: 10;
left: -1px;
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
opacity: 0;
width: 0px;
-webkit-transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
height: 3px;
transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
padding: 0;
}
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container:hover,
.youtube-theme .shaka-controls-button-panel .shaka-volume-bar-container:focus {
display: block;
width: 50px;
opacity: 1;
padding: 0 6px;
}
.youtube-theme .shaka-mute-button:hover + div {
opacity: 1;
width: 50px;
padding: 0 6px;
}
.youtube-theme .shaka-current-time {
padding: 0 10px;
font-size: 12px;
}
.youtube-theme .shaka-seek-bar-container {
height: 3px;
position: relative;
top: -1px;
border-radius: 0;
margin-bottom: 0;
}
.youtube-theme .shaka-seek-bar-container .shaka-range-element {
opacity: 0;
}
.youtube-theme .shaka-seek-bar-container:hover {
height: 5px;
top: 0;
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container:hover .shaka-range-element {
opacity: 1;
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container input[type=range]::-webkit-slider-thumb {
background: #FF0000;
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container input[type=range]::-moz-range-thumb {
background: #FF0000;
cursor: pointer;
}
.youtube-theme .shaka-seek-bar-container input[type=range]::-ms-thumb {
background: #FF0000;
cursor: pointer;
}
.youtube-theme .shaka-video-container * {
font-family: 'Roboto', sans-serif;
}
.youtube-theme .shaka-video-container .material-icons-round {
font-family: 'Material Icons Sharp';
}
.youtube-theme .shaka-overflow-menu,
.youtube-theme .shaka-settings-menu {
border-radius: 2px;
background: rgba(28, 28, 28, 0.9);
text-shadow: 0 0 2px rgb(0 0 0%);
-webkit-transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
right: 10px;
bottom: 50px;
padding: 8px 0;
min-width: 200px;
}
.youtube-theme .shaka-settings-menu {
padding: 0 0 8px 0;
}
.youtube-theme .shaka-settings-menu button {
font-size: 12px;
}
.youtube-theme .shaka-settings-menu button span {
margin-left: 33px;
font-size: 13px;
}
.youtube-theme .shaka-settings-menu button[aria-selected="true"] {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.youtube-theme .shaka-settings-menu button[aria-selected="true"] span {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2;
margin-left: 0;
}
.youtube-theme .shaka-settings-menu button[aria-selected="true"] i {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
font-size: 18px;
padding-left: 5px;
}
.youtube-theme .shaka-overflow-menu button {
padding: 0;
}
.youtube-theme .shaka-overflow-menu button i {
display: none;
}
.youtube-theme .shaka-overflow-menu button .shaka-overflow-button-label {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: default;
outline: none;
height: 40px;
-webkit-box-flex: 0;
-ms-flex: 0 0 100%;
flex: 0 0 100%;
}
.youtube-theme .shaka-overflow-menu button .shaka-overflow-button-label span {
-ms-flex-negative: initial;
flex-shrink: initial;
padding-left: 15px;
font-size: 13px;
font-weight: 500;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.youtube-theme .shaka-overflow-menu span + span {
color: #FFF;
font-weight: 400 !important;
font-size: 12px !important;
padding-right: 8px;
padding-left: 0 !important;
}
.youtube-theme .shaka-overflow-menu span + span:after {
content: "navigate_next";
font-family: 'Material Icons Sharp';
font-size: 20px;
}
.youtube-theme .shaka-overflow-menu .shaka-pip-button span + span {
padding-right: 15px !important;
}
.youtube-theme .shaka-overflow-menu .shaka-pip-button span + span:after {
content: "";
}
.youtube-theme .shaka-back-to-overflow-button {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
font-size: 12px;
color: #eee;
height: 40px;
}
.youtube-theme .shaka-back-to-overflow-button .material-icons-round {
font-size: 15px;
padding-right: 10px;
}
.youtube-theme .shaka-back-to-overflow-button span {
margin-left: 3px !important;
}
.youtube-theme .shaka-overflow-menu button:hover,
.youtube-theme .shaka-settings-menu button:hover {
background-color: rgba(255, 255, 255, 0.1);
cursor: pointer;
}
.youtube-theme .shaka-overflow-menu button:hover label,
.youtube-theme .shaka-settings-menu button:hover label {
cursor: pointer;
}
.youtube-theme .shaka-overflow-menu button:focus,
.youtube-theme .shaka-settings-menu button:focus {
background-color: rgba(255, 255, 255, 0.1);
border: none;
outline: none;
}
.youtube-theme .shaka-overflow-menu button,
.youtube-theme .shaka-settings-menu button {
color: #EEE;
}
.youtube-theme .shaka-captions-off {
color: #BFBFBF;
}
.youtube-theme .shaka-overflow-menu-button {
font-size: 18px;
margin-right: 5px;
}
.youtube-theme .shaka-fullscreen-button:hover {
font-size: 25px;
-webkit-transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
}

View File

@@ -1,7 +1,7 @@
module.exports = {
daisyui: {
themes: [
"night"
"night",
],
},
content: ['./src/**/*.{svelte,js,ts}'],