From eb0d4fb1c5aa23e2caa6ecf29e849c0a0209feb9 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Wed, 5 Aug 2026 12:32:52 +0100 Subject: [PATCH] Init --- .dockerignore | 6 + .github/workflows/build-and-publish.yml | 45 + .gitignore | 1 + Cargo.lock | 1017 +++++++++++++ Cargo.toml | 18 + Dockerfile | 25 + README.md | 43 + src/app.rs | 61 + src/config.rs | 34 + src/diagnostics.rs | 80 + src/error.rs | 42 + src/main.rs | 29 + src/media/browse.rs | 78 + src/media/dash/mod.rs | 195 +++ src/media/dash/process.rs | 65 + src/media/dash/session.rs | 161 ++ src/media/mod.rs | 16 + src/media/path.rs | 30 + src/media/playback/encoder.rs | 61 + src/media/playback/mod.rs | 7 + src/media/playback/planner.rs | 60 + src/media/playback/profile.rs | 95 ++ src/media/preview.rs | 157 ++ src/media/probe.rs | 67 + src/media/stream.rs | 76 + src/media/subtitles.rs | 160 ++ src/media/subtitles/discovery.rs | 127 ++ src/media/subtitles/matching.rs | 237 +++ src/web.rs | 82 + web/alpine.min.js | 5 + web/controlbar.min.js | 1 + web/dash.all.min.js | 3 + web/dash.js.LICENSE.md | 14 + web/index.html | 101 ++ web/js/api.js | 31 + web/js/library.js | 262 ++++ web/js/location.js | 24 + web/js/player.js | 265 ++++ web/js/preferences.js | 63 + web/js/preferences.test.js | 36 + web/styles.css | 61 + web/vendor/bootstrap-icons/LICENSE | 21 + .../bootstrap-icons/bootstrap-icons.min.css | 5 + .../fonts/bootstrap-icons.woff2 | Bin 0 -> 134044 bytes web/vendor/controlbar/ControlBar.js | 1317 +++++++++++++++++ web/vendor/controlbar/controlbar.css | 339 +++++ 46 files changed, 5623 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-and-publish.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 src/app.rs create mode 100644 src/config.rs create mode 100644 src/diagnostics.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/media/browse.rs create mode 100644 src/media/dash/mod.rs create mode 100644 src/media/dash/process.rs create mode 100644 src/media/dash/session.rs create mode 100644 src/media/mod.rs create mode 100644 src/media/path.rs create mode 100644 src/media/playback/encoder.rs create mode 100644 src/media/playback/mod.rs create mode 100644 src/media/playback/planner.rs create mode 100644 src/media/playback/profile.rs create mode 100644 src/media/preview.rs create mode 100644 src/media/probe.rs create mode 100644 src/media/stream.rs create mode 100644 src/media/subtitles.rs create mode 100644 src/media/subtitles/discovery.rs create mode 100644 src/media/subtitles/matching.rs create mode 100644 src/web.rs create mode 100644 web/alpine.min.js create mode 100644 web/controlbar.min.js create mode 100644 web/dash.all.min.js create mode 100644 web/dash.js.LICENSE.md create mode 100644 web/index.html create mode 100644 web/js/api.js create mode 100644 web/js/library.js create mode 100644 web/js/location.js create mode 100644 web/js/player.js create mode 100644 web/js/preferences.js create mode 100644 web/js/preferences.test.js create mode 100644 web/styles.css create mode 100644 web/vendor/bootstrap-icons/LICENSE create mode 100644 web/vendor/bootstrap-icons/bootstrap-icons.min.css create mode 100644 web/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 create mode 100644 web/vendor/controlbar/ControlBar.js create mode 100644 web/vendor/controlbar/controlbar.css diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f9a237d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +**/.git +**/.agents +**/.codex +**/target +/media +**/.DS_Store diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..b350577 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,45 @@ +name: Build and Publish + +on: + push: + tags: + - 'v*.*.*' + +env: + IMAGE_NAME: ultradesu/lan-play + +jobs: + build_docker: + name: Build and Publish Docker Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + run: | + VERSION=$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2) + TAG_NAME=${GITHUB_REF#refs/tags/} + echo "docker_tags=${IMAGE_NAME}:${TAG_NAME},${IMAGE_NAME}:${VERSION},${IMAGE_NAME}:latest" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.docker_tags }} + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e38694c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1017 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lan-play" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "clap", + "mime_guess", + "serde", + "serde_json", + "tempfile", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8cb41b6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lan-play" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1" +axum = "0.8" +clap = { version = "4", features = ["derive"] } +mime_guess = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tempfile = "3" +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["io"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1", features = ["v4"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ce28850 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM rust:1-bookworm AS builder + +WORKDIR /app + +COPY Cargo.toml Cargo.lock ./ +COPY src ./src +COPY web ./web + +RUN cargo build --release --locked + +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates ffmpeg \ + && useradd --system --uid 10001 --create-home lanplay \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/lan-play /usr/local/bin/lan-play + +USER lanplay +WORKDIR /home/lanplay + +EXPOSE 8080 +ENTRYPOINT ["lan-play"] +CMD ["/media", "--bind", "0.0.0.0:8080"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..a446e7c --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# lan-play + +A small self-hosted web player for local video libraries. + +lan-play makes folders with movies, shows, and anime easy to watch on TVs and +other devices with a web browser. Point it at a media directory, open the web +interface, and play files without setting up a database or importing a library. + +## Features + +- Folder-based media browser with animated previews +- Live remuxing and transcoding to browser-compatible MPEG-DASH +- Multiple audio track selection +- Embedded and external ASS, SSA, SRT, and WebVTT subtitles +- Automatic subtitle discovery in nearby folders +- Fullscreen TV-friendly interface with episode navigation +- No database, accounts, or library indexing + +## Docker + +```bash +docker run --rm \ + -p 8080:8080 \ + -v /path/to/videos:/media:ro \ + ultradesu/lan-play:latest +``` + +Open `http://HOST:8080` on your TV or another device on the same network. + +## Run from source + +Requires Rust, `ffmpeg`, and `ffprobe`. + +```bash +cargo run --release -- /path/to/videos +``` + +Use `--bind 127.0.0.1:8080` when placing lan-play behind a reverse proxy. + +## Notes + +lan-play is intended for trusted local networks. It does not provide +authentication and should not be exposed directly to the public internet. diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..a3bea2b --- /dev/null +++ b/src/app.rs @@ -0,0 +1,61 @@ +use std::sync::Arc; + +use axum::{ + Router, + routing::{delete, get, post}, +}; +use tokio::sync::Semaphore; + +use crate::{config::Config, diagnostics, media, media::dash::DashManager, web}; + +pub struct AppState { + pub config: Config, + pub dash: DashManager, + pub preview_jobs: Semaphore, +} + +impl AppState { + pub fn new(config: Config) -> anyhow::Result { + Ok(Self { + config, + dash: DashManager::new()?, + preview_jobs: Semaphore::new(2), + }) + } +} + +pub fn router(state: Arc) -> Router { + Router::new() + .route("/", get(web::index)) + .route("/alpine.min.js", get(web::alpine)) + .route("/dash.all.min.js", get(web::dash)) + .route("/dash.all.min.js.map", get(web::dash_source_map)) + .route("/controlbar.min.js", get(web::controlbar_javascript)) + .route("/controlbar.css", get(web::controlbar_styles)) + .route("/bootstrap-icons.min.css", get(web::bootstrap_icon_styles)) + .route( + "/fonts/bootstrap-icons.woff2", + get(web::bootstrap_icon_font), + ) + .route("/js/api.js", get(web::api_javascript)) + .route("/js/library.js", get(web::library_javascript)) + .route("/js/location.js", get(web::location_javascript)) + .route("/js/preferences.js", get(web::preferences_javascript)) + .route("/js/player.js", get(web::player_javascript)) + .route("/styles.css", get(web::styles)) + .route("/api/info", get(diagnostics::info)) + .route("/api/browse", get(media::browse::browse)) + .route("/api/probe", get(media::probe::probe)) + .route("/api/preview", get(media::preview::sprite)) + .route("/api/stream", get(media::stream::stream)) + .route("/api/subtitles", get(media::subtitles::list)) + .route("/api/subtitles/{index}", get(media::subtitles::extract)) + .route( + "/api/subtitles/external/{id}", + get(media::subtitles::extract_external), + ) + .route("/api/dash", post(media::dash::start)) + .route("/api/dash/{session}", delete(media::dash::stop)) + .route("/api/dash/{session}/{asset}", get(media::dash::asset)) + .with_state(state) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..1315258 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,34 @@ +use std::{net::SocketAddr, path::PathBuf}; + +use anyhow::{Context, ensure}; +use clap::Parser; + +#[derive(Debug, Clone, Parser)] +#[command(version, about = "A small LAN media browser for smart TVs")] +pub struct Config { + /// Directory exposed as the media library. + pub media_root: PathBuf, + + /// HTTP listen address. + #[arg(long, default_value = "0.0.0.0:8080")] + pub bind: SocketAddr, + + /// ffprobe executable. + #[arg(long, default_value = "ffprobe")] + pub ffprobe: PathBuf, + + /// ffmpeg executable used for DASH packaging and transcoding. + #[arg(long, default_value = "ffmpeg")] + pub ffmpeg: PathBuf, +} + +impl Config { + pub fn prepare(mut self) -> anyhow::Result { + ensure!(self.media_root.is_dir(), "media root is not a directory"); + self.media_root = self + .media_root + .canonicalize() + .context("failed to resolve media root")?; + Ok(self) + } +} diff --git a/src/diagnostics.rs b/src/diagnostics.rs new file mode 100644 index 0000000..8dcbc35 --- /dev/null +++ b/src/diagnostics.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use axum::{Json, extract::State}; +use serde::Serialize; +use tokio::process::Command; + +use crate::app::AppState; + +#[derive(Serialize)] +pub struct Diagnostics { + application: &'static str, + version: &'static str, + build: &'static str, + platform: String, + ffmpeg: ToolInfo, + ffprobe: ToolInfo, + media_root: String, + bind: String, +} + +#[derive(Serialize)] +struct ToolInfo { + path: String, + version: String, +} + +pub async fn info(State(state): State>) -> Json { + let (ffmpeg, ffprobe) = tokio::join!( + tool_info(&state.config.ffmpeg), + tool_info(&state.config.ffprobe) + ); + Json(Diagnostics { + application: env!("CARGO_PKG_NAME"), + version: env!("CARGO_PKG_VERSION"), + build: if cfg!(debug_assertions) { + "debug" + } else { + "release" + }, + platform: format!("{} / {}", std::env::consts::OS, std::env::consts::ARCH), + ffmpeg, + ffprobe, + media_root: state.config.media_root.display().to_string(), + bind: state.config.bind.to_string(), + }) +} + +async fn tool_info(path: &std::path::Path) -> ToolInfo { + let version = match Command::new(path).arg("-version").output().await { + Ok(output) if output.status.success() => first_line(&output.stdout), + Ok(output) => format!("Unavailable ({})", output.status), + Err(error) => format!("Unavailable ({error})"), + }; + ToolInfo { + path: path.display().to_string(), + version, + } +} + +fn first_line(output: &[u8]) -> String { + String::from_utf8_lossy(output) + .lines() + .next() + .unwrap_or("Unknown") + .trim() + .to_owned() +} + +#[cfg(test)] +mod tests { + use super::first_line; + + #[test] + fn keeps_only_the_tool_version_line() { + assert_eq!( + first_line(b"ffmpeg version 7.1\nconfiguration: test\n"), + "ffmpeg version 7.1" + ); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c9a9858 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,42 @@ +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde_json::json; + +#[derive(Debug)] +pub struct AppError { + status: StatusCode, + message: String, +} + +impl AppError { + pub fn bad_request(message: impl Into) -> Self { + Self { + status: StatusCode::BAD_REQUEST, + message: message.into(), + } + } + + pub fn not_found(message: impl Into) -> Self { + Self { + status: StatusCode::NOT_FOUND, + message: message.into(), + } + } + + pub fn internal(error: impl std::fmt::Display) -> Self { + tracing::error!(%error, "request failed"); + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: "internal server error".into(), + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + (self.status, Json(json!({ "error": self.message }))).into_response() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0400aa1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,29 @@ +mod app; +mod config; +mod diagnostics; +mod error; +mod media; +mod web; + +use std::sync::Arc; + +use clap::Parser; +use config::Config; +use tracing::info; + +use crate::app::AppState; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let config = Config::parse().prepare()?; + let listener = tokio::net::TcpListener::bind(config.bind).await?; + info!(address = %config.bind, media_root = %config.media_root.display(), "lan-play started"); + let state = Arc::new(AppState::new(config)?); + + axum::serve(listener, app::router(state)).await?; + Ok(()) +} diff --git a/src/media/browse.rs b/src/media/browse.rs new file mode 100644 index 0000000..bb98ddc --- /dev/null +++ b/src/media/browse.rs @@ -0,0 +1,78 @@ +use std::{path::Path, sync::Arc}; + +use axum::{ + Json, + extract::{Query, State}, +}; +use serde::Serialize; +use tokio::fs; + +use crate::{ + app::AppState, + error::AppError, + media::{PathQuery, path}, +}; + +const MEDIA_EXTENSIONS: &[&str] = &["mkv", "mp4", "m4v", "avi", "mov", "webm"]; + +#[derive(Serialize)] +pub struct Entry { + name: String, + path: String, + kind: &'static str, +} + +#[derive(Serialize)] +pub struct Directory { + path: String, + parent: Option, + entries: Vec, +} + +pub async fn browse( + State(state): State>, + Query(query): Query, +) -> Result, AppError> { + let config = &state.config; + let directory = path::resolve(config, &query.path)?; + if !directory.is_dir() { + return Err(AppError::bad_request("path is not a directory")); + } + + let mut reader = fs::read_dir(&directory).await.map_err(AppError::internal)?; + let mut entries = Vec::new(); + while let Some(item) = reader.next_entry().await.map_err(AppError::internal)? { + let file_type = item.file_type().await.map_err(AppError::internal)?; + let item_path = item.path(); + let kind = if file_type.is_dir() { + "directory" + } else if file_type.is_file() && is_media(&item_path) { + "media" + } else { + continue; + }; + entries.push(Entry { + name: item.file_name().to_string_lossy().into_owned(), + path: path::relative(config, &item_path)?, + kind, + }); + } + entries.sort_by_key(|entry| (entry.kind != "directory", entry.name.to_lowercase())); + + let current = path::relative(config, &directory)?; + let parent = directory + .parent() + .filter(|parent| parent.starts_with(&config.media_root)) + .and_then(|parent| path::relative(config, parent).ok()); + Ok(Json(Directory { + path: current, + parent, + entries, + })) +} + +fn is_media(path: &Path) -> bool { + path.extension() + .and_then(|value| value.to_str()) + .is_some_and(|value| MEDIA_EXTENSIONS.contains(&value.to_ascii_lowercase().as_str())) +} diff --git a/src/media/dash/mod.rs b/src/media/dash/mod.rs new file mode 100644 index 0000000..fc8d635 --- /dev/null +++ b/src/media/dash/mod.rs @@ -0,0 +1,195 @@ +mod process; +mod session; + +use std::{collections::HashMap, path::PathBuf, sync::Arc}; + +use axum::{ + Json, + body::Body, + extract::{Path, Query, State}, + http::{StatusCode, header}, + response::{IntoResponse, Response}, +}; +use serde::{Deserialize, Serialize}; +use tempfile::TempDir; +use tokio::{fs, sync::Mutex}; + +use crate::{ + app::AppState, + error::AppError, + media::{path, playback}, +}; + +use self::session::DashSession; + +pub struct DashManager { + _root: TempDir, + root_path: PathBuf, + sessions: Mutex>>, +} + +#[derive(Serialize)] +pub struct StartResponse { + session_id: String, + manifest_url: String, + duration_seconds: Option, + start_seconds: f64, +} + +#[derive(Deserialize)] +pub struct StartQuery { + path: String, + #[serde(default)] + start: f64, +} + +impl DashManager { + pub fn new() -> anyhow::Result { + let root = tempfile::Builder::new() + .prefix("lan-play-dash-") + .tempdir()?; + Ok(Self { + root_path: root.path().to_owned(), + _root: root, + sessions: Mutex::new(HashMap::new()), + }) + } +} + +pub async fn start( + State(state): State>, + Query(query): Query, +) -> Result, AppError> { + if !query.start.is_finite() || query.start < 0.0 { + return Err(AppError::bad_request("invalid playback start position")); + } + let media = path::resolve(&state.config, &query.path)?; + if !media.is_file() { + return Err(AppError::bad_request("path is not a media file")); + } + + let plan = playback::plan(&state.config, &media).await?; + if plan + .duration_seconds + .is_some_and(|duration| query.start >= duration) + { + return Err(AppError::bad_request( + "playback start is beyond the file duration", + )); + } + let duration_seconds = plan.duration_seconds; + let session = DashSession::start( + &state.config.ffmpeg, + &state.dash.root_path, + &media, + &plan, + query.start, + ) + .await?; + let session_id = session.id().to_owned(); + state + .dash + .sessions + .lock() + .await + .insert(session_id.clone(), Arc::new(session)); + + Ok(Json(StartResponse { + manifest_url: format!("/api/dash/{session_id}/manifest.mpd"), + session_id, + duration_seconds, + start_seconds: query.start, + })) +} + +pub async fn asset( + State(state): State>, + Path((session_id, asset)): Path<(String, String)>, +) -> Result { + if !session::valid_asset_name(&asset) { + return Err(AppError::bad_request("invalid DASH asset name")); + } + let session = state + .dash + .sessions + .lock() + .await + .get(&session_id) + .cloned() + .ok_or_else(|| AppError::not_found("DASH session not found"))?; + let file = session.asset_path(&asset); + let bytes = fs::read(file) + .await + .map_err(|_| AppError::not_found("DASH asset not ready"))?; + let content_type = asset_content_type(&asset); + Ok(( + StatusCode::OK, + [ + (header::CONTENT_TYPE, content_type), + (header::CACHE_CONTROL, "no-store"), + ], + Body::from(bytes), + ) + .into_response()) +} + +fn asset_content_type(asset: &str) -> &'static str { + if asset.ends_with(".mpd") { + "application/dash+xml" + } else if dash_stream_index(asset).is_some_and(|stream| stream > 0) { + "audio/iso.segment" + } else if asset.ends_with(".m4s") || asset.ends_with(".mp4") { + "video/iso.segment" + } else { + "application/octet-stream" + } +} + +fn dash_stream_index(asset: &str) -> Option { + let start = asset.find("stream")? + "stream".len(); + let digits: String = asset[start..] + .chars() + .take_while(|character| character.is_ascii_digit()) + .collect(); + digits.parse().ok() +} + +pub async fn stop( + State(state): State>, + Path(session_id): Path, +) -> Result { + let session = state + .dash + .sessions + .lock() + .await + .remove(&session_id) + .ok_or_else(|| AppError::not_found("DASH session not found"))?; + session.stop().await; + Ok(StatusCode::NO_CONTENT) +} + +#[cfg(test)] +mod tests { + use super::asset_content_type; + + #[test] + fn uses_media_specific_content_types() { + assert_eq!(asset_content_type("manifest.mpd"), "application/dash+xml"); + assert_eq!(asset_content_type("init-stream0.m4s"), "video/iso.segment"); + assert_eq!( + asset_content_type("chunk-stream0-00001.m4s"), + "video/iso.segment" + ); + assert_eq!(asset_content_type("init-stream1.m4s"), "audio/iso.segment"); + assert_eq!( + asset_content_type("chunk-stream1-00001.m4s"), + "audio/iso.segment" + ); + assert_eq!(asset_content_type("init-stream2.m4s"), "audio/iso.segment"); + assert_eq!( + asset_content_type("chunk-stream7-00001.m4s"), + "audio/iso.segment" + ); + } +} diff --git a/src/media/dash/process.rs b/src/media/dash/process.rs new file mode 100644 index 0000000..2ae393c --- /dev/null +++ b/src/media/dash/process.rs @@ -0,0 +1,65 @@ +use std::{fs::OpenOptions, path::Path, process::Stdio}; + +use tokio::process::{Child, Command}; + +use crate::error::AppError; +use crate::media::playback::PlaybackPlan; + +const SEEK_PREROLL_SECONDS: f64 = 3.0; + +pub fn spawn( + ffmpeg: &Path, + media: &Path, + directory: &Path, + plan: &PlaybackPlan, + start_seconds: f64, +) -> Result { + let log = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(directory.join("ffmpeg.log")) + .map_err(AppError::internal)?; + let mut command = Command::new(ffmpeg); + command.current_dir(directory).args(["-v", "warning"]); + let (input_seek, output_trim) = seek_offsets(start_seconds); + if input_seek > 0.0 { + command.args(["-ss", &format!("{input_seek:.3}")]); + } + command + // Build a small lead over playback so transient encoder, disk or network + // jitter does not leave the player waiting at the live edge. + .args(["-readrate", "1.15"]); + if output_trim > 0.0 { + command.args(["-readrate_initial_burst", &format!("{output_trim:.3}")]); + } + command.args(["-i"]).arg(media); + if output_trim > 0.0 { + command.args(["-ss", &format!("{output_trim:.3}")]); + } + command + .args(plan.ffmpeg_output_args()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::from(log)) + .kill_on_drop(true) + .spawn() + .map_err(AppError::internal) +} + +fn seek_offsets(start_seconds: f64) -> (f64, f64) { + let input_seek = (start_seconds - SEEK_PREROLL_SECONDS).max(0.0); + (input_seek, start_seconds - input_seek) +} + +#[cfg(test)] +mod tests { + use super::seek_offsets; + + #[test] + fn seek_keeps_a_short_accurate_preroll() { + assert_eq!(seek_offsets(0.0), (0.0, 0.0)); + assert_eq!(seek_offsets(2.0), (0.0, 2.0)); + assert_eq!(seek_offsets(100.0), (97.0, 3.0)); + } +} diff --git a/src/media/dash/session.rs b/src/media/dash/session.rs new file mode 100644 index 0000000..082f375 --- /dev/null +++ b/src/media/dash/session.rs @@ -0,0 +1,161 @@ +use std::{ + path::{Component, Path, PathBuf}, + time::Duration, +}; + +use tokio::{fs, process::Child, sync::Mutex, time}; +use uuid::Uuid; + +use crate::{error::AppError, media::playback::PlaybackPlan}; + +use super::process; + +pub struct DashSession { + id: String, + directory: PathBuf, + process: Mutex>, +} + +impl DashSession { + pub async fn start( + ffmpeg: &Path, + root: &Path, + media: &Path, + plan: &PlaybackPlan, + start_seconds: f64, + ) -> Result { + let id = Uuid::new_v4().simple().to_string(); + let directory = root.join(&id); + fs::create_dir(&directory) + .await + .map_err(AppError::internal)?; + let mut child = match process::spawn(ffmpeg, media, &directory, plan, start_seconds) { + Ok(child) => child, + Err(error) => { + fs::remove_dir_all(&directory).await.ok(); + return Err(error); + } + }; + wait_until_ready(&mut child, &directory, plan.audio_count()).await?; + Ok(Self { + id, + directory, + process: Mutex::new(Some(child)), + }) + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn asset_path(&self, name: &str) -> PathBuf { + self.directory.join(name) + } + + pub async fn stop(&self) { + if let Some(mut child) = self.process.lock().await.take() { + if child.try_wait().ok().flatten().is_none() { + child.start_kill().ok(); + } + child.wait().await.ok(); + } + fs::remove_dir_all(&self.directory).await.ok(); + } +} + +async fn wait_until_ready( + child: &mut Child, + directory: &Path, + audio_count: usize, +) -> Result<(), AppError> { + for _ in 0..1800 { + if ready(directory, audio_count).await { + return Ok(()); + } + if child.try_wait().map_err(AppError::internal)?.is_some() { + return Err(startup_error(directory, "ffmpeg stopped before DASH became ready").await); + } + time::sleep(Duration::from_millis(50)).await; + } + child.start_kill().ok(); + child.wait().await.ok(); + Err(startup_error(directory, "timed out waiting for live DASH segments").await) +} + +async fn ready(directory: &Path, audio_count: usize) -> bool { + let manifest = match fs::read_to_string(directory.join("manifest.mpd")).await { + Ok(value) => value, + Err(_) => return false, + }; + if !manifest.contains("type=\"dynamic\"") && !manifest.contains("type=\"static\"") { + return false; + } + if !stream_ready(directory, 0).await { + return false; + } + for stream in 1..=audio_count { + if !stream_ready(directory, stream).await { + return false; + } + } + true +} + +async fn stream_ready(directory: &Path, stream: usize) -> bool { + directory.join(format!("init-stream{stream}.m4s")).is_file() + && has_segment(directory, stream).await +} + +async fn has_segment(directory: &Path, stream: usize) -> bool { + let prefix = format!("chunk-stream{stream}-"); + let mut entries = match fs::read_dir(directory).await { + Ok(value) => value, + Err(_) => return false, + }; + while let Ok(Some(entry)) = entries.next_entry().await { + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name.starts_with(&prefix) && name.ends_with(".m4s") { + return true; + } + } + false +} + +async fn startup_error(directory: &Path, fallback: &str) -> AppError { + let log = fs::read_to_string(directory.join("ffmpeg.log")) + .await + .unwrap_or_default(); + fs::remove_dir_all(directory).await.ok(); + let details = log.trim(); + AppError::bad_request(if details.is_empty() { + fallback.to_owned() + } else { + format!("{fallback}: {details}") + }) +} + +pub fn valid_asset_name(name: &str) -> bool { + let mut components = Path::new(name).components(); + !name.is_empty() + && matches!(components.next(), Some(Component::Normal(_))) + && components.next().is_none() + && matches!( + Path::new(name).extension().and_then(|value| value.to_str()), + Some("mpd" | "m4s" | "mp4") + ) +} + +#[cfg(test)] +mod tests { + use super::valid_asset_name; + + #[test] + fn accepts_flat_dash_assets_only() { + assert!(valid_asset_name("manifest.mpd")); + assert!(valid_asset_name("chunk-stream0-00001.m4s")); + assert!(!valid_asset_name("../secret.mp4")); + assert!(!valid_asset_name("nested/chunk.m4s")); + assert!(!valid_asset_name("notes.txt")); + } +} diff --git a/src/media/mod.rs b/src/media/mod.rs new file mode 100644 index 0000000..d1e5962 --- /dev/null +++ b/src/media/mod.rs @@ -0,0 +1,16 @@ +pub mod browse; +pub mod dash; +pub mod path; +pub mod playback; +pub mod preview; +pub mod probe; +pub mod stream; +pub mod subtitles; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct PathQuery { + #[serde(default)] + pub path: String, +} diff --git a/src/media/path.rs b/src/media/path.rs new file mode 100644 index 0000000..a4a9758 --- /dev/null +++ b/src/media/path.rs @@ -0,0 +1,30 @@ +use std::path::{Component, Path, PathBuf}; + +use crate::{config::Config, error::AppError}; + +pub fn resolve(config: &Config, relative: &str) -> Result { + let relative = Path::new(relative); + if relative.is_absolute() + || relative + .components() + .any(|part| !matches!(part, Component::Normal(_))) + { + return Err(AppError::bad_request("invalid media path")); + } + + let candidate = config.media_root.join(relative); + let canonical = candidate + .canonicalize() + .map_err(|_| AppError::not_found("media path not found"))?; + if !canonical.starts_with(&config.media_root) { + return Err(AppError::bad_request("media path escapes the library")); + } + Ok(canonical) +} + +pub fn relative(config: &Config, absolute: &Path) -> Result { + absolute + .strip_prefix(&config.media_root) + .map(|path| path.to_string_lossy().replace('\\', "/")) + .map_err(AppError::internal) +} diff --git a/src/media/playback/encoder.rs b/src/media/playback/encoder.rs new file mode 100644 index 0000000..5ec0f59 --- /dev/null +++ b/src/media/playback/encoder.rs @@ -0,0 +1,61 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VideoPipeline { + Copy, + Encode(VideoEncoder), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VideoEncoder { + Libx264, +} + +impl VideoPipeline { + pub fn ffmpeg_args(self, segment_seconds: u8) -> Vec { + match self { + Self::Copy => vec!["-c:v".into(), "copy".into()], + Self::Encode(encoder) => encoder.ffmpeg_args(segment_seconds), + } + } +} + +impl VideoEncoder { + pub fn portable_software() -> Self { + Self::Libx264 + } + + fn ffmpeg_args(self, segment_seconds: u8) -> Vec { + match self { + Self::Libx264 => vec![ + "-c:v".into(), + "libx264".into(), + "-preset".into(), + "veryfast".into(), + "-crf".into(), + "20".into(), + "-vf".into(), + "scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv420p".into(), + "-force_key_frames".into(), + format!("expr:gte(t,n_forced*{segment_seconds})"), + "-sc_threshold".into(), + "0".into(), + ], + } + } +} + +#[cfg(test)] +mod tests { + use super::{VideoEncoder, VideoPipeline}; + + #[test] + fn libx264_profile_is_browser_compatible_and_segment_aligned() { + let args = VideoPipeline::Encode(VideoEncoder::Libx264).ffmpeg_args(2); + assert!(args.windows(2).any(|pair| pair == ["-c:v", "libx264"])); + assert!(args.windows(2).any(|pair| pair == ["-crf", "20"])); + assert!( + args.windows(2) + .any(|pair| { pair == ["-force_key_frames", "expr:gte(t,n_forced*2)"] }) + ); + assert!(args.iter().any(|arg| arg.contains("format=yuv420p"))); + } +} diff --git a/src/media/playback/mod.rs b/src/media/playback/mod.rs new file mode 100644 index 0000000..6a92427 --- /dev/null +++ b/src/media/playback/mod.rs @@ -0,0 +1,7 @@ +mod encoder; +mod planner; +mod profile; + +pub use encoder::{VideoEncoder, VideoPipeline}; +pub use planner::plan; +pub use profile::PlaybackPlan; diff --git a/src/media/playback/planner.rs b/src/media/playback/planner.rs new file mode 100644 index 0000000..03a1310 --- /dev/null +++ b/src/media/playback/planner.rs @@ -0,0 +1,60 @@ +use std::path::Path; + +use crate::{config::Config, error::AppError, media::probe}; + +use super::{PlaybackPlan, VideoEncoder, VideoPipeline}; + +pub async fn plan(config: &Config, media: &Path) -> Result { + let metadata = probe::run(config, media).await?; + let video = metadata + .streams + .iter() + .find(|stream| stream.codec_type.as_deref() == Some("video")) + .ok_or_else(|| AppError::bad_request("media file has no video stream"))?; + let video_pipeline = + select_video_pipeline(video.codec_name.as_deref(), video.pix_fmt.as_deref()); + let audio_streams = metadata + .streams + .iter() + .filter(|stream| stream.codec_type.as_deref() == Some("audio")) + .map(|stream| stream.index) + .collect(); + let duration_seconds = metadata + .format + .and_then(|format| format.duration) + .and_then(|duration| duration.parse().ok()); + Ok(PlaybackPlan::live_dash( + video_pipeline, + audio_streams, + duration_seconds, + )) +} + +fn select_video_pipeline(codec: Option<&str>, pixel_format: Option<&str>) -> VideoPipeline { + if codec == Some("h264") && matches!(pixel_format, Some("yuv420p" | "yuvj420p")) { + VideoPipeline::Copy + } else { + VideoPipeline::Encode(VideoEncoder::portable_software()) + } +} + +#[cfg(test)] +mod tests { + use super::{VideoEncoder, VideoPipeline, select_video_pipeline}; + + #[test] + fn copies_only_browser_compatible_h264() { + assert_eq!( + select_video_pipeline(Some("h264"), Some("yuv420p")), + VideoPipeline::Copy + ); + assert_eq!( + select_video_pipeline(Some("mpeg4"), Some("yuv420p")), + VideoPipeline::Encode(VideoEncoder::Libx264) + ); + assert_eq!( + select_video_pipeline(Some("h264"), Some("yuv420p10le")), + VideoPipeline::Encode(VideoEncoder::Libx264) + ); + } +} diff --git a/src/media/playback/profile.rs b/src/media/playback/profile.rs new file mode 100644 index 0000000..c1edf13 --- /dev/null +++ b/src/media/playback/profile.rs @@ -0,0 +1,95 @@ +use super::VideoPipeline; + +#[derive(Debug, Clone)] +pub struct PlaybackPlan { + pub video_pipeline: VideoPipeline, + pub audio_streams: Vec, + pub duration_seconds: Option, + pub segment_seconds: u8, +} + +impl PlaybackPlan { + pub fn live_dash( + video_pipeline: VideoPipeline, + audio_streams: Vec, + duration_seconds: Option, + ) -> Self { + Self { + video_pipeline, + audio_streams, + duration_seconds, + segment_seconds: 2, + } + } + + pub fn audio_count(&self) -> usize { + self.audio_streams.len() + } + + pub fn ffmpeg_output_args(&self) -> Vec { + let mut args = vec!["-map".into(), "0:v:0".into()]; + args.extend(self.video_pipeline.ffmpeg_args(self.segment_seconds)); + for stream in &self.audio_streams { + args.extend(["-map".into(), format!("0:{stream}")]); + } + if !self.audio_streams.is_empty() { + args.extend([ + "-c:a".into(), + "aac".into(), + "-profile:a".into(), + "aac_low".into(), + "-b:a".into(), + "192k".into(), + "-ac".into(), + "2".into(), + ]); + } + let mut adaptation_sets = vec!["id=0,streams=0".to_owned()]; + adaptation_sets.extend( + (1..=self.audio_streams.len()).map(|output| format!("id={output},streams={output}")), + ); + args.extend([ + "-sn".into(), + "-f".into(), + "dash".into(), + "-seg_duration".into(), + self.segment_seconds.to_string(), + "-update_period".into(), + "1".into(), + "-use_template".into(), + "1".into(), + "-use_timeline".into(), + "1".into(), + "-window_size".into(), + "0".into(), + "-extra_window_size".into(), + "0".into(), + "-adaptation_sets".into(), + adaptation_sets.join(" "), + "manifest.mpd".into(), + ]); + args + } +} + +#[cfg(test)] +mod tests { + use super::{PlaybackPlan, VideoPipeline}; + + #[test] + fn live_profile_copies_video_and_encodes_audio_to_aac() { + let args = PlaybackPlan::live_dash(VideoPipeline::Copy, vec![1, 3], Some(120.0)) + .ffmpeg_output_args(); + assert!(args.windows(2).any(|pair| pair == ["-c:v", "copy"])); + assert!(args.windows(2).any(|pair| pair == ["-c:a", "aac"])); + assert!(args.windows(2).any(|pair| pair == ["-ac", "2"])); + assert!(args.windows(2).any(|pair| pair == ["-map", "0:1"])); + assert!(args.windows(2).any(|pair| pair == ["-map", "0:3"])); + assert!(args.windows(2).any(|pair| { + pair == [ + "-adaptation_sets", + "id=0,streams=0 id=1,streams=1 id=2,streams=2", + ] + })); + } +} diff --git a/src/media/preview.rs b/src/media/preview.rs new file mode 100644 index 0000000..703e68b --- /dev/null +++ b/src/media/preview.rs @@ -0,0 +1,157 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + process::Stdio, + sync::Arc, + time::Duration, +}; + +use axum::{ + body::Body, + extract::{Query, State}, + http::header, + response::{IntoResponse, Response}, +}; +use tokio::process::Command; + +use crate::{ + app::AppState, + error::AppError, + media::{PathQuery, path, probe}, +}; + +const FRAME_COUNT: usize = 10; +const FRAME_WIDTH: usize = 320; +const FRAME_HEIGHT: usize = 180; + +pub async fn sprite( + State(state): State>, + Query(query): Query, +) -> Result { + let media = path::resolve(&state.config, &query.path)?; + if !media.is_file() { + return Err(AppError::bad_request("path is not a media file")); + } + let _permit = state + .preview_jobs + .acquire() + .await + .map_err(AppError::internal)?; + let metadata = probe::run(&state.config, &media).await?; + let duration = metadata + .format + .and_then(|format| format.duration) + .and_then(|value| value.parse::().ok()) + .filter(|value| value.is_finite() && *value > 0.0) + .ok_or_else(|| AppError::bad_request("media duration is unavailable"))?; + + let timestamps = preview_timestamps(&query.path, duration); + let mut command = Command::new(&state.config.ffmpeg); + command.args(["-v", "error"]); + for timestamp in timestamps { + command + .args(["-ss", &format!("{timestamp:.3}"), "-i"]) + .arg(&media); + } + let filter = sprite_filter(); + command + .args([ + "-filter_complex", + &filter, + "-map", + "[sprite]", + "-frames:v", + "1", + "-c:v", + "mjpeg", + "-q:v", + "5", + "-f", + "image2pipe", + "pipe:1", + ]) + .stdin(Stdio::null()) + .kill_on_drop(true); + let output = tokio::time::timeout(Duration::from_secs(30), command.output()) + .await + .map_err(|_| AppError::bad_request("preview generation timed out"))? + .map_err(AppError::internal)?; + if !output.status.success() || output.stdout.is_empty() { + let details = String::from_utf8_lossy(&output.stderr); + return Err(AppError::bad_request(format!( + "failed to create preview: {}", + details.trim() + ))); + } + Ok(( + [ + (header::CONTENT_TYPE, "image/jpeg"), + (header::CACHE_CONTROL, "private, max-age=3600"), + ], + Body::from(output.stdout), + ) + .into_response()) +} + +fn preview_timestamps(path: &str, duration: f64) -> [f64; FRAME_COUNT] { + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + let mut seed = hasher.finish(); + std::array::from_fn(|index| { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + let jitter = (seed as f64 / u64::MAX as f64 - 0.5) * 0.06; + let position = ((index as f64 + 0.5) / FRAME_COUNT as f64 + jitter).clamp(0.02, 0.98); + position * duration + }) +} + +fn sprite_filter() -> String { + let mut filters = (0..FRAME_COUNT) + .map(|index| { + format!( + "[{index}:v:0]setpts=PTS-STARTPTS,scale={FRAME_WIDTH}:{FRAME_HEIGHT}:force_original_aspect_ratio=decrease,pad={FRAME_WIDTH}:{FRAME_HEIGHT}:(ow-iw)/2:(oh-ih)/2:black,setsar=1[v{index}]" + ) + }) + .collect::>(); + let inputs = (0..FRAME_COUNT) + .map(|index| format!("[v{index}]")) + .collect::(); + let layout = (0..FRAME_COUNT) + .map(|index| { + format!( + "{}_{}", + (index % 5) * FRAME_WIDTH, + (index / 5) * FRAME_HEIGHT + ) + }) + .collect::>() + .join("|"); + filters.push(format!( + "{inputs}xstack=inputs={FRAME_COUNT}:layout={layout}[sprite]" + )); + filters.join(";") +} + +#[cfg(test)] +mod tests { + use super::{FRAME_COUNT, preview_timestamps, sprite_filter}; + + #[test] + fn timestamps_are_stable_and_inside_the_video() { + let first = preview_timestamps("series/episode.mkv", 100.0); + let second = preview_timestamps("series/episode.mkv", 100.0); + assert_eq!(first, second); + assert!(first.iter().all(|value| *value >= 2.0 && *value <= 98.0)); + } + + #[test] + fn sprite_contains_every_input() { + let filter = sprite_filter(); + assert!(filter.contains(&format!("xstack=inputs={FRAME_COUNT}"))); + for index in 0..FRAME_COUNT { + assert!(filter.contains(&format!("[{index}:v:0]"))); + } + } +} diff --git a/src/media/probe.rs b/src/media/probe.rs new file mode 100644 index 0000000..aacd653 --- /dev/null +++ b/src/media/probe.rs @@ -0,0 +1,67 @@ +use std::{path::Path, process::Stdio, sync::Arc}; + +use axum::{ + Json, + extract::{Query, State}, +}; +use serde::{Deserialize, Serialize}; +use tokio::process::Command; + +use crate::{ + app::AppState, + config::Config, + error::AppError, + media::{PathQuery, path}, +}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Probe { + #[serde(default)] + pub streams: Vec, + pub format: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Stream { + pub index: u32, + pub codec_name: Option, + pub codec_type: Option, + pub pix_fmt: Option, + #[serde(default)] + pub tags: serde_json::Map, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Format { + pub format_name: Option, + pub duration: Option, +} + +pub async fn probe( + State(state): State>, + Query(query): Query, +) -> Result, AppError> { + let media = path::resolve(&state.config, &query.path)?; + Ok(Json(run(&state.config, &media).await?)) +} + +pub async fn run(config: &Config, media: &Path) -> Result { + let output = Command::new(&config.ffprobe) + .args([ + "-v", + "error", + "-show_format", + "-show_streams", + "-of", + "json", + ]) + .arg(media) + .stdin(Stdio::null()) + .output() + .await + .map_err(AppError::internal)?; + if !output.status.success() { + return Err(AppError::internal(String::from_utf8_lossy(&output.stderr))); + } + serde_json::from_slice(&output.stdout).map_err(AppError::internal) +} diff --git a/src/media/stream.rs b/src/media/stream.rs new file mode 100644 index 0000000..649bb2a --- /dev/null +++ b/src/media/stream.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use axum::{ + body::Body, + extract::{Query, State}, + http::{HeaderMap, StatusCode, header}, + response::Response, +}; +use tokio::{ + fs::File, + io::{AsyncReadExt, AsyncSeekExt, SeekFrom}, +}; + +use crate::{ + app::AppState, + error::AppError, + media::{PathQuery, path}, +}; + +pub async fn stream( + State(state): State>, + Query(query): Query, + headers: HeaderMap, +) -> Result, AppError> { + let media = path::resolve(&state.config, &query.path)?; + if !media.is_file() { + return Err(AppError::bad_request("path is not a file")); + } + let mut file = File::open(&media).await.map_err(AppError::internal)?; + let size = file.metadata().await.map_err(AppError::internal)?.len(); + let (start, end, status) = match headers.get(header::RANGE).and_then(|v| v.to_str().ok()) { + Some(value) => { + let (start, end) = parse_range(value, size)?; + (start, end, StatusCode::PARTIAL_CONTENT) + } + None => (0, size.saturating_sub(1), StatusCode::OK), + }; + let length = end - start + 1; + file.seek(SeekFrom::Start(start)) + .await + .map_err(AppError::internal)?; + let body = Body::from_stream(tokio_util::io::ReaderStream::new(file.take(length))); + let mime = mime_guess::from_path(&media).first_or_octet_stream(); + + let mut response = Response::builder() + .status(status) + .header(header::ACCEPT_RANGES, "bytes") + .header(header::CONTENT_LENGTH, length) + .header(header::CONTENT_TYPE, mime.as_ref()); + if status == StatusCode::PARTIAL_CONTENT { + response = response.header(header::CONTENT_RANGE, format!("bytes {start}-{end}/{size}")); + } + response.body(body).map_err(AppError::internal) +} + +fn parse_range(value: &str, size: u64) -> Result<(u64, u64), AppError> { + let value = value + .strip_prefix("bytes=") + .ok_or_else(|| AppError::bad_request("invalid range"))?; + let (start, end) = value + .split_once('-') + .ok_or_else(|| AppError::bad_request("invalid range"))?; + let start = start + .parse::() + .map_err(|_| AppError::bad_request("invalid range"))?; + let end = if end.is_empty() { + size.saturating_sub(1) + } else { + end.parse() + .map_err(|_| AppError::bad_request("invalid range"))? + }; + if size == 0 || start > end || end >= size { + return Err(AppError::bad_request("range outside file")); + } + Ok((start, end)) +} diff --git a/src/media/subtitles.rs b/src/media/subtitles.rs new file mode 100644 index 0000000..7662acf --- /dev/null +++ b/src/media/subtitles.rs @@ -0,0 +1,160 @@ +use std::{process::Stdio, sync::Arc}; + +use axum::{ + Json, + body::Body, + extract::{Path, Query, State}, + http::header, + response::{IntoResponse, Response}, +}; +use serde::{Deserialize, Serialize}; +use tokio::process::Command; +use tokio_util::io::ReaderStream; + +use crate::{ + app::AppState, + error::AppError, + media::{PathQuery, path, probe}, +}; + +mod discovery; +mod matching; + +#[derive(Serialize)] +pub struct Subtitle { + index: Option, + id: Option, + source: &'static str, + codec: String, + language: Option, + title: Option, + supported: bool, + confidence: Option<&'static str>, + score: Option, +} + +#[derive(Deserialize)] +pub struct ExtractQuery { + path: String, + #[serde(default)] + start: f64, +} + +pub async fn list( + State(state): State>, + Query(query): Query, +) -> Result>, AppError> { + let media = path::resolve(&state.config, &query.path)?; + let info = probe::run(&state.config, &media).await?; + let mut subtitles = info + .streams + .into_iter() + .filter(|stream| stream.codec_type.as_deref() == Some("subtitle")) + .map(|stream| { + let codec = stream.codec_name.unwrap_or_else(|| "unknown".into()); + Subtitle { + index: Some(stream.index), + id: None, + source: "embedded", + supported: matches!( + codec.as_str(), + "subrip" | "srt" | "ass" | "ssa" | "webvtt" | "mov_text" + ), + language: tag(&stream.tags, "language"), + title: tag(&stream.tags, "title"), + codec, + confidence: None, + score: None, + } + }) + .collect::>(); + subtitles.extend( + discovery::find(&state.config, &media) + .await? + .into_iter() + .map(|candidate| Subtitle { + index: None, + id: Some(candidate.id), + source: "external", + codec: candidate.codec, + language: None, + title: Some(candidate.label), + supported: true, + confidence: Some(candidate.confidence.as_str()), + score: Some(candidate.score), + }), + ); + Ok(Json(subtitles)) +} + +pub async fn extract( + State(state): State>, + Path(index): Path, + Query(query): Query, +) -> Result { + if !query.start.is_finite() || query.start < 0.0 { + return Err(AppError::bad_request("invalid subtitle start position")); + } + let media = path::resolve(&state.config, &query.path)?; + stream_webvtt( + &state.config.ffmpeg, + &media, + &format!("0:{index}"), + query.start, + ) + .await +} + +pub async fn extract_external( + State(state): State>, + Path(id): Path, + Query(query): Query, +) -> Result { + if !query.start.is_finite() || query.start < 0.0 { + return Err(AppError::bad_request("invalid subtitle start position")); + } + let media = path::resolve(&state.config, &query.path)?; + let candidate = discovery::resolve_id(&state.config, &media, &id).await?; + stream_webvtt(&state.config.ffmpeg, &candidate.path, "0:0", query.start).await +} + +async fn stream_webvtt( + ffmpeg: &std::path::Path, + input: &std::path::Path, + map: &str, + start: f64, +) -> Result { + let mut command = Command::new(ffmpeg); + command.args(["-v", "error"]); + if start > 0.0 { + command.args(["-ss", &format!("{start:.3}")]); + } + let mut child = command + .arg("-i") + .arg(input) + .args(["-map", map, "-f", "webvtt", "pipe:1"]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .map_err(AppError::internal)?; + let stdout = child + .stdout + .take() + .ok_or_else(|| AppError::internal("ffmpeg stdout unavailable"))?; + tokio::spawn(async move { + let _ = child.wait().await; + }); + Ok(( + [(header::CONTENT_TYPE, "text/vtt; charset=utf-8")], + Body::from_stream(ReaderStream::new(stdout)), + ) + .into_response()) +} + +fn tag(tags: &serde_json::Map, name: &str) -> Option { + tags.get(name) + .or_else(|| tags.get(&name.to_uppercase())) + .and_then(|value| value.as_str()) + .map(str::to_owned) +} diff --git a/src/media/subtitles/discovery.rs b/src/media/subtitles/discovery.rs new file mode 100644 index 0000000..7567f94 --- /dev/null +++ b/src/media/subtitles/discovery.rs @@ -0,0 +1,127 @@ +use std::{ + collections::{HashSet, VecDeque, hash_map::DefaultHasher}, + hash::{Hash, Hasher}, + path::{Path, PathBuf}, +}; + +use tokio::fs; + +use crate::{config::Config, error::AppError, media::path}; + +use super::matching::{self, Confidence}; + +const SUBTITLE_EXTENSIONS: &[&str] = &["ass", "ssa", "srt", "vtt"]; +const MAX_DEPTH: usize = 3; +const MAX_ENTRIES: usize = 2_000; + +#[derive(Debug, Clone)] +pub struct Candidate { + pub id: String, + pub path: PathBuf, + pub relative_path: String, + pub codec: String, + pub label: String, + pub score: u8, + pub confidence: Confidence, +} + +pub async fn find(config: &Config, video: &Path) -> Result, AppError> { + let mut roots = Vec::new(); + if let Some(parent) = video.parent() { + roots.push((parent.to_owned(), MAX_DEPTH)); + if let Some(grandparent) = parent.parent().filter(|path| *path != config.media_root) + && grandparent.starts_with(&config.media_root) + { + roots.push((grandparent.to_owned(), 2)); + } + } + + let mut queue = VecDeque::from(roots); + let mut visited = HashSet::new(); + let mut scanned = 0; + let mut candidates = Vec::new(); + while let Some((directory, depth)) = queue.pop_front() { + if !visited.insert(directory.clone()) { + continue; + } + let mut reader = match fs::read_dir(&directory).await { + Ok(reader) => reader, + Err(_) => continue, + }; + while scanned < MAX_ENTRIES { + let Some(entry) = reader.next_entry().await.map_err(AppError::internal)? else { + break; + }; + scanned += 1; + let file_type = entry.file_type().await.map_err(AppError::internal)?; + if file_type.is_symlink() { + continue; + } + let candidate_path = entry.path(); + if file_type.is_dir() && depth > 0 { + queue.push_back((candidate_path, depth - 1)); + } else if file_type.is_file() + && is_subtitle(&candidate_path) + && let Some(candidate) = make_candidate(config, video, candidate_path)? + { + candidates.push(candidate); + } + } + if scanned >= MAX_ENTRIES { + break; + } + } + candidates.sort_by(|left, right| { + right + .score + .cmp(&left.score) + .then_with(|| left.relative_path.cmp(&right.relative_path)) + }); + candidates.dedup_by(|left, right| left.path == right.path); + Ok(candidates) +} + +pub async fn resolve_id(config: &Config, video: &Path, id: &str) -> Result { + find(config, video) + .await? + .into_iter() + .find(|candidate| candidate.id == id) + .ok_or_else(|| AppError::not_found("external subtitle not found")) +} + +fn make_candidate( + config: &Config, + video: &Path, + subtitle: PathBuf, +) -> Result, AppError> { + let Some(matched) = matching::score(video, &subtitle) else { + return Ok(None); + }; + let relative_path = path::relative(config, &subtitle)?; + let codec = subtitle + .extension() + .and_then(|value| value.to_str()) + .unwrap_or("unknown") + .to_ascii_lowercase(); + Ok(Some(Candidate { + id: candidate_id(&relative_path), + path: subtitle, + relative_path, + codec, + label: matched.label, + score: matched.score, + confidence: matched.confidence, + })) +} + +fn candidate_id(relative_path: &str) -> String { + let mut hasher = DefaultHasher::new(); + relative_path.hash(&mut hasher); + format!("{:016x}", hasher.finish()) +} + +fn is_subtitle(path: &Path) -> bool { + path.extension() + .and_then(|value| value.to_str()) + .is_some_and(|value| SUBTITLE_EXTENSIONS.contains(&value.to_ascii_lowercase().as_str())) +} diff --git a/src/media/subtitles/matching.rs b/src/media/subtitles/matching.rs new file mode 100644 index 0000000..837a86d --- /dev/null +++ b/src/media/subtitles/matching.rs @@ -0,0 +1,237 @@ +use std::path::Path; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Confidence { + Certain, + Likely, + Ambiguous, +} + +impl Confidence { + pub fn as_str(self) -> &'static str { + match self { + Self::Certain => "certain", + Self::Likely => "likely", + Self::Ambiguous => "ambiguous", + } + } +} + +#[derive(Debug, Clone)] +pub struct Match { + pub score: u8, + pub confidence: Confidence, + pub label: String, +} + +pub fn score(video: &Path, subtitle: &Path) -> Option { + let video_stem = stem(video)?; + let subtitle_stem = stem(subtitle)?; + let video_name = Normalized::new(&video_stem); + let subtitle_name = Normalized::new(&subtitle_stem); + if video_name.tokens.is_empty() || subtitle_name.tokens.is_empty() { + return None; + } + + let exact = video_name.tokens == subtitle_name.tokens; + let contains_video = contains_sequence(&subtitle_name.tokens, &video_name.tokens); + let same_directory = video.parent() == subtitle.parent(); + let episode_video = video_name.episode(); + let episode_subtitle = subtitle_name.episode(); + + if !contains_video + && episode_video.is_some() + && episode_subtitle.is_some() + && episode_video != episode_subtitle + { + return None; + } + + let similarity = jaccard(&video_name.tokens, &subtitle_name.tokens); + let (score, confidence) = if exact { + (100, Confidence::Certain) + } else if contains_video { + (95, Confidence::Certain) + } else { + let mut value = (similarity * 55.0).round() as u8; + if episode_video.is_some() && episode_video == episode_subtitle { + value = value.saturating_add(30); + } + if same_directory { + value = value.saturating_add(10); + } + let confidence = if value >= 75 { + Confidence::Likely + } else { + Confidence::Ambiguous + }; + (value.min(94), confidence) + }; + if score < 40 { + return None; + } + + Some(Match { + score, + confidence, + label: candidate_label(video, subtitle, contains_video), + }) +} + +fn stem(path: &Path) -> Option { + path.file_stem() + .map(|value| value.to_string_lossy().into_owned()) +} + +struct Normalized { + tokens: Vec, +} + +impl Normalized { + fn new(value: &str) -> Self { + let mut tokens = Vec::new(); + let mut current = String::new(); + let mut digit = None; + for character in value.chars().flat_map(char::to_lowercase) { + if !character.is_alphanumeric() { + push_token(&mut tokens, &mut current); + digit = None; + continue; + } + let is_digit = character.is_numeric(); + if digit.is_some_and(|previous| previous != is_digit) { + push_token(&mut tokens, &mut current); + } + digit = Some(is_digit); + current.push(character); + } + push_token(&mut tokens, &mut current); + Self { tokens } + } + + fn episode(&self) -> Option { + for window in self.tokens.windows(4) { + if window[0] == "s" + && window[2] == "e" + && let Ok(value) = window[3].parse() + { + return Some(value); + } + } + let technical_boundary = self.tokens.iter().position(|token| { + token + .parse::() + .is_ok_and(|value| is_technical_number(value, None)) + }); + self.tokens + .iter() + .enumerate() + .take(technical_boundary.unwrap_or(self.tokens.len())) + .filter_map(|(index, token)| { + let value: u32 = token.parse().ok()?; + if is_technical_number(value, self.tokens.get(index.wrapping_sub(1))) { + None + } else { + Some(value) + } + }) + .next_back() + } +} + +fn push_token(tokens: &mut Vec, current: &mut String) { + if !current.is_empty() { + tokens.push(std::mem::take(current)); + } +} + +fn is_technical_number(value: u32, previous: Option<&String>) -> bool { + matches!( + value, + 240 | 264 | 265 | 360 | 480 | 576 | 720 | 1080 | 1440 | 2160 | 4320 + ) || previous.is_some_and(|token| matches!(token.as_str(), "x" | "h")) +} + +fn contains_sequence(haystack: &[String], needle: &[String]) -> bool { + needle.len() <= haystack.len() + && haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +fn jaccard(left: &[String], right: &[String]) -> f32 { + let common = left.iter().filter(|token| right.contains(token)).count(); + let union = left.len() + right.len() - common; + if union == 0 { + 0.0 + } else { + common as f32 / union as f32 + } +} + +fn candidate_label(video: &Path, subtitle: &Path, contains_video: bool) -> String { + let video_stem = stem(video).unwrap_or_default(); + let subtitle_stem = stem(subtitle).unwrap_or_default(); + let suffix = if contains_video { + subtitle_stem + .strip_prefix(&video_stem) + .unwrap_or("") + .trim_matches(|character: char| !character.is_alphanumeric()) + .to_owned() + } else { + String::new() + }; + let folder = (video.parent() != subtitle.parent()) + .then(|| subtitle.parent()?.file_name()?.to_str().map(str::to_owned)) + .flatten(); + match (folder, suffix.is_empty()) { + (Some(folder), false) => format!("{folder} · {suffix}"), + (Some(folder), true) => folder, + (None, false) => suffix, + (None, true) => "External".to_owned(), + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::{Confidence, score}; + + #[test] + fn exact_stems_are_certain() { + let result = score( + Path::new("05. Famous Shooter.avi"), + Path::new("05. Famous Shooter.ass"), + ) + .unwrap(); + assert_eq!(result.confidence, Confidence::Certain); + assert_eq!(result.score, 100); + } + + #[test] + fn release_suffixes_are_certain_and_become_labels() { + let result = score( + Path::new("Show - 01 [1080p WEB-DL].mkv"), + Path::new("Show - 01 [1080p WEB-DL]-RELEASE2.ass"), + ) + .unwrap(); + assert_eq!(result.confidence, Confidence::Certain); + assert_eq!(result.label, "RELEASE2"); + } + + #[test] + fn rejects_a_neighboring_episode() { + assert!(score(Path::new("Show - 01.mkv"), Path::new("Show - 02.ass")).is_none()); + } + + #[test] + fn folder_names_are_labels_not_languages() { + let result = score( + Path::new("Show/01.mkv"), + Path::new("Show/SUBS/Variant A/01.srt"), + ) + .unwrap(); + assert_eq!(result.label, "Variant A"); + } +} diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 0000000..873c96d --- /dev/null +++ b/src/web.rs @@ -0,0 +1,82 @@ +use axum::{ + body::Body, + http::header, + response::{Html, IntoResponse}, +}; + +pub async fn index() -> impl IntoResponse { + ( + [(header::CACHE_CONTROL, "no-store")], + Html(include_str!("../web/index.html")), + ) +} +pub async fn alpine() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/javascript; charset=utf-8")], + include_str!("../web/alpine.min.js"), + ) +} +pub async fn dash() -> impl IntoResponse { + javascript(include_str!("../web/dash.all.min.js")) +} +pub async fn dash_source_map() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "application/json; charset=utf-8")], + r#"{"version":3,"sources":[],"names":[],"mappings":""}"#, + ) +} +pub async fn controlbar_javascript() -> impl IntoResponse { + javascript(include_str!("../web/controlbar.min.js")) +} +pub async fn api_javascript() -> impl IntoResponse { + javascript(include_str!("../web/js/api.js")) +} +pub async fn library_javascript() -> impl IntoResponse { + javascript(include_str!("../web/js/library.js")) +} +pub async fn location_javascript() -> impl IntoResponse { + javascript(include_str!("../web/js/location.js")) +} +pub async fn preferences_javascript() -> impl IntoResponse { + javascript(include_str!("../web/js/preferences.js")) +} +pub async fn player_javascript() -> impl IntoResponse { + javascript(include_str!("../web/js/player.js")) +} +fn javascript(source: &'static str) -> impl IntoResponse { + ( + [ + (header::CONTENT_TYPE, "text/javascript; charset=utf-8"), + (header::CACHE_CONTROL, "no-store"), + ], + source, + ) +} +pub async fn styles() -> impl IntoResponse { + stylesheet(include_str!("../web/styles.css")) +} +pub async fn controlbar_styles() -> impl IntoResponse { + stylesheet(include_str!("../web/vendor/controlbar/controlbar.css")) +} +pub async fn bootstrap_icon_styles() -> impl IntoResponse { + stylesheet(include_str!( + "../web/vendor/bootstrap-icons/bootstrap-icons.min.css" + )) +} +pub async fn bootstrap_icon_font() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "font/woff2")], + Body::from( + include_bytes!("../web/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2").as_slice(), + ), + ) +} +fn stylesheet(source: &'static str) -> impl IntoResponse { + ( + [ + (header::CONTENT_TYPE, "text/css; charset=utf-8"), + (header::CACHE_CONTROL, "no-store"), + ], + source, + ) +} diff --git a/web/alpine.min.js b/web/alpine.min.js new file mode 100644 index 0000000..ab371ef --- /dev/null +++ b/web/alpine.min.js @@ -0,0 +1,5 @@ +(()=>{var ee=!1,re=!1,W=[],ne=-1,ie=!1;function Ve(t){Dn(t)}function Ue(){ie=!0}function qe(){ie=!1,We()}function Dn(t){W.includes(t)||W.push(t),We()}function Ke(t){let e=W.indexOf(t);e!==-1&&e>ne&&W.splice(e,1)}function We(){if(!re&&!ee){if(ie)return;ee=!0,queueMicrotask(In)}}function In(){ee=!1,re=!0;for(let t=0;tt.effect(e,{scheduler:r=>{oe?Ve(r):r()}}),se=t.raw}function ae(t){R=t}function Ye(t){let e=()=>{};return[n=>{let i=R(n);return t._x_effects||(t._x_effects=new Set,t._x_runEffects=()=>{t._x_effects.forEach(o=>o())}),t._x_effects.add(i),e=()=>{i!==void 0&&(t._x_effects.delete(i),j(i))},i},()=>{e()}]}function St(t,e){let r=!0,n,i,o=R(()=>{let s=t(),a=JSON.stringify(s);if(!r&&(typeof s=="object"||s!==n)){let c=typeof n=="object"?JSON.parse(i):n;queueMicrotask(()=>{e(s,c)})}n=s,i=a,r=!1});return()=>j(o)}async function Xe(t){Ue();try{await t(),await Promise.resolve()}finally{qe()}}var Ze=[],Qe=[],tr=[];function er(t){tr.push(t)}function et(t,e){typeof e=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(e)):(e=t,Qe.push(e))}function At(t){Ze.push(t)}function Ot(t,e,r){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[e]||(t._x_attributeCleanups[e]=[]),t._x_attributeCleanups[e].push(r)}function ce(t,e){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([r,n])=>{(e===void 0||e.includes(r))&&(n.forEach(i=>i()),delete t._x_attributeCleanups[r])})}function rr(t){for(t._x_effects?.forEach(Ke);t._x_cleanups?.length;)t._x_cleanups.pop()()}var le=new MutationObserver(pe),ue=!1;function ut(){le.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ue=!0}function fe(){kn(),le.disconnect(),ue=!1}var lt=[];function kn(){let t=le.takeRecords();lt.push(()=>t.length>0&&pe(t));let e=lt.length;queueMicrotask(()=>{if(lt.length===e)for(;lt.length>0;)lt.shift()()})}function m(t){if(!ue)return t();fe();let e=t();return ut(),e}var de=!1,vt=[];function nr(){de=!0}function ir(){de=!1,pe(vt),vt=[]}function pe(t){if(de){vt=vt.concat(t);return}let e=[],r=new Set,n=new Map,i=new Map;for(let o=0;o{s.nodeType===1&&s._x_marker&&r.add(s)}),t[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||e.push(s)}})),t[o].type==="attributes")){let s=t[o].target,a=t[o].attributeName,c=t[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ce(s,o)}),n.forEach((o,s)=>{Ze.forEach(a=>a(s,o))});for(let o of r)e.some(s=>s.contains(o))||Qe.forEach(s=>s(o));for(let o of e)o.isConnected&&tr.forEach(s=>s(o));e=null,r=null,n=null,i=null}function Ct(t){return P(F(t))}function N(t,e,r){return t._x_dataStack=[e,...F(r||t)],()=>{t._x_dataStack=t._x_dataStack.filter(n=>n!==e)}}function F(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?F(t.host):t.parentNode?F(t.parentNode):[]}function P(t){return new Proxy({objects:t},$n)}function or(t,e){return t===null||t===Object.prototype?null:Object.prototype.hasOwnProperty.call(t,e)?t:or(Object.getPrototypeOf(t),e)}var $n={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(e=>Object.keys(e))))},has({objects:t},e){return e==Symbol.unscopables?!1:t.some(r=>Object.prototype.hasOwnProperty.call(r,e)||Reflect.has(r,e))},get({objects:t},e,r){return e=="toJSON"?Ln:Reflect.get(t.find(n=>Reflect.has(n,e))||{},e,r)},set({objects:t},e,r,n){let i;for(let s of t)if(i=or(s,e),i)break;i||(i=t[t.length-1]);let o=Object.getOwnPropertyDescriptor(i,e);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,e,r)}};function Ln(){return Reflect.ownKeys(this).reduce((e,r)=>(e[r]=Reflect.get(this,r),e),{})}function rt(t){let e=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(t,c,o):e(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(t)}function Tt(t,e=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return t(this.initialValue,()=>jn(n,i),s=>me(n,i,s),i,o)}};return e(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function jn(t,e){return e.split(".").reduce((r,n)=>r[n],t)}function me(t,e,r){if(typeof e=="string"&&(e=e.split(".")),e.length===1)t[e[0]]=r;else{if(e.length===0)throw error;return t[e[0]]||(t[e[0]]={}),me(t[e[0]],e.slice(1),r)}}var sr={};function x(t,e){sr[t]=e}function H(t,e){let r=Fn(e);return Object.entries(sr).forEach(([n,i])=>{Object.defineProperty(t,`$${n}`,{get(){return i(e,r)},enumerable:!1})}),t}function Fn(t){let[e,r]=he(t),n={interceptor:Tt,...e};return et(t,r),n}function ar(t,e,r,...n){try{return r(...n)}catch(i){nt(i,t,e)}}function nt(...t){return cr(...t)}var cr=Bn;function lr(t){cr=t}function Bn(t,e,r=void 0){t=Object.assign(t??{message:"No error message given."},{el:e,expression:r}),console.warn(`Alpine Expression Error: ${t.message} + +${r?'Expression: "'+r+`" + +`:""}`,e),setTimeout(()=>{throw t},0)}var it=!0;function Mt(t){let e=it;it=!1;let r=t();return it=e,r}function T(t,e,r={}){let n;return _(t,e)(i=>n=i,r),n}function _(...t){return ur(...t)}var ur=()=>{};function fr(t){ur=t}var dr;function pr(t){dr=t}function mr(t,e){let r={};H(r,t);let n=[r,...F(t)],i=typeof e=="function"?zn(n,e):Vn(n,e,t);return ar.bind(null,t,e,i)}function zn(t,e){return(r=()=>{},{scope:n={},params:i=[],context:o}={})=>{if(!it){ft(r,e,P([n,...t]),i);return}let s=e.apply(P([n,...t]),i);ft(r,s)}}var _e={};function Hn(t,e){if(_e[t])return _e[t];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${t}`}),s}catch(s){return nt(s,e,t),Promise.resolve()}})();return _e[t]=o,o}function Vn(t,e,r){let n=Hn(e,r);return(i=()=>{},{scope:o={},params:s=[],context:a}={})=>{n.result=void 0,n.finished=!1;let c=P([o,...t]);if(typeof n=="function"){let l=n.call(a,n,c).catch(u=>nt(u,r,e));n.finished?(ft(i,n.result,c,s,r),n.result=void 0):l.then(u=>{ft(i,u,c,s,r)}).catch(u=>nt(u,r,e)).finally(()=>n.result=void 0)}}}function ft(t,e,r,n,i){if(it&&typeof e=="function"){let o=e.apply(r,n);o instanceof Promise?o.then(s=>ft(t,s,r,n)).catch(s=>nt(s,i,e)):t(o)}else typeof e=="object"&&e instanceof Promise?e.then(o=>t(o)):t(e)}function hr(...t){return dr(...t)}function _r(t,e,r={}){let n={};H(n,t);let i=[n,...F(t)],o=P([r.scope??{},...i]),s=r.params??[];if(e.includes("await")){let a=Object.getPrototypeOf(async function(){}).constructor,c=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e;return new a(["scope"],`with (scope) { let __result = ${c}; return __result }`).call(r.context,o)}else{let a=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(()=>{ ${e} })()`:e,l=new Function(["scope"],`with (scope) { let __result = ${a}; return __result }`).call(r.context,o);return typeof l=="function"&&it?l.apply(o,s):l}}var ye="x-";function O(t=""){return ye+t}function gr(t){ye=t}var Rt={};function p(t,e){return Rt[t]=e,{before(r){if(!Rt[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${t}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,t)}}}function xr(t){return Object.keys(Rt).includes(t)}function pt(t,e,r){if(e=Array.from(e),t._x_virtualDirectives){let o=Object.entries(t._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=be(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),e=e.concat(o)}let n={};return e.map(wr((o,s)=>n[o]=s)).filter(Sr).map(qn(n,r)).sort(Kn).map(o=>Un(t,o))}function be(t){return Array.from(t).map(wr()).filter(e=>!Sr(e))}var ge=!1,dt=new Map,yr=Symbol();function br(t){ge=!0;let e=Symbol();yr=e,dt.set(e,[]);let r=()=>{for(;dt.get(e).length;)dt.get(e).shift()();dt.delete(e)},n=()=>{ge=!1,r()};t(r),n()}function he(t){let e=[],r=a=>e.push(a),[n,i]=Ye(t);return e.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:_.bind(_,t),evaluate:T.bind(T,t)},()=>e.forEach(a=>a())]}function Un(t,e){let r=()=>{},n=Rt[e.type]||r,[i,o]=he(t);Ot(t,e.original,o);let s=()=>{t._x_ignore||t._x_ignoreSelf||(n.inline&&n.inline(t,e,i),n=n.bind(n,t,e,i),ge?dt.get(yr).push(n):n())};return s.runCleanups=o,s}var Nt=(t,e)=>({name:r,value:n})=>(r.startsWith(t)&&(r=r.replace(t,e)),{name:r,value:n}),Pt=t=>t;function wr(t=()=>{}){return({name:e,value:r})=>{let{name:n,value:i}=Er.reduce((o,s)=>s(o),{name:e,value:r});return n!==e&&t(n,e),{name:n,value:i}}}var Er=[];function ot(t){Er.push(t)}function Sr({name:t}){return vr().test(t)}var vr=()=>new RegExp(`^${ye}([^:^.]+)\\b`);function qn(t,e){return({name:r,value:n})=>{r===n&&(n="");let i=r.match(vr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=e||t[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var xe="DEFAULT",G=["ignore","ref","id","data","anchor","bind","init","for","model","modelable","transition","show","if",xe,"teleport"];function Kn(t,e){let r=G.indexOf(t.type)===-1?xe:t.type,n=G.indexOf(e.type)===-1?xe:e.type;return G.indexOf(r)-G.indexOf(n)}function J(t,e,r={},n={}){return t.dispatchEvent(new CustomEvent(e,{detail:r,bubbles:!0,composed:!0,cancelable:!0,...n}))}function D(t,e){if(typeof ShadowRoot=="function"&&t instanceof ShadowRoot){Array.from(t.children).forEach(i=>D(i,e));return}let r=!1;if(e(t,()=>r=!0),r)return;let n=t.firstElementChild;for(;n;)D(n,e,!1),n=n.nextElementSibling}function E(t,...e){console.warn(`Alpine Warning: ${t}`,...e)}var Ar=!1;function Or(){Ar&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Ar=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` + + + + + + + + + +
+

Lan Play

+
+ + + +
+
+
+

Loading…

+

+
+ + +
+
+ + + + + + +
PreviewNameType
📁..Folder
+
+
+
+ +
+
+ +
+ +
+
+ + +
+
+
+ + diff --git a/web/js/api.js b/web/js/api.js new file mode 100644 index 0000000..afb93df --- /dev/null +++ b/web/js/api.js @@ -0,0 +1,31 @@ +(function () { + async function request(url, options) { + const response = await fetch(url, options); + if (!response.ok) { + let message = `HTTP ${response.status}`; + try { message = (await response.json()).error || message; } catch (_) {} + throw new Error(message); + } + return response; + } + + window.LanPlayApi = { + async browse(path) { + return (await request(`/api/browse?path=${encodeURIComponent(path)}`)).json(); + }, + + async info() { + return (await request('/api/info')).json(); + }, + async startDash(path, startSeconds = 0) { + const query = `path=${encodeURIComponent(path)}&start=${encodeURIComponent(startSeconds)}`; + return (await request(`/api/dash?${query}`, { method: 'POST' })).json(); + }, + async stopDash(sessionId) { + await request(`/api/dash/${sessionId}`, { method: 'DELETE' }); + }, + async subtitles(path) { + return (await request(`/api/subtitles?path=${encodeURIComponent(path)}`)).json(); + } + }; +})(); diff --git a/web/js/library.js b/web/js/library.js new file mode 100644 index 0000000..ea703a2 --- /dev/null +++ b/web/js/library.js @@ -0,0 +1,262 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('library', () => ({ + currentPath: '', parentPath: null, entries: [], selected: null, + loading: false, playerLoading: false, playerLoadingCompact: false, + error: '', playerController: null, viewMode: 'grid', + infoOpen: false, infoLoading: false, infoError: '', diagnostics: null, + previewFrames: {}, previewUrls: {}, previewStates: {}, previewTimers: {}, + previewActive: {}, previewImages: {}, previewGeneration: 0, + + init() { + try { this.viewMode = localStorage.getItem('lan-play:view') || 'grid'; } catch (_) {} + this.locationHandler = () => { this.restoreLocation(); }; + this.resizeHandler = () => { this.scheduleMarqueeCheck(); }; + window.addEventListener('popstate', this.locationHandler); + window.addEventListener('resize', this.resizeHandler); + }, + + destroy() { + this.stopAllPreviews(); + this.cancelPreviewLoads(); + window.removeEventListener('popstate', this.locationHandler); + window.removeEventListener('resize', this.resizeHandler); + if (this.marqueeFrame) window.cancelAnimationFrame(this.marqueeFrame); + if (this.playerController) this.playerController.dispose(); + }, + + async restoreLocation() { + const location = window.LanPlayLocation.read(); + if (this.playerController) await this.playerController.stop(); + this.selected = null; + await this.browse(location.path, false); + if (location.play) { + await this.play({ + path: location.play, + name: window.LanPlayLocation.fileName(location.play), + kind: 'media' + }, false); + } + }, + + async browse(path, updateLocation = true) { + this.stopAllPreviews(); + this.cancelPreviewLoads(); + this.loading = true; + this.error = ''; + try { + const data = await window.LanPlayApi.browse(path); + this.currentPath = data.path; + this.parentPath = data.parent; + this.entries = data.entries; + if (updateLocation) window.LanPlayLocation.write(data.path, null, false); + await this.$nextTick(); + this.preloadPreviews(data.entries); + this.scheduleMarqueeCheck(); + } catch (error) { this.error = error.message; } + finally { this.loading = false; } + }, + + async play(entry, updateLocation = true) { + this.stopAllPreviews(); + this.enterFullscreen(); + this.error = ''; + this.selected = entry; + this.playerLoading = true; + if (updateLocation) window.LanPlayLocation.write(this.currentPath, entry.path, false); + await this.$nextTick(); + if (!this.playerController) { + this.playerController = new window.LanPlayPlayer( + this.$refs.video, + this.$refs.videoWrapper, + (message) => { this.error = message; }, + (loading, compact = false) => { + this.playerLoading = loading; + this.playerLoadingCompact = loading && compact; + }, + () => { this.playNext(); } + ); + } + this.playerController.setNextAvailable(Boolean(this.nextEntry())); + try { await this.playerController.play(entry); } + catch (error) { this.error = error.message; } + finally { + this.playerLoading = false; + this.playerLoadingCompact = false; + } + }, + + nextEntry() { + if (!this.selected) return null; + const media = this.entries.filter((entry) => entry.kind === 'media'); + const index = media.findIndex((entry) => entry.path === this.selected.path); + return index >= 0 ? media[index + 1] || null : null; + }, + + async playNext() { + const next = this.nextEntry(); + if (next) await this.play(next); + }, + + mediaTitle() { + if (!this.selected || !this.selected.name) return ''; + return this.selected.name.replace(/\.[^.]+$/, ''); + }, + + setViewMode(mode) { + this.viewMode = mode === 'table' ? 'table' : 'grid'; + try { localStorage.setItem('lan-play:view', this.viewMode); } catch (_) {} + this.$nextTick(() => this.scheduleMarqueeCheck()); + }, + + scheduleMarqueeCheck() { + if (this.marqueeFrame) window.cancelAnimationFrame(this.marqueeFrame); + this.marqueeFrame = window.requestAnimationFrame(() => { + this.marqueeFrame = null; + document.querySelectorAll('.entry-name').forEach((container) => { + if (container.clientWidth <= 0) return; + const text = container.querySelector('.entry-name-track'); + container.classList.toggle('is-overflowing', Boolean(text) && text.scrollWidth > container.clientWidth + 1); + }); + }); + }, + + openEntry(entry) { + return entry.kind === 'directory' ? this.browse(entry.path) : this.play(entry); + }, + + async openInfo() { + this.infoOpen = true; + this.infoLoading = true; + this.infoError = ''; + try { + const server = await window.LanPlayApi.info(); + let dashVersion = 'Unknown'; + try { dashVersion = dashjs.MediaPlayer().create().getVersion(); } catch (_) {} + this.diagnostics = { + ...server, + interface: '20', + dashjs: dashVersion, + browser: navigator.userAgent, + display: `${window.screen.width}×${window.screen.height} @ ${window.devicePixelRatio || 1}x`, + fullscreen: Boolean(document.documentElement.requestFullscreen || document.documentElement.webkitRequestFullscreen), + mediaSource: 'MediaSource' in window + }; + } catch (error) { + this.infoError = error.message; + } finally { + this.infoLoading = false; + } + }, + + closeInfo() { + this.infoOpen = false; + }, + + previewStyle(entry) { + if (this.previewStates[entry.path] !== 'ready') return ''; + const url = this.previewUrls[entry.path]; + if (!url) return ''; + const frame = this.previewFrames[entry.path] || 0; + const column = frame % 5; + const row = Math.floor(frame / 5); + return `background-image:url("${url}");background-position:${column * 25}% ${row * 100}%`; + }, + + startPreview(entry) { + if (!entry || entry.kind !== 'media') return; + this.previewActive[entry.path] = true; + this.stopPreview(entry); + this.previewActive[entry.path] = true; + if (this.previewStates[entry.path] !== 'ready') return; + this.previewFrames[entry.path] = 0; + this.previewTimers[entry.path] = window.setInterval(() => { + this.previewFrames[entry.path] = ((this.previewFrames[entry.path] || 0) + 1) % 10; + }, 500); + }, + + stopPreview(entry) { + if (!entry) return; + this.previewActive[entry.path] = false; + const timer = this.previewTimers[entry.path]; + if (timer) window.clearInterval(timer); + delete this.previewTimers[entry.path]; + this.previewFrames[entry.path] = 0; + }, + + stopAllPreviews() { + Object.values(this.previewTimers).forEach((timer) => window.clearInterval(timer)); + this.previewTimers = {}; + this.previewActive = {}; + }, + + preloadPreviews(entries) { + const generation = this.previewGeneration; + entries.filter((entry) => entry.kind === 'media').forEach((entry) => { + const path = entry.path; + const url = `/api/preview?path=${encodeURIComponent(path)}`; + const image = new Image(); + this.previewUrls[path] = url; + this.previewStates[path] = 'loading'; + this.previewImages[path] = image; + image.onload = () => { + if (generation !== this.previewGeneration) return; + this.previewStates[path] = 'ready'; + delete this.previewImages[path]; + if (this.previewActive[path]) this.startPreview(entry); + }; + image.onerror = () => { + if (generation !== this.previewGeneration) return; + this.previewStates[path] = 'error'; + delete this.previewImages[path]; + }; + image.src = url; + }); + }, + + cancelPreviewLoads() { + this.previewGeneration += 1; + Object.values(this.previewImages).forEach((image) => { + image.onload = null; + image.onerror = null; + image.src = ''; + }); + this.previewImages = {}; + this.previewFrames = {}; + this.previewUrls = {}; + this.previewStates = {}; + }, + + enterFullscreen() { + if (document.fullscreenElement || document.webkitFullscreenElement) return; + const root = document.documentElement; + const request = root.requestFullscreen || root.webkitRequestFullscreen; + if (request) { + try { + const result = request.call(root); + if (result && result.catch) result.catch(() => {}); + } catch (_) {} + } + }, + + exitFullscreen() { + if (!document.fullscreenElement && !document.webkitFullscreenElement) return; + const exit = document.exitFullscreen || document.webkitExitFullscreen; + if (exit) { + try { + const result = exit.call(document); + if (result && result.catch) result.catch(() => {}); + } catch (_) {} + } + }, + + async closePlayer(updateLocation = true) { + this.exitFullscreen(); + this.selected = null; + this.playerLoading = false; + this.playerLoadingCompact = false; + if (updateLocation) window.LanPlayLocation.write(this.currentPath, null, false); + await this.$nextTick(); + if (this.playerController) this.playerController.stop(); + } + })); +}); diff --git a/web/js/location.js b/web/js/location.js new file mode 100644 index 0000000..9f6db57 --- /dev/null +++ b/web/js/location.js @@ -0,0 +1,24 @@ +(function () { + function read() { + const params = new URLSearchParams(window.location.search); + return { + path: params.get('path') || '', + play: params.get('play') || null + }; + } + + function write(path, play, replace) { + const url = new URL(window.location.href); + path ? url.searchParams.set('path', path) : url.searchParams.delete('path'); + play ? url.searchParams.set('play', play) : url.searchParams.delete('play'); + const next = `${url.pathname}${url.search}`; + window.history[replace ? 'replaceState' : 'pushState']({}, '', next); + } + + function fileName(path) { + const parts = path.split('/'); + return parts[parts.length - 1] || path; + } + + window.LanPlayLocation = { read, write, fileName }; +})(); diff --git a/web/js/player.js b/web/js/player.js new file mode 100644 index 0000000..7a8e143 --- /dev/null +++ b/web/js/player.js @@ -0,0 +1,265 @@ +(function () { + class LanPlayPlayer { + constructor(video, wrapper, onError, onLoading, onNext) { + this.video = video; + this.wrapper = wrapper; + this.onError = onError; + this.onLoading = onLoading; + this.onNext = onNext; + this.dashPlayer = null; + this.controlBar = null; + this.sessionId = null; + this.mediaPath = null; + this.dashPreferencesApplied = false; + this.currentEntry = null; + this.nextAvailable = false; + this.restarting = false; + this.operation = 0; + this.freezeFrame = null; + this.playingHandler = null; + this.subtitleUrls = []; + } + + async play(entry, startSeconds = 0, preserveFrame = false) { + const framePreserved = preserveFrame && this.captureFrame(); + this.onLoading(true, framePreserved); + this.stop({ preserveFrame: framePreserved }); + const operation = ++this.operation; + this.currentEntry = entry; + this.mediaPath = entry.path; + this.dashPreferencesApplied = false; + const session = await window.LanPlayApi.startDash(entry.path, startSeconds); + if (operation !== this.operation) { + window.LanPlayApi.stopDash(session.session_id).catch(() => {}); + return; + } + this.sessionId = session.session_id; + this.dashPlayer = dashjs.MediaPlayer().create(); + this.dashPlayer.updateSettings({ + streaming: { + buffer: { + bufferTimeDefault: 8, + bufferTimeAtTopQuality: 8, + bufferTimeAtTopQualityLongForm: 8 + } + } + }); + this.dashPlayer.initialize(this.video, null, true); + await this.addExternalSubtitles(entry.path, session.start_seconds, operation); + if (operation !== this.operation) return; + this.controlBar = new DashControlBarModule.ControlBar(this.dashPlayer, this.video); + this.controlBar.init(this.wrapper); + this.controlBar.setVodDuration(session.duration_seconds, session.start_seconds); + if (typeof this.controlBar.onSeekRequested === 'function') { + this.controlBar.onSeekRequested((time) => this.seek(time)); + } + if (typeof this.controlBar.onNextRequested === 'function') { + this.controlBar.onNextRequested(() => this.onNext()); + } + if (typeof this.controlBar.setNextAvailable === 'function') { + this.controlBar.setNextAvailable(this.nextAvailable); + } + this.controlBar.onTrackSelected((type, track) => this.rememberTrack(type, track)); + this.controlBar.disable(); + const controlBar = this.controlBar; + this.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (event) => { + if (operation !== this.operation) return; + const message = event && event.error && event.error.message; + this.onError(message || 'DASH playback error'); + }); + this.dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { + if (operation !== this.operation || this.controlBar !== controlBar) return; + this.applyDashPreferences(); + controlBar.enable(); + controlBar.refreshTracks(); + }); + this.playingHandler = () => { + if (operation !== this.operation) return; + this.playingHandler = null; + this.clearFreezeFrame(); + this.onLoading(false, false); + }; + this.video.addEventListener('playing', this.playingHandler, { once: true }); + this.dashPlayer.attachSource(session.manifest_url); + if (!framePreserved) this.onLoading(false, false); + } + + async seek(time) { + if (this.restarting || !this.currentEntry) return; + this.restarting = true; + const entry = this.currentEntry; + try { await this.play(entry, time, true); } + catch (error) { + this.onError(error.message); + this.clearFreezeFrame(); + this.onLoading(false, false); + } finally { this.restarting = false; } + } + + setNextAvailable(available) { + this.nextAvailable = Boolean(available); + if (this.controlBar && typeof this.controlBar.setNextAvailable === 'function') { + this.controlBar.setNextAvailable(this.nextAvailable); + } + } + + applyDashPreferences() { + if (this.dashPreferencesApplied || !this.mediaPath) return; + ['audio', 'video'].forEach((type) => { + const tracks = this.dashPlayer.getTracksFor(type) || []; + const preferred = window.LanPlayPreferences.find(this.mediaPath, type, tracks); + if (preferred) this.dashPlayer.setCurrentTrack(preferred); + }); + const textTracks = this.dashPlayer.getTracksFor('text') || []; + const preferredText = window.LanPlayPreferences.find(this.mediaPath, 'subtitle', textTracks); + if (preferredText === null) { + this.dashPlayer.setTextTrack(-1); + } else if (preferredText) { + const index = textTracks.indexOf(preferredText); + if (index >= 0) this.dashPlayer.setTextTrack(index); + } + this.dashPreferencesApplied = true; + } + + rememberTrack(type, track) { + if (!this.mediaPath) return; + if (type === 'nativeText') { + const tracks = Array.from(this.video.textTracks || []); + window.LanPlayPreferences.remember(this.mediaPath, 'subtitle', track, tracks.indexOf(track)); + return; + } + const preferenceType = type === 'text' ? 'subtitle' : type; + const tracks = this.dashPlayer.getTracksFor(type) || []; + window.LanPlayPreferences.remember(this.mediaPath, preferenceType, track, tracks.indexOf(track)); + } + + async addExternalSubtitles(path, startSeconds = 0, operation = this.operation) { + let subtitles = []; + try { subtitles = await window.LanPlayApi.subtitles(path); } catch (_) { return; } + if (operation !== this.operation) return; + const loaded = await Promise.all(subtitles.filter((item) => item.supported).map(async (subtitle, ordinal) => { + const query = new URLSearchParams({ path, start: String(startSeconds) }); + const url = subtitle.source === 'external' + ? `/api/subtitles/external/${encodeURIComponent(subtitle.id)}?${query}` + : `/api/subtitles/${subtitle.index}?${query}`; + try { + const response = await fetch(url); + if (!response.ok) return null; + const contents = await response.text(); + const objectUrl = URL.createObjectURL(new Blob([contents], { type: 'text/vtt' })); + return { subtitle, ordinal, objectUrl }; + } catch (_) { + return null; + } + })); + if (operation !== this.operation) { + loaded.filter(Boolean).forEach((item) => URL.revokeObjectURL(item.objectUrl)); + return; + } + loaded.filter(Boolean).forEach(({ subtitle, ordinal, objectUrl }) => { + const label = subtitle.title || subtitle.language || `Subtitle ${ordinal + 1}`; + const idPart = subtitle.source === 'external' ? subtitle.id : subtitle.index; + this.subtitleUrls.push(objectUrl); + this.dashPlayer.addExternalSubtitle(new dashjs.ExternalSubtitle({ + id: `lan-play-${subtitle.source}-${idPart}`, + url: objectUrl, + language: label, + mimeType: 'text/vtt', + bandwidth: 256, + periodId: '0' + })); + }); + } + + captureFrame() { + if (this.video.readyState < 2 || !this.video.videoWidth || !this.video.videoHeight) return false; + try { + this.clearFreezeFrame(); + const canvas = document.createElement('canvas'); + const width = Math.max(1, this.wrapper.clientWidth); + const height = Math.max(1, this.wrapper.clientHeight); + canvas.width = Math.min(width, 1920); + canvas.height = Math.min(height, 1080); + canvas.className = 'player-freeze-frame'; + const context = canvas.getContext('2d'); + context.fillStyle = '#000'; + context.fillRect(0, 0, canvas.width, canvas.height); + const scale = Math.min(canvas.width / this.video.videoWidth, canvas.height / this.video.videoHeight); + const drawWidth = this.video.videoWidth * scale; + const drawHeight = this.video.videoHeight * scale; + context.drawImage( + this.video, + (canvas.width - drawWidth) / 2, + (canvas.height - drawHeight) / 2, + drawWidth, + drawHeight + ); + this.wrapper.appendChild(canvas); + this.freezeFrame = canvas; + return true; + } catch (_) { + this.clearFreezeFrame(); + return false; + } + } + + clearFreezeFrame() { + if (this.freezeFrame) this.freezeFrame.remove(); + this.freezeFrame = null; + } + + stop(options = {}) { + this.operation += 1; + if (this.playingHandler) { + this.video.removeEventListener('playing', this.playingHandler); + this.playingHandler = null; + } + if (!options.preserveFrame) this.clearFreezeFrame(); + this.revokeSubtitleUrls(); + if (this.controlBar) { + this.controlBar.destroy(); + this.controlBar = null; + } + if (this.dashPlayer) { + this.dashPlayer.destroy(); + this.dashPlayer = null; + } + this.video.pause(); + this.video.removeAttribute('src'); + while (this.video.firstChild) this.video.removeChild(this.video.firstChild); + this.video.load(); + this.mediaPath = null; + this.dashPreferencesApplied = false; + this.currentEntry = null; + if (this.sessionId) { + const sessionId = this.sessionId; + this.sessionId = null; + window.LanPlayApi.stopDash(sessionId).catch(() => {}); + } + } + + dispose() { + this.operation += 1; + if (this.playingHandler) this.video.removeEventListener('playing', this.playingHandler); + this.playingHandler = null; + this.clearFreezeFrame(); + this.revokeSubtitleUrls(); + if (this.controlBar) this.controlBar.destroy(); + if (this.dashPlayer) this.dashPlayer.destroy(); + if (this.sessionId) { + fetch(`/api/dash/${this.sessionId}`, { method: 'DELETE', keepalive: true }).catch(() => {}); + } + this.controlBar = null; + this.dashPlayer = null; + this.sessionId = null; + this.currentEntry = null; + } + + revokeSubtitleUrls() { + this.subtitleUrls.forEach((url) => URL.revokeObjectURL(url)); + this.subtitleUrls = []; + } + } + + window.LanPlayPlayer = LanPlayPlayer; +})(); diff --git a/web/js/preferences.js b/web/js/preferences.js new file mode 100644 index 0000000..9d6df41 --- /dev/null +++ b/web/js/preferences.js @@ -0,0 +1,63 @@ +(function () { + const prefix = 'lan-play:release:v1:'; + + function folder(path) { + const index = path.lastIndexOf('/'); + return index < 0 ? '' : path.slice(0, index); + } + + function key(path) { + return `${prefix}${folder(path)}`; + } + + function load(path) { + try { return JSON.parse(localStorage.getItem(key(path))) || {}; } + catch (_) { return {}; } + } + + function store(path, value) { + try { localStorage.setItem(key(path), JSON.stringify(value)); } catch (_) {} + } + + function label(track) { + const labels = track && track.labels || []; + const selected = labels.find((item) => item.text) || labels[0]; + return selected && selected.text || track && track.label || ''; + } + + function descriptor(track, ordinal) { + if (!track) return { off: true }; + return { + off: false, + lang: track.lang || track.language || '', + label: label(track), + roles: (track.roles || []).map((role) => role.value || role).filter(Boolean).sort(), + ordinal + }; + } + + function remember(path, type, track, ordinal) { + const profile = load(path); + profile[type] = descriptor(track, ordinal); + store(path, profile); + } + + function find(path, type, tracks) { + const wanted = load(path)[type]; + if (!wanted) return undefined; + if (wanted.off) return null; + const described = tracks.map((track, ordinal) => ({ track, ordinal, value: descriptor(track, ordinal) })); + let matches = described.filter((item) => wanted.lang && item.value.lang === wanted.lang); + if (wanted.label) { + const labeled = matches.filter((item) => item.value.label === wanted.label); + if (labeled.length) matches = labeled; + } + if (!matches.length && wanted.label) { + matches = described.filter((item) => item.value.label === wanted.label); + } + if (!matches.length && !wanted.lang && !wanted.label) matches = described; + return (matches.find((item) => item.ordinal === wanted.ordinal) || matches[0] || {}).track; + } + + window.LanPlayPreferences = { folder, load, remember, find }; +})(); diff --git a/web/js/preferences.test.js b/web/js/preferences.test.js new file mode 100644 index 0000000..1c89539 --- /dev/null +++ b/web/js/preferences.test.js @@ -0,0 +1,36 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const values = new Map(); +global.window = global; +global.localStorage = { + getItem: (key) => values.get(key) || null, + setItem: (key, value) => values.set(key, value) +}; +require('./preferences.js'); + +test('restores a matching track for the next file in the same folder', () => { + const firstEpisode = 'Anime/Show/episode-01.mkv'; + const nextEpisode = 'Anime/Show/episode-02.mkv'; + const tracks = [{ lang: 'en' }, { lang: 'ja' }]; + LanPlayPreferences.remember(firstEpisode, 'audio', tracks[1], 1); + assert.equal(LanPlayPreferences.find(nextEpisode, 'audio', tracks), tracks[1]); +}); + +test('does not apply a profile from another folder', () => { + const tracks = [{ lang: 'ja' }]; + assert.equal(LanPlayPreferences.find('Anime/Other/episode-01.mkv', 'audio', tracks), undefined); +}); + +test('falls back to the player default when the saved track is unavailable', () => { + const tracks = [{ lang: 'en' }]; + assert.equal(LanPlayPreferences.find('Anime/Show/episode-03.mkv', 'audio', tracks), undefined); +}); + +test('remembers disabled subtitles for the release folder', () => { + LanPlayPreferences.remember('Series/Season/episode-01.mkv', 'subtitle', null, -1); + assert.equal( + LanPlayPreferences.find('Series/Season/episode-02.mkv', 'subtitle', [{ language: 'en' }]), + null + ); +}); diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..425576e --- /dev/null +++ b/web/styles.css @@ -0,0 +1,61 @@ +:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101216; color: #f4f4f4; } +* { box-sizing: border-box; } +body { margin: 0; } +body.player-open { overflow: hidden; } +[x-cloak] { display: none !important; } +header { position: sticky; top: 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 2rem; background: #181b21; z-index: 1; } +h1 { margin: 0 0 .35rem; } +.path { color: #aab2c0; } +.view-switch { display: flex; gap: .35rem; padding: .3rem; border-radius: .55rem; background: #101216; } +.view-switch button { width: 2.7rem; height: 2.35rem; border: 0; border-radius: .4rem; background: transparent; color: #aab2c0; font-size: 1.15rem; cursor: pointer; } +.view-switch button.active { background: #354156; color: #fff; } +.view-switch button:hover, .view-switch button:focus { outline: none; background: #303746; color: #fff; } +main { padding: 1.5rem 2rem; } +.entries { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: .9rem; } +.entries button { min-width: 0; min-height: 3.5rem; padding: .65rem; border: 2px solid transparent; border-radius: .6rem; background: #252a33; color: inherit; text-align: left; font-size: 1.05rem; cursor: pointer; } +.entries button.media, .entries button.directory { display: grid; grid-template-rows: auto auto; gap: .65rem; } +.entry-preview { display: grid; place-items: center; min-height: 2.2rem; border-radius: .4rem; font-size: 1.35rem; } +.entry-preview-media { width: 100%; aspect-ratio: 16 / 9; min-height: 0; overflow: hidden; background-color: #171a20; background-repeat: no-repeat; background-size: 500% 200%; font-size: 2rem; transition: background-image .15s ease; } +.preview-spinner { animation: player-spin 1s linear infinite; color: #aab2c0; } +.entry-name { display: block; width: 100%; overflow: hidden; white-space: nowrap; } +.entry-name-track { display: inline-block; min-width: max-content; } +.entry-name.is-overflowing .entry-name-track { animation: entry-marquee 14s linear infinite; } +@keyframes entry-marquee { from { transform: translateX(0); } to { transform: translateX(-100%); } } +.entries button:focus { outline: none; border-color: #69a7ff; background: #303746; } +.entry-table-wrap { overflow-x: auto; border: 1px solid #343a45; border-radius: .65rem; } +.entry-table { width: 100%; border-collapse: collapse; table-layout: fixed; background: #20242c; } +.entry-table th { padding: .65rem .8rem; color: #aab2c0; background: #181b21; text-align: left; font-size: .85rem; text-transform: uppercase; letter-spacing: .04em; } +.entry-table th:first-child { width: 11rem; } +.entry-table th:last-child { width: 7rem; } +.entry-table td { height: 5.25rem; padding: .45rem .8rem; border-top: 1px solid #343a45; overflow: hidden; color: #dce1e8; } +.entry-table tbody tr { cursor: pointer; } +.entry-table tbody tr:hover, .entry-table tbody tr:focus { outline: none; background: #303746; } +.table-preview { display: grid; place-items: center; width: 8rem; aspect-ratio: 16 / 9; overflow: hidden; border-radius: .35rem; background-color: #171a20; background-repeat: no-repeat; background-size: 500% 200%; font-size: 1.35rem; } +.table-name { font-size: 1rem; } +.info-backdrop { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 1.5rem; background: rgba(0,0,0,.72); } +.info-dialog { position: relative; width: min(46rem, 100%); max-height: calc(100vh - 3rem); overflow: auto; padding: 1.4rem 1.5rem 1.5rem; border: 1px solid #465064; border-radius: .8rem; background: #1d222b; box-shadow: 0 1rem 4rem rgba(0,0,0,.55); } +.info-dialog h2 { margin: 0 3rem 1.2rem 0; } +.info-close { position: absolute; top: .9rem; right: .9rem; width: 2.5rem; height: 2.5rem; border: 0; border-radius: 50%; background: #303746; color: #fff; cursor: pointer; } +.info-loading { display: flex; align-items: center; gap: .65rem; color: #aab2c0; } +.info-grid { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: .7rem 1rem; margin: 0; } +.info-grid dt { color: #9ea8b8; } +.info-grid dd { min-width: 0; margin: 0; overflow-wrap: anywhere; } +.info-grid small { display: block; margin-top: .2rem; color: #7f8999; } +.info-browser { font-size: .82rem; color: #b9c0cb; } +@media (max-width: 38rem) { .info-grid { grid-template-columns: 1fr; gap: .2rem; } .info-grid dd { margin-bottom: .65rem; } } +.error { color: #ff8989; } +.player { position: fixed; inset: 0; z-index: 5; display: flex; flex-direction: column; background: #000; } +.player-close { position: absolute; top: 1.25rem; right: 1.25rem; z-index: 40; width: 4rem; height: 4rem; display: grid; place-items: center; padding: 0; border: 2px solid rgba(255,255,255,.7); border-radius: 50%; background: rgba(0,0,0,.65); color: #fff; font-size: 1.8rem; cursor: pointer; opacity: 1; transition: opacity .2s ease, transform .2s ease; } +.player-close.player-close-hidden { opacity: 0; pointer-events: none; } +.player-close:hover, .player-close:focus { outline: none; border-color: #fff; background: rgba(30,35,45,.95); transform: scale(1.05); } +.video-wrapper { position: relative; flex: 1; min-height: 0; overflow: hidden; } +.video-wrapper video { display: block; width: 100%; height: 100%; object-fit: contain; } +.player-title { position: absolute; inset: 0 0 auto; z-index: 18; min-height: 7rem; padding: 1.4rem 6.5rem 3rem 1.6rem; overflow: hidden; color: #fff; background: linear-gradient(to bottom, rgba(0,0,0,.78) 0%, rgba(0,0,0,.42) 42%, transparent 100%); font-size: clamp(1.05rem, 2vw, 1.55rem); font-weight: 600; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 3px #000; opacity: 1; transition: opacity .2s ease; pointer-events: none; } +.player-title.player-title-hidden { opacity: 0; } +.player-freeze-frame { position: absolute; inset: 0; z-index: 15; width: 100%; height: 100%; object-fit: contain; background: #000; } +.video-wrapper .cb-controlbar { --cb-accent: #69a7ff; font-family: system-ui, sans-serif; } +.player-status { position: absolute; inset: 0; z-index: 20; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; color: #f4f4f4; background: #000; font-size: 1.1rem; } +.player-status-compact { inset: 1rem auto auto 50%; transform: translateX(-50%); flex-direction: row; padding: .6rem .9rem; border-radius: .5rem; background: rgba(0,0,0,.72); font-size: .9rem; white-space: nowrap; pointer-events: none; } +.player-status-compact .player-spinner { font-size: 1.15rem; } +.player-spinner { font-size: 3rem; animation: player-spin 1s linear infinite; } +@keyframes player-spin { to { transform: rotate(360deg); } } diff --git a/web/vendor/bootstrap-icons/LICENSE b/web/vendor/bootstrap-icons/LICENSE new file mode 100644 index 0000000..f952439 --- /dev/null +++ b/web/vendor/bootstrap-icons/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019-2024 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/web/vendor/bootstrap-icons/bootstrap-icons.min.css b/web/vendor/bootstrap-icons/bootstrap-icons.min.css new file mode 100644 index 0000000..706a5c8 --- /dev/null +++ b/web/vendor/bootstrap-icons/bootstrap-icons.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a") format("woff2"),url("fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"}.bi-alphabet-uppercase::before{content:"\f2a5"}.bi-alphabet::before{content:"\f68a"}.bi-amazon::before{content:"\f68d"}.bi-arrows-collapse-vertical::before{content:"\f690"}.bi-arrows-expand-vertical::before{content:"\f695"}.bi-arrows-vertical::before{content:"\f698"}.bi-arrows::before{content:"\f6a2"}.bi-ban-fill::before{content:"\f6a3"}.bi-ban::before{content:"\f6b6"}.bi-bing::before{content:"\f6c2"}.bi-cake::before{content:"\f6e0"}.bi-cake2::before{content:"\f6ed"}.bi-cookie::before{content:"\f6ee"}.bi-copy::before{content:"\f759"}.bi-crosshair::before{content:"\f769"}.bi-crosshair2::before{content:"\f794"}.bi-emoji-astonished-fill::before{content:"\f795"}.bi-emoji-astonished::before{content:"\f79a"}.bi-emoji-grimace-fill::before{content:"\f79b"}.bi-emoji-grimace::before{content:"\f7a0"}.bi-emoji-grin-fill::before{content:"\f7a1"}.bi-emoji-grin::before{content:"\f7a6"}.bi-emoji-surprise-fill::before{content:"\f7a7"}.bi-emoji-surprise::before{content:"\f7ac"}.bi-emoji-tear-fill::before{content:"\f7ad"}.bi-emoji-tear::before{content:"\f7b2"}.bi-envelope-arrow-down-fill::before{content:"\f7b3"}.bi-envelope-arrow-down::before{content:"\f7b8"}.bi-envelope-arrow-up-fill::before{content:"\f7b9"}.bi-envelope-arrow-up::before{content:"\f7be"}.bi-feather::before{content:"\f7bf"}.bi-feather2::before{content:"\f7c4"}.bi-floppy-fill::before{content:"\f7c5"}.bi-floppy::before{content:"\f7d8"}.bi-floppy2-fill::before{content:"\f7d9"}.bi-floppy2::before{content:"\f7e4"}.bi-gitlab::before{content:"\f7e5"}.bi-highlighter::before{content:"\f7f8"}.bi-marker-tip::before{content:"\f802"}.bi-nvme-fill::before{content:"\f803"}.bi-nvme::before{content:"\f80c"}.bi-opencollective::before{content:"\f80d"}.bi-pci-card-network::before{content:"\f8cd"}.bi-pci-card-sound::before{content:"\f8ce"}.bi-radar::before{content:"\f8cf"}.bi-send-arrow-down-fill::before{content:"\f8d0"}.bi-send-arrow-down::before{content:"\f8d1"}.bi-send-arrow-up-fill::before{content:"\f8d2"}.bi-send-arrow-up::before{content:"\f8d3"}.bi-sim-slash-fill::before{content:"\f8d4"}.bi-sim-slash::before{content:"\f8d5"}.bi-sourceforge::before{content:"\f8d6"}.bi-substack::before{content:"\f8d7"}.bi-threads-fill::before{content:"\f8d8"}.bi-threads::before{content:"\f8d9"}.bi-transparency::before{content:"\f8da"}.bi-twitter-x::before{content:"\f8db"}.bi-type-h4::before{content:"\f8dc"}.bi-type-h5::before{content:"\f8dd"}.bi-type-h6::before{content:"\f8de"}.bi-backpack-fill::before{content:"\f8df"}.bi-backpack::before{content:"\f8e0"}.bi-backpack2-fill::before{content:"\f8e1"}.bi-backpack2::before{content:"\f8e2"}.bi-backpack3-fill::before{content:"\f8e3"}.bi-backpack3::before{content:"\f8e4"}.bi-backpack4-fill::before{content:"\f8e5"}.bi-backpack4::before{content:"\f8e6"}.bi-brilliance::before{content:"\f8e7"}.bi-cake-fill::before{content:"\f8e8"}.bi-cake2-fill::before{content:"\f8e9"}.bi-duffle-fill::before{content:"\f8ea"}.bi-duffle::before{content:"\f8eb"}.bi-exposure::before{content:"\f8ec"}.bi-gender-neuter::before{content:"\f8ed"}.bi-highlights::before{content:"\f8ee"}.bi-luggage-fill::before{content:"\f8ef"}.bi-luggage::before{content:"\f8f0"}.bi-mailbox-flag::before{content:"\f8f1"}.bi-mailbox2-flag::before{content:"\f8f2"}.bi-noise-reduction::before{content:"\f8f3"}.bi-passport-fill::before{content:"\f8f4"}.bi-passport::before{content:"\f8f5"}.bi-person-arms-up::before{content:"\f8f6"}.bi-person-raised-hand::before{content:"\f8f7"}.bi-person-standing-dress::before{content:"\f8f8"}.bi-person-standing::before{content:"\f8f9"}.bi-person-walking::before{content:"\f8fa"}.bi-person-wheelchair::before{content:"\f8fb"}.bi-shadows::before{content:"\f8fc"}.bi-suitcase-fill::before{content:"\f8fd"}.bi-suitcase-lg-fill::before{content:"\f8fe"}.bi-suitcase-lg::before{content:"\f8ff"}.bi-suitcase::before{content:"\f900"}.bi-suitcase2-fill::before{content:"\f901"}.bi-suitcase2::before{content:"\f902"}.bi-vignette::before{content:"\f903"}.bi-bluesky::before{content:"\f7f9"}.bi-tux::before{content:"\f904"}.bi-beaker-fill::before{content:"\f905"}.bi-beaker::before{content:"\f906"}.bi-flask-fill::before{content:"\f907"}.bi-flask-florence-fill::before{content:"\f908"}.bi-flask-florence::before{content:"\f909"}.bi-flask::before{content:"\f90a"}.bi-leaf-fill::before{content:"\f90b"}.bi-leaf::before{content:"\f90c"}.bi-measuring-cup-fill::before{content:"\f90d"}.bi-measuring-cup::before{content:"\f90e"}.bi-unlock2-fill::before{content:"\f90f"}.bi-unlock2::before{content:"\f910"}.bi-battery-low::before{content:"\f911"}.bi-anthropic::before{content:"\f912"}.bi-apple-music::before{content:"\f913"}.bi-claude::before{content:"\f914"}.bi-openai::before{content:"\f915"}.bi-perplexity::before{content:"\f916"}.bi-css::before{content:"\f917"}.bi-javascript::before{content:"\f918"}.bi-typescript::before{content:"\f919"}.bi-fork-knife::before{content:"\f91a"}.bi-globe-americas-fill::before{content:"\f91b"}.bi-globe-asia-australia-fill::before{content:"\f91c"}.bi-globe-central-south-asia-fill::before{content:"\f91d"}.bi-globe-europe-africa-fill::before{content:"\f91e"} \ No newline at end of file diff --git a/web/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 b/web/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4d8c490e1ec1153df2a4d80d1342f1d8820e792d GIT binary patch literal 134044 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

