mirror of
https://github.com/Xinrea/bili-shadowreplay.git
synced 2025-11-24 20:15:34 +08:00
WIP: DVR implement
This commit is contained in:
@@ -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
33
live_index.html
Normal 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
111
src-tauri/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
232
src-tauri/src/recorder_manager.rs
Normal file
232
src-tauri/src/recorder_manager.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
98
src/live_main.ts
Normal 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
286
src/youtube-theme.css
Normal 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);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
daisyui: {
|
||||
themes: [
|
||||
"night"
|
||||
"night",
|
||||
],
|
||||
},
|
||||
content: ['./src/**/*.{svelte,js,ts}'],
|
||||
|
||||
Reference in New Issue
Block a user