I_iT~F!-V*$H;*%98q;k6<1RxCZS#LpiY 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 K8>{rTe*gfoWh&qR literal 0 HcmV?d00001 diff --git a/web/vendor/controlbar/ControlBar.js b/web/vendor/controlbar/ControlBar.js new file mode 100644 index 0000000..c95423f --- /dev/null +++ b/web/vendor/controlbar/ControlBar.js @@ -0,0 +1,1317 @@ +/** + * ControlBar.js - Reusable dash.js video control bar + * + * A self-contained, self-generating control bar component for dash.js. + * Generates its own DOM structure inside a consumer-provided container element. + * + * Features: play/pause, seekbar (div-based with buffer + played overlay), + * volume, mute, fullscreen, bitrate/track/caption menus, thumbnail preview, + * playback rate controls, auto-hide on hover with configurable timeout. + * + * Requirements: + * - dash.js MediaPlayer instance (global `dashjs` must be available) + * - Bootstrap Icons CSS loaded in the page + * - controlbar.css loaded in the page + * + * Usage: + * import { ControlBar } from './ControlBar.js'; + * const cb = new ControlBar(player, videoElement); + * cb.init(document.getElementById('my-container')); + * cb.enable(); + */ + +const HIDE_DELAY = 3000; + +// ---- Inline helpers (replaces UIHelpers.js dependency) ---- + +function formatTime(seconds) { + if (isNaN(seconds) || !isFinite(seconds)) { + return '00:00'; + } + const negative = seconds < 0; + seconds = Math.abs(Math.floor(seconds)); + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + const pad = (n) => String(n).padStart(2, '0'); + const prefix = negative ? '- ' : ''; + if (h > 0) { + return `${prefix}${pad(h)}:${pad(m)}:${pad(s)}`; + } + return `${prefix}${pad(m)}:${pad(s)}`; +} + +function createElement(tag, attrs = {}, ...children) { + const el = document.createElement(tag); + for (const [key, value] of Object.entries(attrs)) { + if (key === 'className') { + el.className = value; + } else if (key === 'textContent') { + el.textContent = value; + } else if (key === 'innerHTML') { + el.innerHTML = value; + } else if (key.startsWith('on') && typeof value === 'function') { + el.addEventListener(key.slice(2).toLowerCase(), value); + } else { + el.setAttribute(key, value); + } + } + for (const child of children) { + if (typeof child === 'string') { + el.appendChild(document.createTextNode(child)); + } else if (child) { + el.appendChild(child); + } + } + return el; +} + +export class ControlBar { + + /** + * @param {object} player - A dash.js MediaPlayer instance + * @param {HTMLVideoElement} videoElement - The video element managed by the player + */ + constructor(player, videoElement) { + this.player = player; + this.video = videoElement; + + // DOM references (populated by _buildDOM) + this.container = null; // the controlbar root element we create + this.wrapper = null; // the consumer-provided wrapper / container parent + this.playPauseBtn = null; + this.playPauseIcon = null; + this.seekBackBtn = null; + this.seekForwardBtn = null; + this.nextBtn = null; + this.timeDisplay = null; + this.durationDisplay = null; + this.seekbarContainer = null; + this.seekbarPlayed = null; + this.seekbarBuffer = null; + this.volumeSlider = null; + this.muteBtn = null; + this.muteIcon = null; + this.fullscreenBtn = null; + this.fullscreenIcon = null; + this.bitrateBtn = null; + this.bitrateMenu = null; + this.trackBtn = null; + this.trackMenu = null; + this.captionBtn = null; + this.captionMenu = null; + this.timeSeparator = null; + this.rateDownBtn = null; + this.rateUpBtn = null; + this.rateDisplay = null; + this.thumbnailContainer = null; + this.thumbnailElem = null; + this.thumbnailTimeLabel = null; + + // State + this._seeking = false; + this._hideTimer = null; + this._enabled = false; + this._duration = 0; + this._isDynamic = false; + this._vodDuration = null; + this._vodOffset = 0; + this._lastVolume = 1; + this._isFullscreen = false; + this._activeMenu = null; + this._selectionHandler = null; + this._seekHandler = null; + this._nextHandler = null; + this._domListeners = []; + this._playerListeners = []; + + // Bound handlers for cleanup + this._onMouseMove = this._onMouseMoveBound.bind(this); + this._onMouseLeave = this._onMouseLeaveBound.bind(this); + this._onFullscreenChange = this._onFullscreenChangeBound.bind(this); + this._onVideoClick = () => this._togglePlayPause(); + } + + /** + * Initialize the control bar: build DOM, inject into container, register events. + * + * @param {HTMLElement|string} wrapperElement - The wrapper element (or CSS selector) + * that contains the video element. The controlbar will be appended inside this + * wrapper. It should have `position: relative` so the controlbar overlays correctly. + */ + init(wrapperElement) { + if (typeof wrapperElement === 'string') { + this.wrapper = document.querySelector(wrapperElement); + } else { + this.wrapper = wrapperElement; + } + + if (!this.wrapper) { + throw new Error('ControlBar: wrapper element not found'); + } + + this._buildDOM(); + this.wrapper.appendChild(this.container); + + this._attachDOMEvents(); + this._attachPlayerEvents(); + + // Initial state + this.volumeSlider.value = 1; + this._updatePlayPauseIcon(); + } + + /** + * Enable the control bar (interactive). + */ + enable() { + this._enabled = true; + this.container.classList.remove('cb-disabled'); + } + + /** + * Disable the control bar (non-interactive, dimmed). + */ + disable() { + this._enabled = false; + this.container.classList.add('cb-disabled'); + this._closeAllMenus(); + } + + /** + * Reset control bar state (call on new stream load). + */ + reset() { + this._seeking = false; + this._duration = 0; + this._isDynamic = false; + this.seekbarPlayed.style.width = '0%'; + this.seekbarBuffer.style.width = '0%'; + this.timeDisplay.textContent = '00:00'; + this.durationDisplay.textContent = '00:00'; + this.durationDisplay.classList.remove('cb-live-indicator', 'cb-at-live-edge'); + if (this.timeSeparator) { + this.timeSeparator.classList.remove('cb-hidden-element'); + } + this._updateRateDisplay(1); + this._closeAllMenus(); + this._destroyMenus(); + } + + /** + * Programmatically set the control bar to muted or unmuted state. + * Updates slider and icon without touching the player (use syncMuteState for that). + * @param {boolean} muted + */ + setMuted(muted) { + if (muted) { + this._lastVolume = parseFloat(this.volumeSlider.value) || 1; + this.volumeSlider.value = 0; + } else { + this.volumeSlider.value = this._lastVolume || 1; + } + this.muteIcon.className = muted ? 'bi bi-volume-mute-fill' : 'bi bi-volume-up-fill'; + } + + /** + * Re-apply the control bar's current volume/mute state to the player. + * Call after a new source is attached to keep mute state in sync. + */ + syncMuteState() { + try { + const vol = parseFloat(this.volumeSlider.value); + const isMuted = vol === 0 || this.muteIcon.className === 'bi bi-volume-mute-fill'; + this.player.setVolume(isMuted ? 0 : vol); + this.player.setMute(isMuted); + } catch (e) { + // Player not ready + } + } + + refreshTracks() { + this._rebuildTrackMenu(); + this._rebuildCaptionMenu(); + } + + setVodDuration(seconds, offset = 0) { + const duration = Number(seconds); + this._vodDuration = Number.isFinite(duration) && duration > 0 ? duration : null; + const start = Number(offset); + this._vodOffset = Number.isFinite(start) && start > 0 ? start : 0; + } + + onSeekRequested(handler) { + this._seekHandler = typeof handler === 'function' ? handler : null; + } + + onNextRequested(handler) { + this._nextHandler = typeof handler === 'function' ? handler : null; + } + + setNextAvailable(available) { + this.nextBtn?.classList.toggle('cb-hidden-element', !available); + } + + onTrackSelected(handler) { + this._selectionHandler = typeof handler === 'function' ? handler : null; + } + + _notifyTrackSelected(type, track) { + if (this._selectionHandler) { + this._selectionHandler(type, track); + } + } + + /** + * Destroy the control bar: remove all event listeners and remove DOM. + */ + destroy() { + this._detachDOMEvents(); + this._detachPlayerEvents(); + this._closeAllMenus(); + this._destroyMenus(); + clearTimeout(this._hideTimer); + this._setCloseButtonHidden(false); + if (this.container && this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } + } + + // ================================================================ + // DOM generation + // ================================================================ + + _buildDOM() { + // Thumbnail preview + this.thumbnailElem = createElement('div', { className: 'cb-thumbnail-elem' }); + this.thumbnailTimeLabel = createElement('div', { className: 'cb-thumbnail-time' }); + this.thumbnailContainer = createElement('div', { className: 'cb-thumbnail-container cb-hidden-element' }, + this.thumbnailElem, + this.thumbnailTimeLabel + ); + + // Seekbar + this.seekbarBuffer = createElement('div', { className: 'cb-seekbar-buffer', style: 'width: 0%' }); + this.seekbarPlayed = createElement('div', { className: 'cb-seekbar-played', style: 'width: 0%' }); + this.seekbarContainer = createElement('div', { className: 'cb-seekbar' }, + this.seekbarBuffer, + this.seekbarPlayed + ); + const seekbarRow = createElement('div', { className: 'cb-seekbar-row' }, this.seekbarContainer); + + // Play / Pause + this.playPauseIcon = createElement('i', { className: 'bi bi-play-fill' }); + this.playPauseBtn = createElement('button', { className: 'cb-btn', title: 'Play/Pause' }, this.playPauseIcon); + this.seekBackBtn = createElement('button', { + className: 'cb-btn cb-skip-btn', title: 'Back 15 seconds', textContent: '-15' + }); + this.seekForwardBtn = createElement('button', { + className: 'cb-btn cb-skip-btn', title: 'Forward 15 seconds', textContent: '+15' + }); + this.nextBtn = createElement('button', { className: 'cb-btn cb-hidden-element', title: 'Next file' }, + createElement('i', { className: 'bi bi-skip-forward-fill' }) + ); + + // Time display + this.timeDisplay = createElement('span', { className: 'cb-time', textContent: '00:00' }); + this.timeSeparator = createElement('span', { className: 'cb-time cb-time-separator', textContent: '/' }); + this.durationDisplay = createElement('span', { className: 'cb-time', textContent: '00:00' }); + + // Spacer + const spacer = createElement('div', { className: 'cb-spacer' }); + + // Volume / Mute + this.muteIcon = createElement('i', { className: 'bi bi-volume-up-fill' }); + this.muteBtn = createElement('button', { className: 'cb-btn', title: 'Mute/Unmute' }, this.muteIcon); + this.volumeSlider = createElement('input', { + type: 'range', + className: 'cb-volume-slider', + min: '0', + max: '1', + step: '0.05', + value: '1' + }); + const volumeGroup = createElement('div', { className: 'cb-volume-group' }, + this.muteBtn, + this.volumeSlider + ); + + // Playback rate + this.rateDownBtn = createElement('button', { className: 'cb-btn cb-btn-sm', title: 'Decrease speed' }, + createElement('i', { className: 'bi bi-dash' }) + ); + this.rateDisplay = createElement('span', { className: 'cb-rate-display', title: 'Reset to 1x', textContent: '1.00x' }); + this.rateUpBtn = createElement('button', { className: 'cb-btn cb-btn-sm', title: 'Increase speed' }, + createElement('i', { className: 'bi bi-plus' }) + ); + const rateGroup = createElement('div', { className: 'cb-rate-group' }, + this.rateDownBtn, + this.rateDisplay, + this.rateUpBtn + ); + + // Bitrate menu + this.bitrateMenu = createElement('div', { className: 'cb-menu cb-hidden-element' }); + this.bitrateBtn = createElement('button', { className: 'cb-btn cb-hidden-element', title: 'Quality' }, + createElement('i', { className: 'bi bi-sliders' }) + ); + const bitrateAnchor = createElement('div', { className: 'cb-menu-anchor' }, + this.bitrateBtn, + this.bitrateMenu + ); + + // Track menu + this.trackMenu = createElement('div', { className: 'cb-menu cb-hidden-element' }); + this.trackBtn = createElement('button', { className: 'cb-btn cb-hidden-element', title: 'Tracks' }, + createElement('i', { className: 'bi bi-music-note-list' }) + ); + const trackAnchor = createElement('div', { className: 'cb-menu-anchor' }, + this.trackBtn, + this.trackMenu + ); + + // Caption menu + this.captionMenu = createElement('div', { className: 'cb-menu cb-hidden-element' }); + this.captionBtn = createElement('button', { className: 'cb-btn cb-hidden-element', title: 'Captions' }, + createElement('i', { className: 'bi bi-badge-cc' }) + ); + const captionAnchor = createElement('div', { className: 'cb-menu-anchor' }, + this.captionBtn, + this.captionMenu + ); + + // Fullscreen + this.fullscreenIcon = createElement('i', { className: 'bi bi-fullscreen' }); + this.fullscreenBtn = createElement('button', { className: 'cb-btn', title: 'Fullscreen' }, this.fullscreenIcon); + + // Controls row + const controlsRow = createElement('div', { className: 'cb-controls-row' }, + this.playPauseBtn, + this.seekBackBtn, + this.seekForwardBtn, + this.nextBtn, + this.timeDisplay, + this.timeSeparator, + this.durationDisplay, + spacer, + volumeGroup, + rateGroup, + bitrateAnchor, + trackAnchor, + captionAnchor, + this.fullscreenBtn + ); + + // Root container + this.container = createElement('div', { className: 'cb-controlbar cb-disabled' }, + this.thumbnailContainer, + seekbarRow, + controlsRow + ); + } + + // ================================================================ + // DOM event handlers + // ================================================================ + + _attachDOMEvents() { + this._listen(this.playPauseBtn, 'click', () => this._togglePlayPause()); + this._listen(this.seekBackBtn, 'click', () => this._requestRelativeSeek(-15)); + this._listen(this.seekForwardBtn, 'click', () => this._requestRelativeSeek(15)); + this._listen(this.nextBtn, 'click', () => this._nextHandler?.()); + this._listen(this.muteBtn, 'click', () => this._toggleMute()); + this._listen(this.fullscreenBtn, 'click', () => this._toggleFullscreen()); + this._listen(this.volumeSlider, 'input', () => this._onVolumeInput()); + this._listen(this.video, 'click', this._onVideoClick); + + // Live indicator click — seek to live edge + this._listen(this.durationDisplay, 'click', () => this._seekToLiveEdge()); + + // Playback rate controls + this._listen(this.rateDownBtn, 'click', () => this._changeRate(-0.25)); + this._listen(this.rateUpBtn, 'click', () => this._changeRate(0.25)); + this._listen(this.rateDisplay, 'click', () => this._resetRate()); + + // Seekbar mouse events + this._listen(this.seekbarContainer, 'mousedown', (e) => this._onSeekMouseDown(e)); + this._listen(this.seekbarContainer, 'mousemove', (e) => this._onSeekMouseMove(e)); + this._listen(this.seekbarContainer, 'mouseleave', () => this._onSeekMouseLeave()); + this._listen(document, 'mousemove', (e) => this._onDocumentMouseMove(e)); + this._listen(document, 'mouseup', (e) => this._onDocumentMouseUp(e)); + + // Touch events for seekbar + this._listen(this.seekbarContainer, 'touchstart', (e) => this._onSeekTouchStart(e), { passive: false }); + this._listen(this.seekbarContainer, 'touchmove', (e) => this._onSeekTouchMove(e), { passive: false }); + this._listen(this.seekbarContainer, 'touchend', (e) => this._onSeekTouchEnd(e)); + + // Auto-hide: show on mouse move over wrapper, hide after delay + this._listen(this.wrapper, 'mousemove', this._onMouseMove); + this._listen(this.wrapper, 'mouseleave', this._onMouseLeave); + this._listen(this.wrapper, 'touchstart', this._onMouseMove, { passive: true }); + + // Fullscreen change + this._listen(document, 'fullscreenchange', this._onFullscreenChange); + this._listen(document, 'webkitfullscreenchange', this._onFullscreenChange); + + // Bitrate / track / caption buttons + this._listen(this.bitrateBtn, 'click', (e) => { + e.stopPropagation(); + this._toggleMenu('bitrate'); + }); + this._listen(this.trackBtn, 'click', (e) => { + e.stopPropagation(); + this._toggleMenu('track'); + }); + this._listen(this.captionBtn, 'click', (e) => { + e.stopPropagation(); + this._toggleMenu('caption'); + }); + + // Close menus on click outside + this._listen(document, 'click', () => this._closeAllMenus()); + + // Prevent control bar clicks from closing menus + this._listen(this.container, 'click', (e) => e.stopPropagation()); + } + + _detachDOMEvents() { + this._domListeners.forEach(({ target, type, handler, options }) => { + target.removeEventListener(type, handler, options); + }); + this._domListeners = []; + } + + _listen(target, type, handler, options) { + target.addEventListener(type, handler, options); + this._domListeners.push({ target, type, handler, options }); + } + + _attachPlayerEvents() { + const events = dashjs.MediaPlayer.events; + + this._listenPlayer(events.PLAYBACK_STARTED, () => this._onPlaybackStarted()); + this._listenPlayer(events.PLAYBACK_PAUSED, () => this._onPlaybackPaused()); + this._listenPlayer(events.PLAYBACK_TIME_UPDATED, (e) => this._onTimeUpdate(e)); + this._listenPlayer(events.STREAM_ACTIVATED, () => this._onStreamActivated()); + this._listenPlayer(events.STREAM_TEARDOWN_COMPLETE, () => this._onStreamTeardown()); + this._listenPlayer(events.BUFFER_LEVEL_UPDATED, (e) => this._onBufferLevelUpdated(e)); + this._listenPlayer(events.TEXT_TRACKS_ADDED, (e) => this._onTextTracksAdded(e)); + + if (dashjs.Protection && dashjs.Protection.events) { + this._listenPlayer(dashjs.Protection.events.KEY_STATUSES_MAP_UPDATED, () => this._rebuildBitrateMenu()); + } + } + + _detachPlayerEvents() { + this._playerListeners.forEach(({ event, handler }) => this.player.off(event, handler)); + this._playerListeners = []; + } + + _listenPlayer(event, handler) { + this.player.on(event, handler); + this._playerListeners.push({ event, handler }); + } + + // ================================================================ + // Play / Pause + // ================================================================ + + _togglePlayPause() { + if (!this._enabled) { + return; + } + if (this.player.isPaused()) { + this.player.play(); + } else { + this.player.pause(); + } + } + + _updatePlayPauseIcon() { + if (!this.playPauseIcon) { + return; + } + try { + const isPaused = this.player.isPaused(); + this.playPauseIcon.className = isPaused ? 'bi bi-play-fill' : 'bi bi-pause-fill'; + } catch (e) { + // Player not ready yet — default to play icon + this.playPauseIcon.className = 'bi bi-play-fill'; + } + } + + _onPlaybackStarted() { + this._updatePlayPauseIcon(); + this._startHideTimer(); + } + + _onPlaybackPaused() { + this._updatePlayPauseIcon(); + this._showControlBar(); + this._clearHideTimer(); + } + + // ================================================================ + // Time / Duration + // ================================================================ + + _onTimeUpdate() { + if (this._seeking) { + return; + } + + try { + const time = this._vodOffset + this._sessionTime(); + const duration = this._vodDuration || this.player.duration(); + this._duration = duration; + this._isDynamic = this.player.isDynamic() && !this._vodDuration; + + if (this._isDynamic) { + // Show negative latency (e.g. -00:20) instead of DVR position + const liveLatency = this.player.getCurrentLiveLatency() || 0; + this.timeDisplay.textContent = `-${formatTime(Math.round(liveLatency))}`; + + this.durationDisplay.innerHTML = ' LIVE'; + this.durationDisplay.classList.add('cb-live-indicator'); + + // Hide separator for live + if (this.timeSeparator) { + this.timeSeparator.classList.add('cb-hidden-element'); + } + + // Check if at live edge + const targetDelay = this.player.getTargetLiveDelay() || 0; + const atLiveEdge = liveLatency <= targetDelay + 1; + this.durationDisplay.classList.toggle('cb-at-live-edge', atLiveEdge); + } else { + this.timeDisplay.textContent = formatTime(time); + this.durationDisplay.textContent = formatTime(duration); + this.durationDisplay.classList.remove('cb-live-indicator', 'cb-at-live-edge'); + + // Show separator for VoD + if (this.timeSeparator) { + this.timeSeparator.classList.remove('cb-hidden-element'); + } + } + + // Update seekbar progress + if (duration > 0) { + const pct = (time / duration) * 100; + this.seekbarPlayed.style.width = `${Math.min(pct, 100)}%`; + } + + // Sync rate display with actual playback rate + const actualRate = this.player.getPlaybackRate(); + if (actualRate !== undefined && actualRate !== null) { + this._updateRateDisplay(actualRate); + } + } catch (err) { + // Player may not be ready + } + } + + _seekToLiveEdge() { + if (!this._enabled || !this._isDynamic) { + return; + } + try { + this.player.seekToOriginalLive(); + } catch (e) { + // Fallback: seek to duration (end of DVR window) + try { + this.player.seek(this.player.duration()); + } catch (e2) { + // Player not ready + } + } + } + + // ================================================================ + // Playback Rate + // ================================================================ + + _changeRate(delta) { + if (!this._enabled) { + return; + } + try { + const current = this.player.getPlaybackRate() || 1; + const next = Math.min(4, Math.max(0.25, Math.round((current + delta) * 4) / 4)); + this.player.setPlaybackRate(next); + this._updateRateDisplay(next); + } catch (e) { + // Player not ready + } + } + + _resetRate() { + if (!this._enabled) { + return; + } + try { + this.player.setPlaybackRate(1); + this._updateRateDisplay(1); + } catch (e) { + // Player not ready + } + } + + _updateRateDisplay(rate) { + if (this.rateDisplay) { + this.rateDisplay.textContent = `${rate.toFixed(2)}x`; + } + } + + // ================================================================ + // Seekbar + // ================================================================ + + _getSeekTime(clientX) { + const rect = this.seekbarContainer.getBoundingClientRect(); + let pct = (clientX - rect.left) / rect.width; + pct = Math.max(0, Math.min(1, pct)); + return pct * (this._duration || 0); + } + + _currentAbsoluteTime() { + return this._vodOffset + this._sessionTime(); + } + + _sessionTime() { + const time = Number(this.video.currentTime); + return Number.isFinite(time) && time > 0 ? time : 0; + } + + _requestRelativeSeek(delta) { + this._requestSeek(this._currentAbsoluteTime() + delta); + } + + _requestSeek(time) { + const maximum = this._vodDuration || this.player.duration(); + const target = Math.max(0, Math.min(time, Math.max(0, maximum - 0.1))); + if (this._seekHandler) { + this._seekHandler(target); + } else { + this.player.seek(Math.max(0, target - this._vodOffset)); + } + } + + _onSeekMouseDown(e) { + if (!this._enabled) { + return; + } + this._seeking = true; + e.preventDefault(); + } + + _onDocumentMouseMove(e) { + if (!this._seeking) { + return; + } + const time = this._getSeekTime(e.clientX); + const pct = (this._duration > 0) ? (time / this._duration) * 100 : 0; + this.seekbarPlayed.style.width = `${pct}%`; + this.timeDisplay.textContent = formatTime(time); + } + + _onDocumentMouseUp(e) { + if (!this._seeking) { + return; + } + this._seeking = false; + const time = this._getSeekTime(e.clientX); + this._requestSeek(time); + } + + _onSeekMouseMove(e) { + if (!this._enabled || !this._duration) { + return; + } + const time = this._getSeekTime(e.clientX); + this._showThumbnail(e.clientX, time); + } + + _onSeekMouseLeave() { + this._hideThumbnail(); + } + + _onSeekTouchStart(e) { + if (!this._enabled) { + return; + } + this._seeking = true; + e.preventDefault(); + } + + _onSeekTouchMove(e) { + if (!this._seeking || !e.touches[0]) { + return; + } + e.preventDefault(); + const time = this._getSeekTime(e.touches[0].clientX); + const pct = (this._duration > 0) ? (time / this._duration) * 100 : 0; + this.seekbarPlayed.style.width = `${pct}%`; + this.timeDisplay.textContent = formatTime(time); + } + + _onSeekTouchEnd(e) { + if (!this._seeking) { + return; + } + this._seeking = false; + if (e.changedTouches && e.changedTouches[0]) { + const time = this._getSeekTime(e.changedTouches[0].clientX); + this._requestSeek(time); + } + } + + // ================================================================ + // Thumbnails + // ================================================================ + + _showThumbnail(clientX, time) { + if (!this.player.provideThumbnail) { + return; + } + + this.player.provideThumbnail(time, (thumbnail) => { + if (!thumbnail || !thumbnail.url) { + this._hideThumbnail(); + return; + } + + const containerRect = this.seekbarContainer.getBoundingClientRect(); + const wrapperRect = this.wrapper.getBoundingClientRect(); + + // Scale thumbnail + const maxHeight = wrapperRect.height * 0.15; + const scale = Math.min(maxHeight / thumbnail.height, 2); + const width = thumbnail.width * scale; + const height = thumbnail.height * scale; + + this.thumbnailElem.style.width = `${thumbnail.width}px`; + this.thumbnailElem.style.height = `${thumbnail.height}px`; + this.thumbnailElem.style.background = `url("${thumbnail.url}") -${thumbnail.x}px -${thumbnail.y}px`; + this.thumbnailElem.style.backgroundSize = ''; + this.thumbnailElem.style.transform = `scale(${scale})`; + + // Position horizontally centered on mouse + let left = clientX - containerRect.left - width / 2; + left = Math.max(0, Math.min(left, containerRect.width - width)); + + this.thumbnailContainer.style.left = `${left}px`; + this.thumbnailContainer.style.bottom = `${containerRect.height + 10}px`; + this.thumbnailTimeLabel.textContent = formatTime(time); + this.thumbnailContainer.classList.remove('cb-hidden-element'); + }); + } + + _hideThumbnail() { + if (this.thumbnailContainer) { + this.thumbnailContainer.classList.add('cb-hidden-element'); + } + } + + // ================================================================ + // Buffer + // ================================================================ + + _onBufferLevelUpdated(e) { + if (!e || e.mediaType !== 'video' || !this._duration || this._isDynamic) { + return; + } + try { + const dashMetrics = this.player.getDashMetrics(); + const bufferLevel = dashMetrics.getCurrentBufferLevel('video', true) || 0; + const time = this._vodOffset + this._sessionTime(); + const bufferEnd = time + bufferLevel; + const pct = (bufferEnd / this._duration) * 100; + this.seekbarBuffer.style.width = `${Math.min(pct, 100)}%`; + } catch (err) { + // ignore + } + } + + // ================================================================ + // Volume / Mute + // ================================================================ + + _onVolumeInput() { + const val = parseFloat(this.volumeSlider.value); + try { + this.player.setVolume(val); + this.player.setMute(val === 0); + } catch (e) { + // Player not ready + } + this._updateMuteIcon(); + } + + _toggleMute() { + try { + if (this.player.isMuted()) { + this.player.setMute(false); + this.player.setVolume(this._lastVolume || 1); + this.volumeSlider.value = this._lastVolume || 1; + } else { + this._lastVolume = this.player.getVolume(); + this.player.setMute(true); + this.volumeSlider.value = 0; + } + } catch (e) { + // Player not ready + } + this._updateMuteIcon(); + } + + _updateMuteIcon() { + try { + const muted = this.player.isMuted() || parseFloat(this.volumeSlider.value) === 0; + this.muteIcon.className = muted ? 'bi bi-volume-mute-fill' : 'bi bi-volume-up-fill'; + } catch (e) { + // Player not ready — default to unmuted icon + this.muteIcon.className = 'bi bi-volume-up-fill'; + } + } + + // ================================================================ + // Fullscreen + // ================================================================ + + _toggleFullscreen() { + if (!document.fullscreenElement && !document.webkitFullscreenElement) { + if (this.wrapper.requestFullscreen) { + this.wrapper.requestFullscreen(); + } else if (this.wrapper.webkitRequestFullscreen) { + this.wrapper.webkitRequestFullscreen(); + } + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + } + } + + _onFullscreenChangeBound() { + this._isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement); + this.fullscreenIcon.className = this._isFullscreen + ? 'bi bi-fullscreen-exit' + : 'bi bi-fullscreen'; + this.wrapper.classList.toggle('cb-fullscreen', this._isFullscreen); + } + + // ================================================================ + // Auto-hide + // ================================================================ + + _onMouseMoveBound() { + this._showControlBar(); + this._startHideTimer(); + } + + _onMouseLeaveBound() { + try { + if (!this.player.isPaused()) { + this._startHideTimer(); + } + } catch (e) { + // Player not ready + } + } + + _showControlBar() { + this.container.classList.remove('cb-hidden'); + this._setCloseButtonHidden(false); + this.wrapper.style.cursor = ''; + } + + _hideControlBar() { + try { + if (this.player.isPaused() || this._seeking || this._activeMenu) { + return; + } + } catch (e) { + return; + } + this.container.classList.add('cb-hidden'); + this._setCloseButtonHidden(true); + this.wrapper.style.cursor = 'none'; + } + + _setCloseButtonHidden(hidden) { + const playerShell = this.wrapper.closest('.player'); + const closeButton = playerShell && playerShell.querySelector('.player-close'); + const title = playerShell && playerShell.querySelector('.player-title'); + if (closeButton) { + closeButton.classList.toggle('player-close-hidden', hidden); + } + if (title) { + title.classList.toggle('player-title-hidden', hidden); + } + } + + _startHideTimer() { + this._clearHideTimer(); + try { + if (this.player.isPaused()) { + return; + } + } catch (e) { + return; + } + this._hideTimer = setTimeout(() => this._hideControlBar(), HIDE_DELAY); + } + + _clearHideTimer() { + if (this._hideTimer) { + clearTimeout(this._hideTimer); + this._hideTimer = null; + } + } + + // ================================================================ + // Stream events + // ================================================================ + + _onStreamActivated() { + this._rebuildBitrateMenu(); + this._rebuildTrackMenu(); + this._rebuildCaptionMenu(); + + // Update duration + try { + this._duration = this._vodDuration || this.player.duration(); + this._isDynamic = this.player.isDynamic() && !this._vodDuration; + } catch (err) { + // ignore + } + } + + _onStreamTeardown() { + this.reset(); + } + + _onTextTracksAdded() { + this._rebuildCaptionMenu(); + } + + // ================================================================ + // Menus + // ================================================================ + + _toggleMenu(menuName) { + const menuEl = this._getMenuElement(menuName); + if (!menuEl) { + return; + } + + if (this._activeMenu === menuName) { + this._closeAllMenus(); + } else { + this._closeAllMenus(); + menuEl.classList.remove('cb-hidden-element'); + this._activeMenu = menuName; + } + } + + _closeAllMenus() { + for (const name of ['bitrate', 'track', 'caption']) { + const menuEl = this._getMenuElement(name); + if (menuEl) { + menuEl.classList.add('cb-hidden-element'); + } + } + this._activeMenu = null; + } + + _getMenuElement(name) { + switch (name) { + case 'bitrate': + return this.bitrateMenu; + case 'track': + return this.trackMenu; + case 'caption': + return this.captionMenu; + } + return null; + } + + _destroyMenus() { + if (this.bitrateMenu) { + this.bitrateMenu.innerHTML = ''; + } + if (this.trackMenu) { + this.trackMenu.innerHTML = ''; + } + if (this.captionMenu) { + this.captionMenu.innerHTML = ''; + } + } + + _rebuildBitrateMenu() { + if (!this.bitrateMenu) { + return; + } + this.bitrateMenu.innerHTML = ''; + + let hasItems = false; + + for (const type of ['video', 'audio']) { + try { + const reps = this.player.getRepresentationsByType(type); + if (!reps || reps.length < 1) { + continue; + } + + hasItems = true; + const title = createElement('div', { className: 'cb-menu-title' }, type.charAt(0).toUpperCase() + type.slice(1)); + this.bitrateMenu.appendChild(title); + + // Auto switch option + const autoItem = createElement('div', { + className: 'cb-menu-item cb-menu-item-selected', + textContent: 'Auto', + onClick: () => { + this.player.updateSettings({ + streaming: { abr: { autoSwitchBitrate: { [type]: true } } } + }); + this._rebuildBitrateMenu(); + } + }); + this.bitrateMenu.appendChild(autoItem); + + const settings = this.player.getSettings(); + const autoSwitch = settings?.streaming?.abr?.autoSwitchBitrate?.[type] !== false; + const currentRep = this.player.getCurrentRepresentationForType(type); + + reps.forEach((rep, idx) => { + const bitrate = Math.round(rep.bandwidth / 1000); + let label = `${bitrate} kbps`; + if (rep.width && rep.height) { + label += ` (${rep.width}x${rep.height})`; + } + + const isCurrent = !autoSwitch && currentRep && currentRep.id === rep.id; + const item = createElement('div', { + className: `cb-menu-item ${isCurrent ? 'cb-menu-item-selected' : ''}`, + textContent: label, + onClick: () => { + this.player.updateSettings({ + streaming: { abr: { autoSwitchBitrate: { [type]: false } } } + }); + this.player.setRepresentationForTypeByIndex(type, idx, false); + this._rebuildBitrateMenu(); + } + }); + this.bitrateMenu.appendChild(item); + }); + + // Update auto item class + if (!autoSwitch) { + autoItem.classList.remove('cb-menu-item-selected'); + } + + } catch (err) { + // ignore + } + } + + // Show/hide button + if (this.bitrateBtn) { + this.bitrateBtn.classList.toggle('cb-hidden-element', !hasItems); + } + } + + _rebuildTrackMenu() { + if (!this.trackMenu) { + return; + } + this.trackMenu.innerHTML = ''; + + let hasItems = false; + + for (const type of ['audio', 'video']) { + try { + const tracks = this.player.getTracksFor(type); + if (!tracks || tracks.length < 1) { + continue; + } + + hasItems = true; + const title = createElement('div', { className: 'cb-menu-title' }, type.charAt(0).toUpperCase() + type.slice(1)); + this.trackMenu.appendChild(title); + + const currentTrack = this.player.getCurrentTrackFor(type); + + tracks.forEach((track) => { + let label = ''; + + // Prefer explicit label if available + if (track.labels && track.labels.length > 0) { + const textLabel = track.labels.find(l => l.text) || track.labels[0]; + if (textLabel && textLabel.text) { + label = textLabel.text; + } + } + + // Fall back to language, then to media type name + if (!label) { + label = track.lang || type.charAt(0).toUpperCase() + type.slice(1); + } + + const isCurrent = currentTrack && this._isTracksEqual(currentTrack, track); + const item = createElement('div', { + className: `cb-menu-item ${isCurrent ? 'cb-menu-item-selected' : ''}`, + textContent: label, + onClick: () => { + this.player.setCurrentTrack(track); + this._notifyTrackSelected(type, track); + this._rebuildTrackMenu(); + } + }); + this.trackMenu.appendChild(item); + }); + } catch (err) { + // ignore + } + } + + if (this.trackBtn) { + this.trackBtn.classList.toggle('cb-hidden-element', !hasItems); + } + } + + _isTracksEqual(t1, t2) { + try { + if (!t1 || !t2) { + return false; + } + return t1.id === t2.id + && t1.lang === t2.lang + && t1.viewpoint === t2.viewpoint + && String(t1.roles || '') === String(t2.roles || '') + && String(t1.accessibility || '') === String(t2.accessibility || '') + && String(t1.audioChannelConfiguration || '') === String(t2.audioChannelConfiguration || ''); + } catch (e) { + return false; + } + } + + _formatChannels(track) { + if (!track.audioChannelConfiguration || !track.audioChannelConfiguration.length) { + return null; + } + const value = track.audioChannelConfiguration[0].value; + switch (value) { + case '1': + return 'mono'; + case '2': + return 'stereo'; + case '6': + return '5.1'; + case '8': + return '7.1'; + default: + return value ? `${value}ch` : null; + } + } + + _rebuildCaptionMenu() { + if (!this.captionMenu) { + return; + } + this.captionMenu.innerHTML = ''; + + try { + // Prefer dash.js tracks. dash.js also exposes them as native TextTracks, + // but selecting those directly bypasses its text timeline controller. + const tracks = this.player.getTracksFor('text'); + if (!tracks || tracks.length === 0) { + this._rebuildNativeCaptionMenu(); + return; + } + + const currentIdx = this.player.getCurrentTextTrackIndex(); + + // OFF option + const offItem = createElement('div', { + className: `cb-menu-item ${currentIdx === -1 ? 'cb-menu-item-selected' : ''}`, + textContent: 'Off', + onClick: () => { + this.player.setTextTrack(-1); + this._notifyTrackSelected('text', null); + this._rebuildCaptionMenu(); + } + }); + this.captionMenu.appendChild(offItem); + + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i]; + + // Build label: prefer Label element text, fall back to lang + let label = ''; + if (track.labels && track.labels.length > 0) { + const textLabel = track.labels.find(l => l.text); + if (textLabel) { + label = textLabel.text; + } + } + if (!label) { + label = track.lang || 'Und'; + } + + const isCurrent = currentIdx === i; + const item = createElement('div', { + className: `cb-menu-item ${isCurrent ? 'cb-menu-item-selected' : ''}`, + textContent: label, + onClick: () => { + this.player.setTextTrack(i); + this._notifyTrackSelected('text', track); + this._rebuildCaptionMenu(); + } + }); + this.captionMenu.appendChild(item); + } + + if (this.captionBtn) { + this.captionBtn.classList.toggle('cb-hidden-element', tracks.length === 0); + } + } catch (err) { + if (this.captionBtn) { + this.captionBtn.classList.add('cb-hidden-element'); + } + } + } + + _rebuildNativeCaptionMenu() { + const tracks = Array.from(this.video.textTracks || []); + if (!tracks.length) { + this.captionBtn?.classList.add('cb-hidden-element'); + return; + } + + const currentIdx = tracks.findIndex(track => track.mode === 'showing'); + const select = (selected) => { + tracks.forEach((track, index) => { + track.mode = index === selected ? 'showing' : 'disabled'; + }); + this._notifyTrackSelected('nativeText', selected >= 0 ? tracks[selected] : null); + this._rebuildCaptionMenu(); + }; + this.captionMenu.appendChild(createElement('div', { + className: `cb-menu-item ${currentIdx === -1 ? 'cb-menu-item-selected' : ''}`, + textContent: 'Off', + onClick: () => select(-1) + })); + tracks.forEach((track, index) => { + this.captionMenu.appendChild(createElement('div', { + className: `cb-menu-item ${currentIdx === index ? 'cb-menu-item-selected' : ''}`, + textContent: track.label || track.language || `Subtitle ${index + 1}`, + onClick: () => select(index) + })); + }); + this.captionBtn?.classList.remove('cb-hidden-element'); + } +} diff --git a/web/vendor/controlbar/controlbar.css b/web/vendor/controlbar/controlbar.css new file mode 100644 index 0000000..3b5e78e --- /dev/null +++ b/web/vendor/controlbar/controlbar.css @@ -0,0 +1,339 @@ +/* ============================================================ + dash.js Controlbar – Standalone CSS + ============================================================ + Include this stylesheet alongside ControlBar.js. + Requires Bootstrap Icons CSS for icon display. + + Theming: override --cb-accent and --cb-danger on the + .cb-controlbar selector (or any ancestor) to customise + highlight and live-indicator colours. + ============================================================ */ + +/* ---- Default theme variables ---- */ +.cb-controlbar { + --cb-accent: #5b8def; + --cb-danger: #e74c3c; +} + +.cb-skip-btn { + min-width: 2.5rem; + font-size: 0.8rem; + font-weight: 700; +} + +/* ---- Utility: hidden element (replaces Bootstrap d-none) ---- */ +.cb-hidden-element { + display: none !important; +} + +/* ---- Control Bar container ---- */ +.cb-controlbar { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.85)); + padding: 2rem 1rem 0.5rem 1rem; + z-index: 10; + transition: opacity 0.3s ease, visibility 0.3s ease; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.cb-controlbar.cb-hidden { + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.cb-controlbar.cb-disabled { + pointer-events: none; + opacity: 0.3; +} + +/* ---- Seekbar ---- */ +.cb-seekbar-row { + display: flex; + align-items: center; + gap: 0.5rem; + position: relative; +} + +.cb-seekbar { + flex: 1; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + position: relative; + cursor: pointer; + transition: height 0.1s; +} + +.cb-seekbar:hover { + height: 6px; +} + +.cb-seekbar-buffer { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: rgba(255, 255, 255, 0.3); + border-radius: 2px; + pointer-events: none; +} + +.cb-seekbar-played { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: var(--cb-accent); + border-radius: 2px; + pointer-events: none; +} + +.cb-seekbar-played::after { + content: ''; + position: absolute; + right: -6px; + top: 50%; + transform: translateY(-50%); + width: 12px; + height: 12px; + background: var(--cb-accent); + border-radius: 50%; + opacity: 0; + transition: opacity 0.1s; +} + +.cb-seekbar:hover .cb-seekbar-played::after { + opacity: 1; +} + +/* ---- Control buttons row ---- */ +.cb-controls-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.cb-btn { + background: none; + border: none; + color: #fff; + font-size: 1.1rem; + cursor: pointer; + padding: 0.25rem; + opacity: 0.85; + transition: opacity 0.15s; + display: flex; + align-items: center; + justify-content: center; +} + +.cb-btn:hover { + opacity: 1; +} + +.cb-time { + font-size: 0.8rem; + color: #fff; + font-variant-numeric: tabular-nums; + white-space: nowrap; + user-select: none; +} + +.cb-time-separator { + opacity: 0.4; +} + +.cb-spacer { + flex: 1; +} + +/* ---- Volume ---- */ +.cb-volume-group { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.cb-volume-slider { + width: 80px; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: rgba(255, 255, 255, 0.3); + border-radius: 2px; + outline: none; + cursor: pointer; +} + +.cb-volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 12px; + background: #fff; + border-radius: 50%; + cursor: pointer; +} + +.cb-volume-slider::-moz-range-thumb { + width: 12px; + height: 12px; + background: #fff; + border-radius: 50%; + cursor: pointer; + border: none; +} + +/* ---- Playback rate controls ---- */ +.cb-rate-group { + display: flex; + align-items: center; + gap: 0.15rem; + margin: 0 0.25rem; +} + +.cb-rate-display { + color: #e0e0e0; + font-size: 0.75rem; + min-width: 2.5rem; + text-align: center; + cursor: pointer; + user-select: none; + line-height: 1; +} + +.cb-rate-display:hover { + color: #ffffff; +} + +.cb-btn-sm { + font-size: 0.7rem; + padding: 0.1rem 0.3rem; +} + +/* ---- Live indicator ---- */ +.cb-live-indicator { + cursor: pointer; + color: #8a8aa0; +} + +.cb-live-indicator.cb-at-live-edge { + color: var(--cb-danger); +} + +.cb-live-indicator .bi-circle-fill { + font-size: 0.5rem; + vertical-align: middle; +} + +/* ---- Thumbnail preview ---- */ +.cb-thumbnail-container { + position: absolute; + bottom: 100%; + pointer-events: none; + z-index: 20; + text-align: center; +} + +.cb-thumbnail-elem { + border: 2px solid #fff; + border-radius: 4px; + overflow: hidden; + transform-origin: center bottom; +} + +.cb-thumbnail-time { + font-size: 0.7rem; + color: #fff; + background: rgba(0, 0, 0, 0.7); + padding: 1px 6px; + border-radius: 3px; + margin-top: 4px; + display: inline-block; +} + +/* ---- Menu anchors and menus ---- */ +.cb-menu-anchor { + position: relative; +} + +/* Menus — always dark (overlays video) */ +.cb-menu { + position: absolute; + bottom: 100%; + right: 0; + background: rgba(20, 20, 40, 0.95); + border: 1px solid #2d2d50; + border-radius: 6px; + min-width: 180px; + max-height: min(400px, 60vh); + overflow-y: auto; + padding: 0.25rem 0; + margin-bottom: 0.5rem; + backdrop-filter: blur(10px); + z-index: 30; +} + +/* Scrollbar styling for menus */ +.cb-menu::-webkit-scrollbar { + width: 6px; +} + +.cb-menu::-webkit-scrollbar-track { + background: transparent; +} + +.cb-menu::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.cb-menu::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.35); +} + +/* Firefox scrollbar */ +.cb-menu { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.2) transparent; +} + +.cb-menu-title { + padding: 0.35rem 0.75rem; + font-size: 0.7rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.5); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.cb-menu-item { + padding: 0.35rem 0.75rem; + font-size: 0.8rem; + cursor: pointer; + color: #e0e0e0; + transition: background 0.1s; +} + +.cb-menu-item:hover { + background: rgba(255, 255, 255, 0.1); +} + +.cb-menu-item-selected { + color: var(--cb-accent); + font-weight: 600; +} + +.cb-menu-item-selected::before { + content: '✓ '; +} + +/* ---- Fullscreen wrapper helper ---- */ +.cb-fullscreen { + /* Applied to the wrapper element when entering fullscreen */ +}