Compare commits

...

7 Commits

Author SHA1 Message Date
uttarayan21
77fe7b6bb4 feat: Update compilation for macos
Some checks failed
build / checks-matrix (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
build / checks-build (push) Has been cancelled
2025-11-22 05:03:07 +05:30
uttarayan21
0fb5cb1d99 feat: Move stuff under linux
Some checks failed
build / checks-matrix (push) Has been cancelled
build / checks-build (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
2025-11-22 04:42:37 +05:30
uttarayan21
61a2ea1733 feat: Added stuff
Some checks failed
build / checks-matrix (push) Has been cancelled
build / checks-build (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
2025-11-22 04:39:42 +05:30
uttarayan21
b1cfc19b96 feat: Added iced_video_player
Some checks failed
build / checks-matrix (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
build / checks-build (push) Has been cancelled
2025-11-20 21:59:47 +05:30
uttarayan21
f41625e0ed feat: Added stuff
Some checks failed
build / checks-matrix (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
build / checks-build (push) Has been cancelled
2025-11-19 17:01:14 +05:30
uttarayan21
29674df85e feat: Use fluid grid
Some checks failed
build / checks-matrix (push) Has been cancelled
build / checks-build (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
2025-11-19 02:29:04 +05:30
uttarayan21
442a7e49b2 feat(ui): enhance BlurHash and navigation functionality
Some checks failed
build / checks-matrix (push) Has been cancelled
build / checks-build (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled
- Modify BlurHash struct to accept iced::Length for dimension
- Add Back and Home navigation messages
- Implement scrollable container and button interactions
2025-11-19 02:16:47 +05:30
28 changed files with 8744 additions and 222 deletions

356
Cargo.lock generated
View File

@@ -172,22 +172,22 @@ dependencies = [
[[package]] [[package]]
name = "anstyle-query" name = "anstyle-query"
version = "1.1.4" version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.10" version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -200,6 +200,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
name = "api" name = "api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bytes",
"iref", "iref",
"jiff", "jiff",
"reqwest", "reqwest",
@@ -211,6 +212,7 @@ dependencies = [
"tokio-test", "tokio-test",
"toml 0.9.8", "toml 0.9.8",
"tracing", "tracing",
"url",
"uuid", "uuid",
] ]
@@ -592,6 +594,12 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atomic_refcell"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -647,6 +655,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.71.1" version = "0.71.1"
@@ -908,9 +925,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.1" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]] [[package]]
name = "calloop" name = "calloop"
@@ -992,9 +1009,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.45" version = "1.2.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -1026,6 +1043,16 @@ dependencies = [
"nom 7.1.3", "nom 7.1.3",
] ]
[[package]]
name = "cfg-expr"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9acd0bdbbf4b2612d09f52ba61da432140cb10930354079d0d53fafc12968726"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@@ -1098,9 +1125,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.51" version = "4.5.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -1108,9 +1135,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.51" version = "4.5.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -2073,9 +2100,9 @@ dependencies = [
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]] [[package]]
name = "flate2" name = "flate2"
@@ -2428,6 +2455,19 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "gio-sys"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "gl_generator" name = "gl_generator"
version = "0.14.0" version = "0.14.0"
@@ -2445,6 +2485,50 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
[[package]]
name = "glib"
version = "0.20.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683"
dependencies = [
"bitflags 2.10.0",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys",
"glib-macros",
"glib-sys",
"gobject-sys",
"libc",
"memchr",
"smallvec",
]
[[package]]
name = "glib-macros"
version = "0.20.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145"
dependencies = [
"heck 0.5.0",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "glib-sys"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215"
dependencies = [
"libc",
"system-deps",
]
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.3" version = "0.3.3"
@@ -2497,6 +2581,17 @@ dependencies = [
"gl_generator", "gl_generator",
] ]
[[package]]
name = "gobject-sys"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda"
dependencies = [
"glib-sys",
"libc",
"system-deps",
]
[[package]] [[package]]
name = "gpu-alloc" name = "gpu-alloc"
version = "0.6.0" version = "0.6.0"
@@ -2819,6 +2914,98 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681"
[[package]]
name = "gstreamer"
version = "0.23.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8757a87f3706560037a01a9f06a59fcc7bdb0864744dcf73546606e60c4316e1"
dependencies = [
"cfg-if",
"futures-channel",
"futures-core",
"futures-util",
"glib",
"gstreamer-sys",
"itertools 0.14.0",
"libc",
"muldiv",
"num-integer",
"num-rational",
"once_cell",
"option-operations",
"paste",
"pin-project-lite",
"smallvec",
"thiserror 2.0.17",
]
[[package]]
name = "gstreamer-app"
version = "0.23.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9a883eb21aebcf1289158225c05f7aea5da6ecf71fa7f0ff1ce4d25baf004e"
dependencies = [
"futures-core",
"futures-sink",
"glib",
"gstreamer",
"gstreamer-app-sys",
"gstreamer-base",
"libc",
]
[[package]]
name = "gstreamer-app-sys"
version = "0.23.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f7ef838306fe51852d503a14dc79ac42de005a59008a05098de3ecdaf05455"
dependencies = [
"glib-sys",
"gstreamer-base-sys",
"gstreamer-sys",
"libc",
"system-deps",
]
[[package]]
name = "gstreamer-base"
version = "0.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f19a74fd04ffdcb847dd322640f2cf520897129d00a7bcb92fd62a63f3e27404"
dependencies = [
"atomic_refcell",
"cfg-if",
"glib",
"gstreamer",
"gstreamer-base-sys",
"libc",
]
[[package]]
name = "gstreamer-base-sys"
version = "0.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f2fb0037b6d3c5b51f60dea11e667910f33be222308ca5a101450018a09840"
dependencies = [
"glib-sys",
"gobject-sys",
"gstreamer-sys",
"libc",
"system-deps",
]
[[package]]
name = "gstreamer-sys"
version = "0.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feea73b4d92dbf9c24a203c9cd0bcc740d584f6b5960d5faf359febf288919b2"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
]
[[package]] [[package]]
name = "guillotiere" name = "guillotiere"
version = "0.6.2" version = "0.6.2"
@@ -3007,9 +3194,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.8.0" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -3062,9 +3249,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.17" version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@@ -3083,7 +3270,7 @@ dependencies = [
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
"windows-registry 0.5.3", "windows-registry 0.6.1",
] ]
[[package]] [[package]]
@@ -3093,6 +3280,7 @@ source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276
dependencies = [ dependencies = [
"iced_core", "iced_core",
"iced_debug", "iced_debug",
"iced_devtools",
"iced_futures", "iced_futures",
"iced_renderer", "iced_renderer",
"iced_runtime", "iced_runtime",
@@ -3102,6 +3290,21 @@ dependencies = [
"thiserror 2.0.17", "thiserror 2.0.17",
] ]
[[package]]
name = "iced_beacon"
version = "0.14.0-dev"
source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498"
dependencies = [
"bincode",
"futures",
"iced_core",
"log",
"semver",
"serde",
"thiserror 2.0.17",
"tokio",
]
[[package]] [[package]]
name = "iced_core" name = "iced_core"
version = "0.14.0-dev" version = "0.14.0-dev"
@@ -3114,6 +3317,7 @@ dependencies = [
"log", "log",
"num-traits", "num-traits",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"serde",
"smol_str", "smol_str",
"thiserror 2.0.17", "thiserror 2.0.17",
"web-time", "web-time",
@@ -3124,11 +3328,23 @@ name = "iced_debug"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498" source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498"
dependencies = [ dependencies = [
"iced_beacon",
"iced_core", "iced_core",
"iced_futures", "iced_futures",
"log", "log",
] ]
[[package]]
name = "iced_devtools"
version = "0.14.0-dev"
source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498"
dependencies = [
"iced_debug",
"iced_program",
"iced_widget",
"log",
]
[[package]] [[package]]
name = "iced_futures" name = "iced_futures"
version = "0.14.0-dev" version = "0.14.0-dev"
@@ -3194,6 +3410,7 @@ dependencies = [
"iced_core", "iced_core",
"iced_futures", "iced_futures",
"raw-window-handle", "raw-window-handle",
"sipper",
"thiserror 2.0.17", "thiserror 2.0.17",
] ]
@@ -3213,6 +3430,21 @@ dependencies = [
"tiny-skia", "tiny-skia",
] ]
[[package]]
name = "iced_video_player"
version = "0.6.0"
dependencies = [
"glib",
"gstreamer",
"gstreamer-app",
"gstreamer-base",
"iced",
"iced_wgpu",
"log",
"thiserror 1.0.69",
"url",
]
[[package]] [[package]]
name = "iced_wgpu" name = "iced_wgpu"
version = "0.14.0-dev" version = "0.14.0-dev"
@@ -3908,9 +4140,9 @@ dependencies = [
[[package]] [[package]]
name = "lyon_geom" name = "lyon_geom"
version = "1.0.17" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" checksum = "e260b6de923e6e47adfedf6243013a7a874684165a6a277594ee3906021b2343"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"euclid", "euclid",
@@ -4095,6 +4327,12 @@ dependencies = [
"pxfm", "pxfm",
] ]
[[package]]
name = "muldiv"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"
[[package]] [[package]]
name = "mundy" name = "mundy"
version = "0.2.1" version = "0.2.1"
@@ -4875,9 +5113,9 @@ dependencies = [
[[package]] [[package]]
name = "open" name = "open"
version = "5.3.2" version = "5.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
dependencies = [ dependencies = [
"is-wsl", "is-wsl",
"libc", "libc",
@@ -4934,6 +5172,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "option-operations"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0"
dependencies = [
"paste",
]
[[package]] [[package]]
name = "orbclient" name = "orbclient"
version = "0.3.49" version = "0.3.49"
@@ -5499,9 +5746,9 @@ checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab"
[[package]] [[package]]
name = "rangemap" name = "rangemap"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" checksum = "acbbbbea733ec66275512d0b9694f34102e7d5406fdbe2ad8d21b28dce92887c"
[[package]] [[package]]
name = "rav1e" name = "rav1e"
@@ -6085,6 +6332,10 @@ name = "semver"
version = "1.0.27" version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
dependencies = [
"serde",
"serde_core",
]
[[package]] [[package]]
name = "serde" name = "serde"
@@ -6275,6 +6526,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "sipper"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bccb4192828b3d9a08e0b5a73f17795080dfb278b50190216e3ae2132cf4f95"
dependencies = [
"futures",
"pin-project-lite",
]
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.37.0" version = "0.37.0"
@@ -6762,6 +7023,19 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "system-deps"
version = "7.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
dependencies = [
"cfg-expr",
"heck 0.5.0",
"pkg-config",
"toml 0.9.8",
"version-compare",
]
[[package]] [[package]]
name = "taffy" name = "taffy"
version = "0.9.0" version = "0.9.0"
@@ -6786,6 +7060,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.23.0" version = "3.23.0"
@@ -7319,9 +7599,14 @@ version = "0.1.0"
dependencies = [ dependencies = [
"api", "api",
"blurhash", "blurhash",
"bytes",
"gpui_util", "gpui_util",
"iced", "iced",
"iced_video_player",
"reqwest",
"tap",
"tracing", "tracing",
"url",
"uuid", "uuid",
] ]
@@ -7544,6 +7829,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@@ -8301,6 +8592,17 @@ dependencies = [
"windows-strings 0.4.2", "windows-strings 0.4.2",
] ]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.2.0" version = "0.2.0"

View File

@@ -1,5 +1,23 @@
[workspace] [workspace]
members = [".", "api", "typegen", "ui-gpui", "ui-iced"] members = [
".",
"api",
"typegen",
"ui-gpui",
"ui-iced",
"crates/iced_video_player",
]
[workspace.dependencies]
iced = { git = "https://github.com/iced-rs/iced", features = [
"advanced",
"canvas",
"image",
"sipper",
"tokio",
"debug",
] }
iced_wgpu = { git = "https://github.com/iced-rs/iced" }
iced_video_player = { path = "crates/iced_video_player" }
[package] [package]
name = "jello" name = "jello"

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bytes = "1.11.0"
iref = { version = "3.2.2", features = ["serde"] } iref = { version = "3.2.2", features = ["serde"] }
jiff = { version = "0.2.16", features = ["serde"] } jiff = { version = "0.2.16", features = ["serde"] }
reqwest = { version = "0.12.24", features = ["json"] } reqwest = { version = "0.12.24", features = ["json"] }
@@ -14,6 +15,7 @@ thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["fs"] } tokio = { version = "1.48.0", features = ["fs"] }
toml = "0.9.8" toml = "0.9.8"
tracing = "0.1.41" tracing = "0.1.41"
url = "2.5.7"
uuid = { version = "1.18.1", features = ["serde"] } uuid = { version = "1.18.1", features = ["serde"] }
[dev-dependencies] [dev-dependencies]

View File

@@ -17,8 +17,16 @@ pub async fn main() {
for item in items { for item in items {
println!("{}: {:?}", item.id, item.name); println!("{}: {:?}", item.id, item.name);
let items = jellyfin.items(item.id).await.expect("Items"); let items = jellyfin.items(item.id).await.expect("Items");
for item in items { std::fs::write(
println!(" {}: {:?}", item.id, item.name); format!("items_{:?}.json", item.name),
} serde_json::to_string_pretty(&items).expect("Serialize items"),
);
// for item in items {
// println!(" {}: {:?}", item.id, item.name);
// std::fs::write(
// format!("item_{}.json", item.id),
// serde_json::to_string_pretty(&item).expect("Serialize item"),
// );
// }
} }
} }

View File

@@ -6013,7 +6013,6 @@ pub struct XbmcMetadataOptions {
pub enable_extra_thumbs_duplication: bool, pub enable_extra_thumbs_duplication: bool,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ApiName { pub enum ApiName {
#[serde(rename = "Mal")] #[serde(rename = "Mal")]
Mal, Mal,
@@ -6030,7 +6029,6 @@ pub enum ApiName {
} }
/// An enum representing formats of spatial audio. /// An enum representing formats of spatial audio.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum AudioSpatialFormat { pub enum AudioSpatialFormat {
#[serde(rename = "None")] #[serde(rename = "None")]
None, None,
@@ -6041,7 +6039,6 @@ pub enum AudioSpatialFormat {
} }
/// The base item kind. /// The base item kind.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum BaseItemKind { pub enum BaseItemKind {
#[serde(rename = "AggregateFolder")] #[serde(rename = "AggregateFolder")]
AggregateFolder, AggregateFolder,
@@ -6119,7 +6116,6 @@ pub enum BaseItemKind {
Year, Year,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ChannelItemSortField { pub enum ChannelItemSortField {
#[serde(rename = "Name")] #[serde(rename = "Name")]
Name, Name,
@@ -6137,7 +6133,6 @@ pub enum ChannelItemSortField {
CommunityPlayCount, CommunityPlayCount,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ChannelMediaContentType { pub enum ChannelMediaContentType {
#[serde(rename = "Clip")] #[serde(rename = "Clip")]
Clip, Clip,
@@ -6157,7 +6152,6 @@ pub enum ChannelMediaContentType {
TvExtra, TvExtra,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ChannelMediaType { pub enum ChannelMediaType {
#[serde(rename = "Audio")] #[serde(rename = "Audio")]
Audio, Audio,
@@ -6168,7 +6162,6 @@ pub enum ChannelMediaType {
} }
/// Enum ChannelType. /// Enum ChannelType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ChannelType { pub enum ChannelType {
#[serde(rename = "TV")] #[serde(rename = "TV")]
Tv, Tv,
@@ -6176,7 +6169,6 @@ pub enum ChannelType {
Radio, Radio,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum CodecType { pub enum CodecType {
#[serde(rename = "Video")] #[serde(rename = "Video")]
Video, Video,
@@ -6187,7 +6179,6 @@ pub enum CodecType {
} }
/// Collection type. /// Collection type.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum CollectionType { pub enum CollectionType {
#[serde(rename = "unknown")] #[serde(rename = "unknown")]
Unknown, Unknown,
@@ -6218,7 +6209,6 @@ pub enum CollectionType {
} }
/// The collection type options. /// The collection type options.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum CollectionTypeOptions { pub enum CollectionTypeOptions {
#[serde(rename = "movies")] #[serde(rename = "movies")]
Movies, Movies,
@@ -6238,7 +6228,6 @@ pub enum CollectionTypeOptions {
Mixed, Mixed,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DayOfWeek { pub enum DayOfWeek {
#[serde(rename = "Sunday")] #[serde(rename = "Sunday")]
Sunday, Sunday,
@@ -6256,7 +6245,6 @@ pub enum DayOfWeek {
Saturday, Saturday,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DayPattern { pub enum DayPattern {
#[serde(rename = "Daily")] #[serde(rename = "Daily")]
Daily, Daily,
@@ -6267,7 +6255,6 @@ pub enum DayPattern {
} }
/// Enum containing deinterlace methods. /// Enum containing deinterlace methods.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DeinterlaceMethod { pub enum DeinterlaceMethod {
#[serde(rename = "yadif")] #[serde(rename = "yadif")]
Yadif, Yadif,
@@ -6275,7 +6262,6 @@ pub enum DeinterlaceMethod {
Bwdif, Bwdif,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DlnaProfileType { pub enum DlnaProfileType {
#[serde(rename = "Audio")] #[serde(rename = "Audio")]
Audio, Audio,
@@ -6290,7 +6276,6 @@ pub enum DlnaProfileType {
} }
/// An enum representing an algorithm to downmix surround sound to stereo. /// An enum representing an algorithm to downmix surround sound to stereo.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DownMixStereoAlgorithms { pub enum DownMixStereoAlgorithms {
#[serde(rename = "None")] #[serde(rename = "None")]
None, None,
@@ -6305,7 +6290,6 @@ pub enum DownMixStereoAlgorithms {
} }
/// An enum that represents a day of the week, weekdays, weekends, or all days. /// An enum that represents a day of the week, weekdays, weekends, or all days.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DynamicDayOfWeek { pub enum DynamicDayOfWeek {
#[serde(rename = "Sunday")] #[serde(rename = "Sunday")]
Sunday, Sunday,
@@ -6330,7 +6314,6 @@ pub enum DynamicDayOfWeek {
} }
/// An enum representing the options to disable embedded subs. /// An enum representing the options to disable embedded subs.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum EmbeddedSubtitleOptions { pub enum EmbeddedSubtitleOptions {
#[serde(rename = "AllowAll")] #[serde(rename = "AllowAll")]
AllowAll, AllowAll,
@@ -6343,7 +6326,6 @@ pub enum EmbeddedSubtitleOptions {
} }
/// Enum containing encoder presets. /// Enum containing encoder presets.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum EncoderPreset { pub enum EncoderPreset {
#[serde(rename = "auto")] #[serde(rename = "auto")]
Auto, Auto,
@@ -6369,7 +6351,6 @@ pub enum EncoderPreset {
Ultrafast, Ultrafast,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum EncodingContext { pub enum EncodingContext {
#[serde(rename = "Streaming")] #[serde(rename = "Streaming")]
Streaming, Streaming,
@@ -6378,7 +6359,6 @@ pub enum EncodingContext {
} }
/// The specific media type of an MediaBrowser.Model.Providers.ExternalIdInfo. /// The specific media type of an MediaBrowser.Model.Providers.ExternalIdInfo.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ExternalIdMediaType { pub enum ExternalIdMediaType {
#[serde(rename = "Album")] #[serde(rename = "Album")]
Album, Album,
@@ -6408,7 +6388,6 @@ pub enum ExternalIdMediaType {
Book, Book,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ExtraType { pub enum ExtraType {
#[serde(rename = "Unknown")] #[serde(rename = "Unknown")]
Unknown, Unknown,
@@ -6437,7 +6416,6 @@ pub enum ExtraType {
} }
/// Enum FileSystemEntryType. /// Enum FileSystemEntryType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum FileSystemEntryType { pub enum FileSystemEntryType {
#[serde(rename = "File")] #[serde(rename = "File")]
File, File,
@@ -6449,7 +6427,6 @@ pub enum FileSystemEntryType {
NetworkShare, NetworkShare,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ForgotPasswordAction { pub enum ForgotPasswordAction {
#[serde(rename = "ContactAdmin")] #[serde(rename = "ContactAdmin")]
ContactAdmin, ContactAdmin,
@@ -6460,7 +6437,6 @@ pub enum ForgotPasswordAction {
} }
/// This exists simply to identify a set of known commands. /// This exists simply to identify a set of known commands.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum GeneralCommandType { pub enum GeneralCommandType {
#[serde(rename = "MoveUp")] #[serde(rename = "MoveUp")]
MoveUp, MoveUp,
@@ -6551,7 +6527,6 @@ pub enum GeneralCommandType {
} }
/// Enum GroupQueueMode. /// Enum GroupQueueMode.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum GroupQueueMode { pub enum GroupQueueMode {
#[serde(rename = "Queue")] #[serde(rename = "Queue")]
Queue, Queue,
@@ -6560,7 +6535,6 @@ pub enum GroupQueueMode {
} }
/// Enum GroupRepeatMode. /// Enum GroupRepeatMode.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum GroupRepeatMode { pub enum GroupRepeatMode {
#[serde(rename = "RepeatOne")] #[serde(rename = "RepeatOne")]
RepeatOne, RepeatOne,
@@ -6571,7 +6545,6 @@ pub enum GroupRepeatMode {
} }
/// Enum GroupShuffleMode. /// Enum GroupShuffleMode.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum GroupShuffleMode { pub enum GroupShuffleMode {
#[serde(rename = "Sorted")] #[serde(rename = "Sorted")]
Sorted, Sorted,
@@ -6580,7 +6553,6 @@ pub enum GroupShuffleMode {
} }
/// Enum GroupState. /// Enum GroupState.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum GroupStateType { pub enum GroupStateType {
#[serde(rename = "Idle")] #[serde(rename = "Idle")]
Idle, Idle,
@@ -6593,7 +6565,6 @@ pub enum GroupStateType {
} }
/// Enum GroupUpdateType. /// Enum GroupUpdateType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum GroupUpdateType { pub enum GroupUpdateType {
#[serde(rename = "UserJoined")] #[serde(rename = "UserJoined")]
UserJoined, UserJoined,
@@ -6620,7 +6591,6 @@ pub enum GroupUpdateType {
} }
/// Enum containing hardware acceleration types. /// Enum containing hardware acceleration types.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum HardwareAccelerationType { pub enum HardwareAccelerationType {
#[serde(rename = "none")] #[serde(rename = "none")]
None, None,
@@ -6641,7 +6611,6 @@ pub enum HardwareAccelerationType {
} }
/// Enum ImageOutputFormat. /// Enum ImageOutputFormat.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ImageFormat { pub enum ImageFormat {
#[serde(rename = "Bmp")] #[serde(rename = "Bmp")]
Bmp, Bmp,
@@ -6657,7 +6626,6 @@ pub enum ImageFormat {
Svg, Svg,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ImageOrientation { pub enum ImageOrientation {
#[serde(rename = "TopLeft")] #[serde(rename = "TopLeft")]
TopLeft, TopLeft,
@@ -6678,7 +6646,6 @@ pub enum ImageOrientation {
} }
/// Enum ImageResolution. /// Enum ImageResolution.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ImageResolution { pub enum ImageResolution {
#[serde(rename = "MatchSource")] #[serde(rename = "MatchSource")]
MatchSource, MatchSource,
@@ -6700,7 +6667,6 @@ pub enum ImageResolution {
P2160, P2160,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ImageSavingConvention { pub enum ImageSavingConvention {
#[serde(rename = "Legacy")] #[serde(rename = "Legacy")]
Legacy, Legacy,
@@ -6709,7 +6675,6 @@ pub enum ImageSavingConvention {
} }
/// Enum ImageType. /// Enum ImageType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ImageType { pub enum ImageType {
#[serde(rename = "Primary")] #[serde(rename = "Primary")]
Primary, Primary,
@@ -6740,7 +6705,6 @@ pub enum ImageType {
} }
/// Enum IsoType. /// Enum IsoType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum IsoType { pub enum IsoType {
#[serde(rename = "Dvd")] #[serde(rename = "Dvd")]
Dvd, Dvd,
@@ -6749,7 +6713,6 @@ pub enum IsoType {
} }
/// Used to control the data that gets attached to DtoBaseItems. /// Used to control the data that gets attached to DtoBaseItems.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ItemFields { pub enum ItemFields {
#[serde(rename = "AirTime")] #[serde(rename = "AirTime")]
AirTime, AirTime,
@@ -6874,7 +6837,6 @@ pub enum ItemFields {
} }
/// Enum ItemFilter. /// Enum ItemFilter.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ItemFilter { pub enum ItemFilter {
#[serde(rename = "IsFolder")] #[serde(rename = "IsFolder")]
IsFolder, IsFolder,
@@ -6897,7 +6859,6 @@ pub enum ItemFilter {
} }
/// These represent sort orders. /// These represent sort orders.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ItemSortBy { pub enum ItemSortBy {
#[serde(rename = "Default")] #[serde(rename = "Default")]
Default, Default,
@@ -6965,7 +6926,6 @@ pub enum ItemSortBy {
SearchScore, SearchScore,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum KeepUntil { pub enum KeepUntil {
#[serde(rename = "UntilDeleted")] #[serde(rename = "UntilDeleted")]
UntilDeleted, UntilDeleted,
@@ -6977,7 +6937,6 @@ pub enum KeepUntil {
UntilDate, UntilDate,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum LiveTvServiceStatus { pub enum LiveTvServiceStatus {
#[serde(rename = "Ok")] #[serde(rename = "Ok")]
Ok, Ok,
@@ -6986,7 +6945,6 @@ pub enum LiveTvServiceStatus {
} }
/// Enum LocationType. /// Enum LocationType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum LocationType { pub enum LocationType {
#[serde(rename = "FileSystem")] #[serde(rename = "FileSystem")]
FileSystem, FileSystem,
@@ -6998,7 +6956,6 @@ pub enum LocationType {
Offline, Offline,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum LogLevel { pub enum LogLevel {
#[serde(rename = "Trace")] #[serde(rename = "Trace")]
Trace, Trace,
@@ -7016,7 +6973,6 @@ pub enum LogLevel {
None, None,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MediaProtocol { pub enum MediaProtocol {
#[serde(rename = "File")] #[serde(rename = "File")]
File, File,
@@ -7035,7 +6991,6 @@ pub enum MediaProtocol {
} }
/// Defines the types of content an individual Jellyfin.Data.Entities.MediaSegment represents. /// Defines the types of content an individual Jellyfin.Data.Entities.MediaSegment represents.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MediaSegmentType { pub enum MediaSegmentType {
#[serde(rename = "Unknown")] #[serde(rename = "Unknown")]
Unknown, Unknown,
@@ -7051,7 +7006,6 @@ pub enum MediaSegmentType {
Intro, Intro,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MediaSourceType { pub enum MediaSourceType {
#[serde(rename = "Default")] #[serde(rename = "Default")]
Default, Default,
@@ -7063,7 +7017,6 @@ pub enum MediaSourceType {
/** Media streaming protocol. /** Media streaming protocol.
Lowercase for backwards compatibility.*/ Lowercase for backwards compatibility.*/
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MediaStreamProtocol { pub enum MediaStreamProtocol {
#[serde(rename = "http")] #[serde(rename = "http")]
Http, Http,
@@ -7072,7 +7025,6 @@ pub enum MediaStreamProtocol {
} }
/// Enum MediaStreamType. /// Enum MediaStreamType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MediaStreamType { pub enum MediaStreamType {
#[serde(rename = "Audio")] #[serde(rename = "Audio")]
Audio, Audio,
@@ -7089,7 +7041,6 @@ pub enum MediaStreamType {
} }
/// Media types. /// Media types.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MediaType { pub enum MediaType {
#[serde(rename = "Unknown")] #[serde(rename = "Unknown")]
Unknown, Unknown,
@@ -7104,7 +7055,6 @@ pub enum MediaType {
} }
/// Enum MetadataFields. /// Enum MetadataFields.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MetadataField { pub enum MetadataField {
#[serde(rename = "Cast")] #[serde(rename = "Cast")]
Cast, Cast,
@@ -7126,7 +7076,6 @@ pub enum MetadataField {
OfficialRating, OfficialRating,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum MetadataRefreshMode { pub enum MetadataRefreshMode {
#[serde(rename = "None")] #[serde(rename = "None")]
None, None,
@@ -7138,7 +7087,6 @@ pub enum MetadataRefreshMode {
FullRefresh, FullRefresh,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ParameterInclude { pub enum ParameterInclude {
#[serde(rename = "ProviderList")] #[serde(rename = "ProviderList")]
ProviderList, ProviderList,
@@ -7151,7 +7099,6 @@ pub enum ParameterInclude {
} }
/// The person kind. /// The person kind.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PersonKind { pub enum PersonKind {
#[serde(rename = "Unknown")] #[serde(rename = "Unknown")]
Unknown, Unknown,
@@ -7205,7 +7152,6 @@ pub enum PersonKind {
Translator, Translator,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlayAccess { pub enum PlayAccess {
#[serde(rename = "Full")] #[serde(rename = "Full")]
Full, Full,
@@ -7213,7 +7159,6 @@ pub enum PlayAccess {
None, None,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlaybackErrorCode { pub enum PlaybackErrorCode {
#[serde(rename = "NotAllowed")] #[serde(rename = "NotAllowed")]
NotAllowed, NotAllowed,
@@ -7224,7 +7169,6 @@ pub enum PlaybackErrorCode {
} }
/// Enum PlaybackOrder. /// Enum PlaybackOrder.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlaybackOrder { pub enum PlaybackOrder {
#[serde(rename = "Default")] #[serde(rename = "Default")]
Default, Default,
@@ -7233,7 +7177,6 @@ pub enum PlaybackOrder {
} }
/// Enum PlaybackRequestType. /// Enum PlaybackRequestType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlaybackRequestType { pub enum PlaybackRequestType {
#[serde(rename = "Play")] #[serde(rename = "Play")]
Play, Play,
@@ -7272,7 +7215,6 @@ pub enum PlaybackRequestType {
} }
/// Enum PlayCommand. /// Enum PlayCommand.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlayCommand { pub enum PlayCommand {
#[serde(rename = "PlayNow")] #[serde(rename = "PlayNow")]
PlayNow, PlayNow,
@@ -7286,7 +7228,6 @@ pub enum PlayCommand {
PlayShuffle, PlayShuffle,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlayMethod { pub enum PlayMethod {
#[serde(rename = "Transcode")] #[serde(rename = "Transcode")]
Transcode, Transcode,
@@ -7297,7 +7238,6 @@ pub enum PlayMethod {
} }
/// Enum PlayQueueUpdateReason. /// Enum PlayQueueUpdateReason.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlayQueueUpdateReason { pub enum PlayQueueUpdateReason {
#[serde(rename = "NewPlaylist")] #[serde(rename = "NewPlaylist")]
NewPlaylist, NewPlaylist,
@@ -7322,7 +7262,6 @@ pub enum PlayQueueUpdateReason {
} }
/// Enum PlaystateCommand. /// Enum PlaystateCommand.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PlaystateCommand { pub enum PlaystateCommand {
#[serde(rename = "Stop")] #[serde(rename = "Stop")]
Stop, Stop,
@@ -7345,7 +7284,6 @@ pub enum PlaystateCommand {
} }
/// Plugin load status. /// Plugin load status.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum PluginStatus { pub enum PluginStatus {
#[serde(rename = "Active")] #[serde(rename = "Active")]
Active, Active,
@@ -7363,7 +7301,6 @@ pub enum PluginStatus {
Disabled, Disabled,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ProcessPriorityClass { pub enum ProcessPriorityClass {
#[serde(rename = "Normal")] #[serde(rename = "Normal")]
Normal, Normal,
@@ -7379,7 +7316,6 @@ pub enum ProcessPriorityClass {
AboveNormal, AboveNormal,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ProfileConditionType { pub enum ProfileConditionType {
#[serde(rename = "Equals")] #[serde(rename = "Equals")]
Equals, Equals,
@@ -7393,7 +7329,6 @@ pub enum ProfileConditionType {
EqualsAny, EqualsAny,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ProfileConditionValue { pub enum ProfileConditionValue {
#[serde(rename = "AudioChannels")] #[serde(rename = "AudioChannels")]
AudioChannels, AudioChannels,
@@ -7445,7 +7380,6 @@ pub enum ProfileConditionValue {
VideoRangeType, VideoRangeType,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ProgramAudio { pub enum ProgramAudio {
#[serde(rename = "Mono")] #[serde(rename = "Mono")]
Mono, Mono,
@@ -7461,7 +7395,6 @@ pub enum ProgramAudio {
Atmos, Atmos,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum RatingType { pub enum RatingType {
#[serde(rename = "Score")] #[serde(rename = "Score")]
Score, Score,
@@ -7469,7 +7402,6 @@ pub enum RatingType {
Likes, Likes,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum RecommendationType { pub enum RecommendationType {
#[serde(rename = "SimilarToRecentlyPlayed")] #[serde(rename = "SimilarToRecentlyPlayed")]
SimilarToRecentlyPlayed, SimilarToRecentlyPlayed,
@@ -7485,7 +7417,6 @@ pub enum RecommendationType {
HasLikedActor, HasLikedActor,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum RecordingStatus { pub enum RecordingStatus {
#[serde(rename = "New")] #[serde(rename = "New")]
New, New,
@@ -7503,7 +7434,6 @@ pub enum RecordingStatus {
Error, Error,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum RepeatMode { pub enum RepeatMode {
#[serde(rename = "RepeatNone")] #[serde(rename = "RepeatNone")]
RepeatNone, RepeatNone,
@@ -7514,7 +7444,6 @@ pub enum RepeatMode {
} }
/// An enum representing the axis that should be scrolled. /// An enum representing the axis that should be scrolled.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ScrollDirection { pub enum ScrollDirection {
#[serde(rename = "Horizontal")] #[serde(rename = "Horizontal")]
Horizontal, Horizontal,
@@ -7523,7 +7452,6 @@ pub enum ScrollDirection {
} }
/// Enum SendCommandType. /// Enum SendCommandType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SendCommandType { pub enum SendCommandType {
#[serde(rename = "Unpause")] #[serde(rename = "Unpause")]
Unpause, Unpause,
@@ -7536,7 +7464,6 @@ pub enum SendCommandType {
} }
/// The status of a series. /// The status of a series.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SeriesStatus { pub enum SeriesStatus {
#[serde(rename = "Continuing")] #[serde(rename = "Continuing")]
Continuing, Continuing,
@@ -7547,7 +7474,6 @@ pub enum SeriesStatus {
} }
/// The different kinds of messages that are used in the WebSocket api. /// The different kinds of messages that are used in the WebSocket api.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SessionMessageType { pub enum SessionMessageType {
#[serde(rename = "ForceKeepAlive")] #[serde(rename = "ForceKeepAlive")]
ForceKeepAlive, ForceKeepAlive,
@@ -7620,7 +7546,6 @@ pub enum SessionMessageType {
} }
/// An enum representing the sorting order. /// An enum representing the sorting order.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SortOrder { pub enum SortOrder {
#[serde(rename = "Ascending")] #[serde(rename = "Ascending")]
Ascending, Ascending,
@@ -7628,7 +7553,6 @@ pub enum SortOrder {
Descending, Descending,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum Status { pub enum Status {
#[serde(rename = "Completed")] #[serde(rename = "Completed")]
Completed, Completed,
@@ -7639,7 +7563,6 @@ pub enum Status {
} }
/// Delivery method to use during playback of a specific subtitle format. /// Delivery method to use during playback of a specific subtitle format.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SubtitleDeliveryMethod { pub enum SubtitleDeliveryMethod {
#[serde(rename = "Encode")] #[serde(rename = "Encode")]
Encode, Encode,
@@ -7654,7 +7577,6 @@ pub enum SubtitleDeliveryMethod {
} }
/// An enum representing a subtitle playback mode. /// An enum representing a subtitle playback mode.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SubtitlePlaybackMode { pub enum SubtitlePlaybackMode {
#[serde(rename = "Default")] #[serde(rename = "Default")]
Default, Default,
@@ -7668,7 +7590,6 @@ pub enum SubtitlePlaybackMode {
Smart, Smart,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SyncAction { pub enum SyncAction {
#[serde(rename = "UpdateProvider")] #[serde(rename = "UpdateProvider")]
UpdateProvider, UpdateProvider,
@@ -7677,7 +7598,6 @@ pub enum SyncAction {
} }
/// Enum SyncPlayUserAccessType. /// Enum SyncPlayUserAccessType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum SyncPlayUserAccessType { pub enum SyncPlayUserAccessType {
#[serde(rename = "CreateAndJoinGroups")] #[serde(rename = "CreateAndJoinGroups")]
CreateAndJoinGroups, CreateAndJoinGroups,
@@ -7688,7 +7608,6 @@ pub enum SyncPlayUserAccessType {
} }
/// Enum TaskCompletionStatus. /// Enum TaskCompletionStatus.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TaskCompletionStatus { pub enum TaskCompletionStatus {
#[serde(rename = "Completed")] #[serde(rename = "Completed")]
Completed, Completed,
@@ -7701,7 +7620,6 @@ pub enum TaskCompletionStatus {
} }
/// Enum TaskState. /// Enum TaskState.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TaskState { pub enum TaskState {
#[serde(rename = "Idle")] #[serde(rename = "Idle")]
Idle, Idle,
@@ -7712,7 +7630,6 @@ pub enum TaskState {
} }
/// Enum containing tonemapping algorithms. /// Enum containing tonemapping algorithms.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TonemappingAlgorithm { pub enum TonemappingAlgorithm {
#[serde(rename = "none")] #[serde(rename = "none")]
None, None,
@@ -7733,7 +7650,6 @@ pub enum TonemappingAlgorithm {
} }
/// Enum containing tonemapping modes. /// Enum containing tonemapping modes.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TonemappingMode { pub enum TonemappingMode {
#[serde(rename = "auto")] #[serde(rename = "auto")]
Auto, Auto,
@@ -7748,7 +7664,6 @@ pub enum TonemappingMode {
} }
/// Enum containing tonemapping ranges. /// Enum containing tonemapping ranges.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TonemappingRange { pub enum TonemappingRange {
#[serde(rename = "auto")] #[serde(rename = "auto")]
Auto, Auto,
@@ -7758,7 +7673,6 @@ pub enum TonemappingRange {
Pc, Pc,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TranscodeReason { pub enum TranscodeReason {
#[serde(rename = "ContainerNotSupported")] #[serde(rename = "ContainerNotSupported")]
ContainerNotSupported, ContainerNotSupported,
@@ -7814,7 +7728,6 @@ pub enum TranscodeReason {
VideoCodecTagNotSupported, VideoCodecTagNotSupported,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TranscodeSeekInfo { pub enum TranscodeSeekInfo {
#[serde(rename = "Auto")] #[serde(rename = "Auto")]
Auto, Auto,
@@ -7822,7 +7735,6 @@ pub enum TranscodeSeekInfo {
Bytes, Bytes,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TransportStreamTimestamp { pub enum TransportStreamTimestamp {
#[serde(rename = "None")] #[serde(rename = "None")]
None, None,
@@ -7833,7 +7745,6 @@ pub enum TransportStreamTimestamp {
} }
/// Enum TrickplayScanBehavior. /// Enum TrickplayScanBehavior.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TrickplayScanBehavior { pub enum TrickplayScanBehavior {
#[serde(rename = "Blocking")] #[serde(rename = "Blocking")]
Blocking, Blocking,
@@ -7842,7 +7753,6 @@ pub enum TrickplayScanBehavior {
} }
/// An enum representing an unrated item. /// An enum representing an unrated item.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum UnratedItem { pub enum UnratedItem {
#[serde(rename = "Movie")] #[serde(rename = "Movie")]
Movie, Movie,
@@ -7864,7 +7774,6 @@ pub enum UnratedItem {
Other, Other,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum Video3DFormat { pub enum Video3DFormat {
#[serde(rename = "HalfSideBySide")] #[serde(rename = "HalfSideBySide")]
HalfSideBySide, HalfSideBySide,
@@ -7879,7 +7788,6 @@ pub enum Video3DFormat {
} }
/// An enum representing video ranges. /// An enum representing video ranges.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum VideoRange { pub enum VideoRange {
#[serde(rename = "Unknown")] #[serde(rename = "Unknown")]
Unknown, Unknown,
@@ -7890,7 +7798,6 @@ pub enum VideoRange {
} }
/// An enum representing types of video ranges. /// An enum representing types of video ranges.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum VideoRangeType { pub enum VideoRangeType {
#[serde(rename = "Unknown")] #[serde(rename = "Unknown")]
Unknown, Unknown,
@@ -7913,7 +7820,6 @@ pub enum VideoRangeType {
} }
/// Enum VideoType. /// Enum VideoType.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum VideoType { pub enum VideoType {
#[serde(rename = "VideoFile")] #[serde(rename = "VideoFile")]
VideoFile, VideoFile,

View File

@@ -185,6 +185,75 @@ impl JellyfinClient {
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?; let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
Ok(out.items) Ok(out.items)
} }
pub async fn search(&self, query: impl AsRef<str>) -> Result<Vec<jellyfin::BaseItemDto>> {
let text = &self
.request_builder(Method::GET, "Items/Search")
.query(&[("searchTerm", query.as_ref()), ("recursive", "true")])
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::BaseItemDtoQueryResult = serde_json::from_str(&text)?;
Ok(out.items)
}
pub async fn thumbnail(
&self,
item: uuid::Uuid,
image_type: jellyfin::ImageType,
) -> Result<bytes::Bytes> {
let uri = format!(
"Items/{}/Images/{}",
item,
serde_json::to_string(&image_type).expect("Failed to serialize image type")
);
let bytes = self
.request_builder(Method::GET, uri)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
Ok(bytes)
}
pub async fn playback_info(&self, item: uuid::Uuid) -> Result<jellyfin::PlaybackInfoDto> {
let uri = format!("Items/{}/PlaybackInfo", item);
let text = &self
.request_builder(Method::GET, uri)
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::PlaybackInfoDto = serde_json::from_str(&text)?;
Ok(out)
}
pub async fn user_data(&self, item: uuid::Uuid) -> Result<jellyfin::UserItemDataDto> {
let uri = format!("UserItems/{}/UserData", item);
let text = &self
.request_builder(Method::GET, uri)
.send()
.await?
.error_for_status()?
.text()
.await?;
let out: jellyfin::UserItemDataDto = serde_json::from_str(&text)?;
Ok(out)
}
pub fn stream_url(&self, item: uuid::Uuid) -> Result<url::Url> {
let stream_url = format!(
"{}/Videos/{}/stream?static=true",
self.config.server_url.as_str(),
item,
// item,
);
Ok(url::Url::parse(&stream_url).expect("Failed to parse stream URL"))
}
} }
// pub trait Item { // pub trait Item {

3
crates/iced_video_player/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
.direnv
.media

5217
crates/iced_video_player/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
[package]
name = "iced_video_player"
description = "A convenient video player widget for Iced"
homepage = "https://github.com/jazzfool/iced_video_player"
repository = "https://github.com/jazzfool/iced_video_player"
readme = "README.md"
keywords = ["gui", "iced", "video"]
categories = ["gui", "multimedia"]
version = "0.6.0"
authors = ["jazzfool"]
edition = "2021"
resolver = "2"
license = "MIT OR Apache-2.0"
exclude = [".media/test.mp4"]
[dependencies]
iced = { git = "https://github.com/iced-rs/iced", features = [
"image",
"advanced",
"wgpu",
] }
iced_wgpu = { git = "https://github.com/iced-rs/iced" }
gstreamer = "0.23"
gstreamer-app = "0.23" # appsink
gstreamer-base = "0.23" # basesrc
glib = "0.20" # gobject traits and error type
log = "0.4"
thiserror = "1"
url = "2" # media uri
[package.metadata.nix]
systems = ["x86_64-linux"]
app = true
build = true
runtimeLibs = [
"vulkan-loader",
"wayland",
"wayland-protocols",
"libxkbcommon",
"xorg.libX11",
"xorg.libXrandr",
"xorg.libXi",
"gst_all_1.gstreamer",
"gst_all_1.gstreamermm",
"gst_all_1.gst-plugins-bad",
"gst_all_1.gst-plugins-ugly",
"gst_all_1.gst-plugins-good",
"gst_all_1.gst-plugins-base",
]
buildInputs = [
"libxkbcommon",
"gst_all_1.gstreamer",
"gst_all_1.gstreamermm",
"gst_all_1.gst-plugins-bad",
"gst_all_1.gst-plugins-ugly",
"gst_all_1.gst-plugins-good",
"gst_all_1.gst-plugins-base",
]
[package.metadata.docs.rs]
rustc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs"]
targets = ["wasm32-unknown-unknown"]

View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,23 @@
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.

View File

@@ -0,0 +1,64 @@
# Iced Video Player Widget
Composable component to play videos in any Iced application built on the excellent GStreamer library.
<img src=".media/screenshot.png" width="50%" />
## Overview
In general, this supports anything that [`gstreamer/playbin`](https://gstreamer.freedesktop.org/documentation/playback/playbin.html?gi-language=c) supports.
Features:
- Load video files from any file path **or URL** (support for streaming over network).
- Video buffering when streaming on a network.
- Audio support.
- Programmatic control.
- Can capture thumbnails from a set of timestamps.
- Good performance (i.e., comparable to other video players). GStreamer (with the right plugins) will perform hardware-accelerated decoding, and the color space (YUV to RGB) is converted on the GPU whilst rendering the frame.
Limitations (hopefully to be fixed):
- GStreamer is a bit annoying to set up on Windows.
The player **does not** come with any surrounding GUI controls, but they should be quite easy to implement should you need them.
See the "minimal" example for a demonstration on how you could implement pausing, looping, and seeking.
## Example Usage
```rust
use iced_video_player::{Video, VideoPlayer};
fn main() -> iced::Result {
iced::run("Video Player", (), App::view)
}
struct App {
video: Video,
}
impl Default for App {
fn default() -> Self {
App {
video: Video::new(&url::Url::parse("file:///C:/my_video.mp4").unwrap()).unwrap(),
}
}
}
impl App {
fn view(&self) -> iced::Element<()> {
VideoPlayer::new(&self.video).into()
}
}
```
## Building
Follow the [GStreamer build instructions](https://github.com/sdroege/gstreamer-rs#installation). This should be able to compile on MSVC, MinGW, Linux, and MacOS.
## License
Licensed under either
- [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
- [MIT](http://opensource.org/licenses/MIT)
at your option.

View File

@@ -0,0 +1,139 @@
use iced::{
widget::{Button, Column, Container, Row, Slider, Text},
Element,
};
use iced_video_player::{Video, VideoPlayer};
use std::time::Duration;
fn main() -> iced::Result {
iced::run(App::update, App::view)
}
#[derive(Clone, Debug)]
enum Message {
TogglePause,
ToggleLoop,
Seek(f64),
SeekRelease,
EndOfStream,
NewFrame,
}
struct App {
video: Video,
position: f64,
dragging: bool,
}
impl Default for App {
fn default() -> Self {
App {
video: Video::new(
&url::Url::parse("https://jellyfin.tsuba.darksailor.dev/Videos/1d7e2012-e17d-edbb-25c3-2dbcc803d6b6/stream?static=true")
.expect("Failed to parse URL"),
)
.expect("Failed to create video"),
position: 0.0,
dragging: false,
}
}
}
impl App {
fn update(&mut self, message: Message) {
match message {
Message::TogglePause => {
self.video.set_paused(!self.video.paused());
}
Message::ToggleLoop => {
self.video.set_looping(!self.video.looping());
}
Message::Seek(secs) => {
self.dragging = true;
self.video.set_paused(true);
self.position = secs;
}
Message::SeekRelease => {
self.dragging = false;
self.video
.seek(Duration::from_secs_f64(self.position), false)
.expect("seek");
self.video.set_paused(false);
}
Message::EndOfStream => {
println!("end of stream");
}
Message::NewFrame => {
if !self.dragging {
self.position = self.video.position().as_secs_f64();
}
}
}
}
fn view(&self) -> Element<Message> {
Column::new()
.push(
Container::new(
VideoPlayer::new(&self.video)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.content_fit(iced::ContentFit::Contain)
.on_end_of_stream(Message::EndOfStream)
.on_new_frame(Message::NewFrame),
)
.align_x(iced::Alignment::Center)
.align_y(iced::Alignment::Center)
.width(iced::Length::Fill)
.height(iced::Length::Fill),
)
.push(
Container::new(
Slider::new(
0.0..=self.video.duration().as_secs_f64(),
self.position,
Message::Seek,
)
.step(0.1)
.on_release(Message::SeekRelease),
)
.padding(iced::Padding::new(5.0).left(10.0).right(10.0)),
)
.push(
Row::new()
.spacing(5)
.align_y(iced::alignment::Vertical::Center)
.padding(iced::Padding::new(10.0).top(0.0))
.push(
Button::new(Text::new(if self.video.paused() {
"Play"
} else {
"Pause"
}))
.width(80.0)
.on_press(Message::TogglePause),
)
.push(
Button::new(Text::new(if self.video.looping() {
"Disable Loop"
} else {
"Enable Loop"
}))
.width(120.0)
.on_press(Message::ToggleLoop),
)
.push(
Text::new(format!(
"{}:{:02}s / {}:{:02}s",
self.position as u64 / 60,
self.position as u64 % 60,
self.video.duration().as_secs() / 60,
self.video.duration().as_secs() % 60,
))
.width(iced::Length::Fill)
.align_x(iced::alignment::Horizontal::Right),
),
)
.into()
}
}

275
crates/iced_video_player/flake.lock generated Normal file
View File

@@ -0,0 +1,275 @@
{
"nodes": {
"crane": {
"flake": false,
"locked": {
"lastModified": 1758758545,
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
"owner": "ipetkov",
"repo": "crane",
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
"type": "github"
},
"original": {
"owner": "ipetkov",
"ref": "v0.21.1",
"repo": "crane",
"type": "github"
}
},
"dream2nix": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"nixpkgs"
],
"purescript-overlay": "purescript-overlay",
"pyproject-nix": "pyproject-nix"
},
"locked": {
"lastModified": 1763413832,
"narHash": "sha256-dkqBwDXiv8MPoFyIvOuC4bVubAP+TlVZUkVMB78TTSg=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "5658fba3a0b6b7d5cb0460b949651f64f644a743",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "dream2nix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"mk-naked-shell": {
"flake": false,
"locked": {
"lastModified": 1681286841,
"narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=",
"owner": "90-008",
"repo": "mk-naked-shell",
"rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd",
"type": "github"
},
"original": {
"owner": "90-008",
"repo": "mk-naked-shell",
"type": "github"
}
},
"nixCargoIntegration": {
"inputs": {
"crane": "crane",
"dream2nix": "dream2nix",
"mk-naked-shell": "mk-naked-shell",
"nixpkgs": [
"nixpkgs"
],
"parts": "parts",
"rust-overlay": "rust-overlay",
"treefmt": "treefmt"
},
"locked": {
"lastModified": 1763619566,
"narHash": "sha256-92rSHIwh5qTXjcktVEWyKu5EPB3/7UdgjgjtWZ5ET6w=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "ac45d8c0d6876e6547d62bc729654c7b9a79c760",
"type": "github"
},
"original": {
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1763421233,
"narHash": "sha256-Stk9ZYRkGrnnpyJ4eqt9eQtdFWRRIvMxpNRf4sIegnw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "89c2b2330e733d6cdb5eae7b899326930c2c0648",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"parts": {
"inputs": {
"nixpkgs-lib": [
"nixCargoIntegration",
"nixpkgs"
]
},
"locked": {
"lastModified": 1762980239,
"narHash": "sha256-8oNVE8TrD19ulHinjaqONf9QWCKK+w4url56cdStMpM=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "52a2caecc898d0b46b2b905f058ccc5081f842da",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"purescript-overlay": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"nixCargoIntegration",
"dream2nix",
"nixpkgs"
],
"slimlock": "slimlock"
},
"locked": {
"lastModified": 1728546539,
"narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=",
"owner": "thomashoneyman",
"repo": "purescript-overlay",
"rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4",
"type": "github"
},
"original": {
"owner": "thomashoneyman",
"repo": "purescript-overlay",
"type": "github"
}
},
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"dream2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1752481895,
"narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "16ee295c25107a94e59a7fc7f2e5322851781162",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": {
"inputs": {
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"nixpkgs"
]
},
"locked": {
"lastModified": 1763606317,
"narHash": "sha256-lsq4Urmb9Iyg2zyg2yG6oMQk9yuaoIgy+jgvYM4guxA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a5615abaf30cfaef2e32f1ff9bd5ca94e2911371",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"slimlock": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"dream2nix",
"purescript-overlay",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688756706,
"narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=",
"owner": "thomashoneyman",
"repo": "slimlock",
"rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c",
"type": "github"
},
"original": {
"owner": "thomashoneyman",
"repo": "slimlock",
"type": "github"
}
},
"treefmt": {
"inputs": {
"nixpkgs": [
"nixCargoIntegration",
"nixpkgs"
]
},
"locked": {
"lastModified": 1762938485,
"narHash": "sha256-AlEObg0syDl+Spi4LsZIBrjw+snSVU4T8MOeuZJUJjM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,38 @@
{
inputs = {
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs: let
pkgs = import inputs.nixpkgs {
system = "x86_64-linux";
};
in {
devShells."x86_64-linux".default = pkgs.mkShell {
# "GST_PLUGIN_PATH" = "${pkgs.gst_all_1.gstreamer}:${pkgs.gst_all_1.gst-plugins-bad}:${pkgs.gst_all_1.gst-plugins-ugly}:${pkgs.gst_all_1.gst-plugins-good}:${pkgs.gst_all_1.gst-plugins-base}";
buildInputs = with pkgs; [
gst_all_1.gstreamer
gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-ugly
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-base
libxkbcommon
wayland
rustup
];
nativeBuildInputs = with pkgs; [
pkg-config
wayland
];
packages = with pkgs; [wayland];
};
};
}

View File

@@ -0,0 +1,12 @@
# Flake's devShell for non-flake-enabled nix instances
(import
(
let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url =
"https://github.com/edolstra/flake-compat/archive/${lock.nodes.flakeCompat.locked.rev}.tar.gz";
sha256 = lock.nodes.flakeCompat.locked.narHash;
}
)
{ src = ./.; }).shellNix.default

View File

@@ -0,0 +1,76 @@
//! # Iced Video Player
//!
//! A convenient video player widget for Iced.
//!
//! To get started, load a video from a URI (e.g., a file path prefixed with `file:///`) using [`Video::new`](crate::Video::new),
//! then use it like any other Iced widget in your `view` function by creating a [`VideoPlayer`].
//!
//! Example:
//! ```rust
//! use iced_video_player::{Video, VideoPlayer};
//!
//! fn main() -> iced::Result {
//! iced::run("Video Player", (), App::view)
//! }
//!
//! struct App {
//! video: Video,
//! }
//!
//! impl Default for App {
//! fn default() -> Self {
//! App {
//! video: Video::new(&url::Url::parse("file:///C:/my_video.mp4").unwrap()).unwrap(),
//! }
//! }
//! }
//!
//! impl App {
//! fn view(&self) -> iced::Element<()> {
//! VideoPlayer::new(&self.video).into()
//! }
//! }
//! ```
//!
//! You can programmatically control the video (e.g., seek, pause, loop, grab thumbnails) by accessing various methods on [`Video`].
mod pipeline;
mod video;
mod video_player;
use gstreamer as gst;
use thiserror::Error;
pub use video::Position;
pub use video::Video;
pub use video_player::VideoPlayer;
#[derive(Debug, Error)]
pub enum Error {
#[error("{0}")]
Glib(#[from] glib::Error),
#[error("{0}")]
Bool(#[from] glib::BoolError),
#[error("failed to get the gstreamer bus")]
Bus,
#[error("failed to get AppSink element with name='{0}' from gstreamer pipeline")]
AppSink(String),
#[error("{0}")]
StateChange(#[from] gst::StateChangeError),
#[error("failed to cast gstreamer element")]
Cast,
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("invalid URI")]
Uri,
#[error("failed to get media capabilities")]
Caps,
#[error("failed to query media duration or position")]
Duration,
#[error("failed to sync with playback")]
Sync,
#[error("failed to lock internal sync primitive")]
Lock,
#[error("invalid framerate: {0}")]
Framerate(f64),
}

View File

@@ -0,0 +1,469 @@
use crate::video::Frame;
use iced_wgpu::primitive::Primitive;
use iced_wgpu::wgpu;
use std::{
collections::{btree_map::Entry, BTreeMap},
num::NonZero,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex,
},
};
#[repr(C)]
struct Uniforms {
rect: [f32; 4],
// because wgpu min_uniform_buffer_offset_alignment
_pad: [u8; 240],
}
struct VideoEntry {
texture_y: wgpu::Texture,
texture_uv: wgpu::Texture,
instances: wgpu::Buffer,
bg0: wgpu::BindGroup,
alive: Arc<AtomicBool>,
prepare_index: AtomicUsize,
render_index: AtomicUsize,
}
pub(crate) struct VideoPipeline {
pipeline: wgpu::RenderPipeline,
bg0_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
videos: BTreeMap<u64, VideoEntry>,
}
impl VideoPipeline {
fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_video_player shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
});
let bg0_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_video_player bind group 0 layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: None,
},
count: None,
},
],
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_video_player pipeline layout"),
bind_group_layouts: &[&bg0_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_video_player pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("iced_video_player sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
lod_min_clamp: 0.0,
lod_max_clamp: 1.0,
compare: None,
anisotropy_clamp: 1,
border_color: None,
});
VideoPipeline {
pipeline,
bg0_layout,
sampler,
videos: BTreeMap::new(),
}
}
fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
video_id: u64,
alive: &Arc<AtomicBool>,
(width, height): (u32, u32),
frame: &[u8],
) {
if let Entry::Vacant(entry) = self.videos.entry(video_id) {
let texture_y = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_video_player texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let texture_uv = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_video_player texture"),
size: wgpu::Extent3d {
width: width / 2,
height: height / 2,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg8Unorm,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view_y = texture_y.create_view(&wgpu::TextureViewDescriptor {
label: Some("iced_video_player texture view"),
format: None,
dimension: None,
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
usage: Some(wgpu::TextureUsages::empty()),
});
let view_uv = texture_uv.create_view(&wgpu::TextureViewDescriptor {
label: Some("iced_video_player texture view"),
format: None,
dimension: None,
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
usage: Some(wgpu::TextureUsages::empty()),
});
let instances = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_video_player uniform buffer"),
size: 256 * std::mem::size_of::<Uniforms>() as u64, // max 256 video players per frame
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_video_player bind group"),
layout: &self.bg0_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view_y),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&view_uv),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &instances,
offset: 0,
size: Some(NonZero::new(std::mem::size_of::<Uniforms>() as _).unwrap()),
}),
},
],
});
entry.insert(VideoEntry {
texture_y,
texture_uv,
instances,
bg0: bind_group,
alive: Arc::clone(alive),
prepare_index: AtomicUsize::new(0),
render_index: AtomicUsize::new(0),
});
}
let VideoEntry {
texture_y,
texture_uv,
..
} = self.videos.get(&video_id).unwrap();
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: texture_y,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&frame[..(width * height) as usize],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: texture_uv,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&frame[(width * height) as usize..],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width),
rows_per_image: Some(height / 2),
},
wgpu::Extent3d {
width: width / 2,
height: height / 2,
depth_or_array_layers: 1,
},
);
}
fn cleanup(&mut self) {
let ids: Vec<_> = self
.videos
.iter()
.filter_map(|(id, entry)| (!entry.alive.load(Ordering::SeqCst)).then_some(*id))
.collect();
for id in ids {
if let Some(video) = self.videos.remove(&id) {
video.texture_y.destroy();
video.texture_uv.destroy();
video.instances.destroy();
}
}
}
fn prepare(&mut self, queue: &wgpu::Queue, video_id: u64, bounds: &iced::Rectangle) {
if let Some(video) = self.videos.get_mut(&video_id) {
let uniforms = Uniforms {
rect: [
bounds.x,
bounds.y,
bounds.x + bounds.width,
bounds.y + bounds.height,
],
_pad: [0; 240],
};
queue.write_buffer(
&video.instances,
(video.prepare_index.load(Ordering::Relaxed) * std::mem::size_of::<Uniforms>())
as u64,
unsafe {
std::slice::from_raw_parts(
&uniforms as *const _ as *const u8,
std::mem::size_of::<Uniforms>(),
)
},
);
video.prepare_index.fetch_add(1, Ordering::Relaxed);
video.render_index.store(0, Ordering::Relaxed);
}
self.cleanup();
}
fn draw(
&self,
target: &wgpu::TextureView,
encoder: &mut wgpu::CommandEncoder,
clip: &iced::Rectangle<u32>,
video_id: u64,
) {
if let Some(video) = self.videos.get(&video_id) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_video_player render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(
0,
&video.bg0,
&[
(video.render_index.load(Ordering::Relaxed) * std::mem::size_of::<Uniforms>())
as u32,
],
);
pass.set_scissor_rect(clip.x as _, clip.y as _, clip.width as _, clip.height as _);
pass.draw(0..6, 0..1);
video.prepare_index.store(0, Ordering::Relaxed);
video.render_index.fetch_add(1, Ordering::Relaxed);
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct VideoPrimitive {
video_id: u64,
alive: Arc<AtomicBool>,
frame: Arc<Mutex<Frame>>,
size: (u32, u32),
upload_frame: bool,
}
impl VideoPrimitive {
pub fn new(
video_id: u64,
alive: Arc<AtomicBool>,
frame: Arc<Mutex<Frame>>,
size: (u32, u32),
upload_frame: bool,
) -> Self {
VideoPrimitive {
video_id,
alive,
frame,
size,
upload_frame,
}
}
}
impl Primitive for VideoPrimitive {
type Renderer = VideoPipeline;
fn initialize(
&self,
device: &wgpu::Device,
_queue: &wgpu::Queue,
format: wgpu::TextureFormat,
) -> Self::Renderer {
VideoPipeline::new(device, format)
}
fn prepare(
&self,
renderer: &mut Self::Renderer,
device: &wgpu::Device,
queue: &wgpu::Queue,
bounds: &iced::Rectangle,
viewport: &iced_wgpu::graphics::Viewport,
) {
if self.upload_frame {
if let Some(readable) = self.frame.lock().expect("lock frame mutex").readable() {
renderer.upload(
device,
queue,
self.video_id,
&self.alive,
self.size,
readable.as_slice(),
);
}
}
renderer.prepare(
queue,
self.video_id,
&(*bounds
* iced::Transformation::orthographic(
viewport.logical_size().width as _,
viewport.logical_size().height as _,
)),
);
}
fn render(
&self,
renderer: &Self::Renderer,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clip_bounds: &iced::Rectangle<u32>,
) {
renderer.draw(target, encoder, clip_bounds, self.video_id);
}
}

View File

@@ -0,0 +1,61 @@
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
struct Uniforms {
rect: vec4<f32>,
}
@group(0) @binding(0)
var tex_y: texture_2d<f32>;
@group(0) @binding(1)
var tex_uv: texture_2d<f32>;
@group(0) @binding(2)
var s: sampler;
@group(0) @binding(3)
var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput {
var quad = array<vec4<f32>, 6>(
vec4<f32>(uniforms.rect.xy, 0.0, 0.0),
vec4<f32>(uniforms.rect.zy, 1.0, 0.0),
vec4<f32>(uniforms.rect.xw, 0.0, 1.0),
vec4<f32>(uniforms.rect.zy, 1.0, 0.0),
vec4<f32>(uniforms.rect.zw, 1.0, 1.0),
vec4<f32>(uniforms.rect.xw, 0.0, 1.0),
);
var out: VertexOutput;
out.uv = quad[in_vertex_index].zw;
out.position = vec4<f32>(quad[in_vertex_index].xy, 1.0, 1.0);
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let yuv2r = vec3<f32>(1.164, 0.0, 1.596);
let yuv2g = vec3<f32>(1.164, -0.391, -0.813);
let yuv2b = vec3<f32>(1.164, 2.018, 0.0);
var yuv = vec3<f32>(0.0);
yuv.x = textureSample(tex_y, s, in.uv).r - 0.0625;
yuv.y = textureSample(tex_uv, s, in.uv).r - 0.5;
yuv.z = textureSample(tex_uv, s, in.uv).g - 0.5;
var rgb = vec3<f32>(0.0);
rgb.x = dot(yuv, yuv2r);
rgb.y = dot(yuv, yuv2g);
rgb.z = dot(yuv, yuv2b);
let threshold = rgb <= vec3<f32>(0.04045);
let hi = pow((rgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
let lo = rgb * vec3<f32>(1.0 / 12.92);
rgb = select(hi, lo, threshold);
return vec4<f32>(rgb, 1.0);
}

View File

@@ -0,0 +1,662 @@
use crate::Error;
use gstreamer as gst;
use gstreamer_app as gst_app;
use gstreamer_app::prelude::*;
use iced::widget::image as img;
use std::num::NonZeroU8;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant};
/// Position in the media.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Position {
/// Position based on time.
///
/// Not the most accurate format for videos.
Time(Duration),
/// Position based on nth frame.
Frame(u64),
}
impl From<Position> for gst::GenericFormattedValue {
fn from(pos: Position) -> Self {
match pos {
Position::Time(t) => gst::ClockTime::from_nseconds(t.as_nanos() as _).into(),
Position::Frame(f) => gst::format::Default::from_u64(f).into(),
}
}
}
impl From<Duration> for Position {
fn from(t: Duration) -> Self {
Position::Time(t)
}
}
impl From<u64> for Position {
fn from(f: u64) -> Self {
Position::Frame(f)
}
}
#[derive(Debug)]
pub(crate) struct Frame(gst::Sample);
impl Frame {
pub fn empty() -> Self {
Self(gst::Sample::builder().build())
}
pub fn readable(&self) -> Option<gst::BufferMap<'_, gst::buffer::Readable>> {
self.0.buffer().and_then(|x| x.map_readable().ok())
}
}
#[derive(Debug)]
pub(crate) struct Internal {
pub(crate) id: u64,
pub(crate) bus: gst::Bus,
pub(crate) source: gst::Pipeline,
pub(crate) alive: Arc<AtomicBool>,
pub(crate) worker: Option<std::thread::JoinHandle<()>>,
pub(crate) width: i32,
pub(crate) height: i32,
pub(crate) framerate: f64,
pub(crate) duration: Duration,
pub(crate) speed: f64,
pub(crate) sync_av: bool,
pub(crate) frame: Arc<Mutex<Frame>>,
pub(crate) upload_frame: Arc<AtomicBool>,
pub(crate) last_frame_time: Arc<Mutex<Instant>>,
pub(crate) looping: bool,
pub(crate) is_eos: bool,
pub(crate) restart_stream: bool,
pub(crate) sync_av_avg: u64,
pub(crate) sync_av_counter: u64,
pub(crate) subtitle_text: Arc<Mutex<Option<String>>>,
pub(crate) upload_text: Arc<AtomicBool>,
}
impl Internal {
pub(crate) fn seek(&self, position: impl Into<Position>, accurate: bool) -> Result<(), Error> {
let position = position.into();
// gstreamer complains if the start & end value types aren't the same
match &position {
Position::Time(_) => self.source.seek(
self.speed,
gst::SeekFlags::FLUSH
| if accurate {
gst::SeekFlags::ACCURATE
} else {
gst::SeekFlags::empty()
},
gst::SeekType::Set,
gst::GenericFormattedValue::from(position),
gst::SeekType::Set,
gst::ClockTime::NONE,
)?,
Position::Frame(_) => self.source.seek(
self.speed,
gst::SeekFlags::FLUSH
| if accurate {
gst::SeekFlags::ACCURATE
} else {
gst::SeekFlags::empty()
},
gst::SeekType::Set,
gst::GenericFormattedValue::from(position),
gst::SeekType::Set,
gst::format::Default::NONE,
)?,
};
*self.subtitle_text.lock().expect("lock subtitle_text") = None;
self.upload_text.store(true, Ordering::SeqCst);
Ok(())
}
pub(crate) fn set_speed(&mut self, speed: f64) -> Result<(), Error> {
let Some(position) = self.source.query_position::<gst::ClockTime>() else {
return Err(Error::Caps);
};
if speed > 0.0 {
self.source.seek(
speed,
gst::SeekFlags::FLUSH | gst::SeekFlags::ACCURATE,
gst::SeekType::Set,
position,
gst::SeekType::End,
gst::ClockTime::from_seconds(0),
)?;
} else {
self.source.seek(
speed,
gst::SeekFlags::FLUSH | gst::SeekFlags::ACCURATE,
gst::SeekType::Set,
gst::ClockTime::from_seconds(0),
gst::SeekType::Set,
position,
)?;
}
self.speed = speed;
Ok(())
}
pub(crate) fn restart_stream(&mut self) -> Result<(), Error> {
self.is_eos = false;
self.set_paused(false);
self.seek(0, false)?;
Ok(())
}
pub(crate) fn set_paused(&mut self, paused: bool) {
self.source
.set_state(if paused {
gst::State::Paused
} else {
gst::State::Playing
})
.unwrap(/* state was changed in ctor; state errors caught there */);
// Set restart_stream flag to make the stream restart on the next Message::NextFrame
if self.is_eos && !paused {
self.restart_stream = true;
}
}
pub(crate) fn paused(&self) -> bool {
self.source.state(gst::ClockTime::ZERO).1 == gst::State::Paused
}
/// Syncs audio with video when there is (inevitably) latency presenting the frame.
pub(crate) fn set_av_offset(&mut self, offset: Duration) {
if self.sync_av {
self.sync_av_counter += 1;
self.sync_av_avg = self.sync_av_avg * (self.sync_av_counter - 1) / self.sync_av_counter
+ offset.as_nanos() as u64 / self.sync_av_counter;
if self.sync_av_counter % 128 == 0 {
self.source
.set_property("av-offset", -(self.sync_av_avg as i64));
}
}
}
}
/// A multimedia video loaded from a URI (e.g., a local file path or HTTP stream).
#[derive(Debug)]
pub struct Video(pub(crate) RwLock<Internal>);
impl Drop for Video {
fn drop(&mut self) {
let inner = self.0.get_mut().expect("failed to lock");
inner
.source
.set_state(gst::State::Null)
.expect("failed to set state");
inner.alive.store(false, Ordering::SeqCst);
if let Some(worker) = inner.worker.take() {
if let Err(err) = worker.join() {
match err.downcast_ref::<String>() {
Some(e) => log::error!("Video thread panicked: {e}"),
None => log::error!("Video thread panicked with unknown reason"),
}
}
}
}
}
impl Video {
/// Create a new video player from a given video which loads from `uri`.
/// Note that live sources will report the duration to be zero.
pub fn new(uri: &url::Url) -> Result<Self, Error> {
gst::init()?;
let pipeline = format!("playbin uri=\"{}\" text-sink=\"appsink name=iced_text sync=true drop=true\" video-sink=\"videoscale ! videoconvert ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1\"", uri.as_str());
let pipeline = gst::parse::launch(pipeline.as_ref())?
.downcast::<gst::Pipeline>()
.map_err(|_| Error::Cast)?;
let video_sink: gst::Element = pipeline.property("video-sink");
let pad = video_sink.pads().first().cloned().unwrap();
let pad = pad.dynamic_cast::<gst::GhostPad>().unwrap();
let bin = pad
.parent_element()
.unwrap()
.downcast::<gst::Bin>()
.unwrap();
let video_sink = bin.by_name("iced_video").unwrap();
let video_sink = video_sink.downcast::<gst_app::AppSink>().unwrap();
let text_sink: gst::Element = pipeline.property("text-sink");
let text_sink = text_sink.downcast::<gst_app::AppSink>().unwrap();
Self::from_gst_pipeline(pipeline, video_sink, Some(text_sink))
}
/// Creates a new video based on an existing GStreamer pipeline and appsink.
/// Expects an `appsink` plugin with `caps=video/x-raw,format=NV12`.
///
/// An optional `text_sink` can be provided, which enables subtitle messages
/// to be emitted.
///
/// **Note:** Many functions of [`Video`] assume a `playbin` pipeline.
/// Non-`playbin` pipelines given here may not have full functionality.
pub fn from_gst_pipeline(
pipeline: gst::Pipeline,
video_sink: gst_app::AppSink,
text_sink: Option<gst_app::AppSink>,
) -> Result<Self, Error> {
gst::init()?;
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
// We need to ensure we stop the pipeline if we hit an error,
// or else there may be audio left playing in the background.
macro_rules! cleanup {
($expr:expr) => {
$expr.map_err(|e| {
let _ = pipeline.set_state(gst::State::Null);
e
})
};
}
let pad = video_sink.pads().first().cloned().unwrap();
dbg!(&pad);
dbg!(&pipeline);
cleanup!(pipeline.set_state(gst::State::Playing))?;
// wait for up to 5 seconds until the decoder gets the source capabilities
cleanup!(pipeline.state(gst::ClockTime::from_seconds(5)).0)?;
// extract resolution and framerate
// TODO(jazzfool): maybe we want to extract some other information too?
let caps = cleanup!(pad.current_caps().ok_or(Error::Caps))?;
let s = cleanup!(caps.structure(0).ok_or(Error::Caps))?;
let width = cleanup!(s.get::<i32>("width").map_err(|_| Error::Caps))?;
let height = cleanup!(s.get::<i32>("height").map_err(|_| Error::Caps))?;
// resolution should be mod4
let width = ((width + 4 - 1) / 4) * 4;
let framerate = cleanup!(s.get::<gst::Fraction>("framerate").map_err(|_| Error::Caps))?;
let framerate = framerate.numer() as f64 / framerate.denom() as f64;
if framerate.is_nan()
|| framerate.is_infinite()
|| framerate < 0.0
|| framerate.abs() < f64::EPSILON
{
let _ = pipeline.set_state(gst::State::Null);
return Err(Error::Framerate(framerate));
}
let duration = Duration::from_nanos(
pipeline
.query_duration::<gst::ClockTime>()
.map(|duration| duration.nseconds())
.unwrap_or(0),
);
let sync_av = pipeline.has_property("av-offset", None);
// NV12 = 12bpp
let frame = Arc::new(Mutex::new(Frame::empty()));
let upload_frame = Arc::new(AtomicBool::new(false));
let alive = Arc::new(AtomicBool::new(true));
let last_frame_time = Arc::new(Mutex::new(Instant::now()));
let frame_ref = Arc::clone(&frame);
let upload_frame_ref = Arc::clone(&upload_frame);
let alive_ref = Arc::clone(&alive);
let last_frame_time_ref = Arc::clone(&last_frame_time);
let subtitle_text = Arc::new(Mutex::new(None));
let upload_text = Arc::new(AtomicBool::new(false));
let subtitle_text_ref = Arc::clone(&subtitle_text);
let upload_text_ref = Arc::clone(&upload_text);
let pipeline_ref = pipeline.clone();
let worker = std::thread::spawn(move || {
let mut clear_subtitles_at = None;
while alive_ref.load(Ordering::Acquire) {
if let Err(gst::FlowError::Error) = (|| -> Result<(), gst::FlowError> {
let sample =
if pipeline_ref.state(gst::ClockTime::ZERO).1 != gst::State::Playing {
video_sink
.try_pull_preroll(gst::ClockTime::from_mseconds(16))
.ok_or(gst::FlowError::Eos)?
} else {
video_sink
.try_pull_sample(gst::ClockTime::from_mseconds(16))
.ok_or(gst::FlowError::Eos)?
};
*last_frame_time_ref
.lock()
.map_err(|_| gst::FlowError::Error)? = Instant::now();
let frame_segment = sample.segment().cloned().ok_or(gst::FlowError::Error)?;
let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
let frame_pts = buffer.pts().ok_or(gst::FlowError::Error)?;
let frame_duration = buffer.duration().ok_or(gst::FlowError::Error)?;
{
let mut frame_guard =
frame_ref.lock().map_err(|_| gst::FlowError::Error)?;
*frame_guard = Frame(sample);
}
upload_frame_ref.swap(true, Ordering::SeqCst);
if let Some(at) = clear_subtitles_at {
if frame_pts >= at {
*subtitle_text_ref
.lock()
.map_err(|_| gst::FlowError::Error)? = None;
upload_text_ref.store(true, Ordering::SeqCst);
clear_subtitles_at = None;
}
}
let text = text_sink
.as_ref()
.and_then(|sink| sink.try_pull_sample(gst::ClockTime::from_seconds(0)));
if let Some(text) = text {
let text_segment = text.segment().ok_or(gst::FlowError::Error)?;
let text = text.buffer().ok_or(gst::FlowError::Error)?;
let text_pts = text.pts().ok_or(gst::FlowError::Error)?;
let text_duration = text.duration().ok_or(gst::FlowError::Error)?;
let frame_running_time = frame_segment.to_running_time(frame_pts).value();
let frame_running_time_end = frame_segment
.to_running_time(frame_pts + frame_duration)
.value();
let text_running_time = text_segment.to_running_time(text_pts).value();
let text_running_time_end = text_segment
.to_running_time(text_pts + text_duration)
.value();
// see gst-plugins-base/ext/pango/gstbasetextoverlay.c (gst_base_text_overlay_video_chain)
// as an example of how to correctly synchronize the text+video segments
if text_running_time_end > frame_running_time
&& frame_running_time_end > text_running_time
{
let duration = text.duration().unwrap_or(gst::ClockTime::ZERO);
let map = text.map_readable().map_err(|_| gst::FlowError::Error)?;
let text = std::str::from_utf8(map.as_slice())
.map_err(|_| gst::FlowError::Error)?
.to_string();
*subtitle_text_ref
.lock()
.map_err(|_| gst::FlowError::Error)? = Some(text);
upload_text_ref.store(true, Ordering::SeqCst);
clear_subtitles_at = Some(text_pts + duration);
}
}
Ok(())
})() {
log::error!("error pulling frame");
}
}
});
Ok(Video(RwLock::new(Internal {
id,
bus: pipeline.bus().unwrap(),
source: pipeline,
alive,
worker: Some(worker),
width,
height,
framerate,
duration,
speed: 1.0,
sync_av,
frame,
upload_frame,
last_frame_time,
looping: false,
is_eos: false,
restart_stream: false,
sync_av_avg: 0,
sync_av_counter: 0,
subtitle_text,
upload_text,
})))
}
pub(crate) fn read(&self) -> impl Deref<Target = Internal> + '_ {
self.0.read().expect("lock")
}
pub(crate) fn write(&self) -> impl DerefMut<Target = Internal> + '_ {
self.0.write().expect("lock")
}
pub(crate) fn get_mut(&mut self) -> impl DerefMut<Target = Internal> + '_ {
self.0.get_mut().expect("lock")
}
/// Get the size/resolution of the video as `(width, height)`.
pub fn size(&self) -> (i32, i32) {
(self.read().width, self.read().height)
}
/// Get the framerate of the video as frames per second.
pub fn framerate(&self) -> f64 {
self.read().framerate
}
/// Set the volume multiplier of the audio.
/// `0.0` = 0% volume, `1.0` = 100% volume.
///
/// This uses a linear scale, for example `0.5` is perceived as half as loud.
pub fn set_volume(&mut self, volume: f64) {
self.get_mut().source.set_property("volume", volume);
self.set_muted(self.muted()); // for some reason gstreamer unmutes when changing volume?
}
/// Get the volume multiplier of the audio.
pub fn volume(&self) -> f64 {
self.read().source.property("volume")
}
/// Set if the audio is muted or not, without changing the volume.
pub fn set_muted(&mut self, muted: bool) {
self.get_mut().source.set_property("mute", muted);
}
/// Get if the audio is muted or not.
pub fn muted(&self) -> bool {
self.read().source.property("mute")
}
/// Get if the stream ended or not.
pub fn eos(&self) -> bool {
self.read().is_eos
}
/// Get if the media will loop or not.
pub fn looping(&self) -> bool {
self.read().looping
}
/// Set if the media will loop or not.
pub fn set_looping(&mut self, looping: bool) {
self.get_mut().looping = looping;
}
/// Set if the media is paused or not.
pub fn set_paused(&mut self, paused: bool) {
self.get_mut().set_paused(paused)
}
/// Get if the media is paused or not.
pub fn paused(&self) -> bool {
self.read().paused()
}
/// Jumps to a specific position in the media.
/// Passing `true` to the `accurate` parameter will result in more accurate seeking,
/// however, it is also slower. For most seeks (e.g., scrubbing) this is not needed.
pub fn seek(&mut self, position: impl Into<Position>, accurate: bool) -> Result<(), Error> {
self.get_mut().seek(position, accurate)
}
/// Set the playback speed of the media.
/// The default speed is `1.0`.
pub fn set_speed(&mut self, speed: f64) -> Result<(), Error> {
self.get_mut().set_speed(speed)
}
/// Get the current playback speed.
pub fn speed(&self) -> f64 {
self.read().speed
}
/// Get the current playback position in time.
pub fn position(&self) -> Duration {
Duration::from_nanos(
self.read()
.source
.query_position::<gst::ClockTime>()
.map_or(0, |pos| pos.nseconds()),
)
}
/// Get the media duration.
pub fn duration(&self) -> Duration {
self.read().duration
}
/// Restarts a stream; seeks to the first frame and unpauses, sets the `eos` flag to false.
pub fn restart_stream(&mut self) -> Result<(), Error> {
self.get_mut().restart_stream()
}
/// Set the subtitle URL to display.
pub fn set_subtitle_url(&mut self, url: &url::Url) -> Result<(), Error> {
let paused = self.paused();
let mut inner = self.get_mut();
inner.source.set_state(gst::State::Ready)?;
inner.source.set_property("suburi", url.as_str());
inner.set_paused(paused);
Ok(())
}
/// Get the current subtitle URL.
pub fn subtitle_url(&self) -> Option<url::Url> {
url::Url::parse(
&self
.read()
.source
.property::<Option<String>>("current-suburi")?,
)
.ok()
}
/// Get the underlying GStreamer pipeline.
pub fn pipeline(&self) -> gst::Pipeline {
self.read().source.clone()
}
/// Generates a list of thumbnails based on a set of positions in the media, downscaled by a given factor.
///
/// Slow; only needs to be called once for each instance.
/// It's best to call this at the very start of playback, otherwise the position may shift.
pub fn thumbnails<I>(
&mut self,
positions: I,
downscale: NonZeroU8,
) -> Result<Vec<img::Handle>, Error>
where
I: IntoIterator<Item = Position>,
{
let downscale = u8::from(downscale) as u32;
let paused = self.paused();
let muted = self.muted();
let pos = self.position();
self.set_paused(false);
self.set_muted(true);
let out = {
let inner = self.read();
let width = inner.width;
let height = inner.height;
positions
.into_iter()
.map(|pos| {
inner.seek(pos, true)?;
inner.upload_frame.store(false, Ordering::SeqCst);
while !inner.upload_frame.load(Ordering::SeqCst) {
std::hint::spin_loop();
}
let frame_guard = inner.frame.lock().map_err(|_| Error::Lock)?;
let frame = frame_guard.readable().ok_or(Error::Lock)?;
Ok(img::Handle::from_rgba(
inner.width as u32 / downscale,
inner.height as u32 / downscale,
yuv_to_rgba(frame.as_slice(), width as _, height as _, downscale),
))
})
.collect()
};
self.set_paused(paused);
self.set_muted(muted);
self.seek(pos, true)?;
out
}
}
fn yuv_to_rgba(yuv: &[u8], width: u32, height: u32, downscale: u32) -> Vec<u8> {
let uv_start = width * height;
let mut rgba = vec![];
for y in 0..height / downscale {
for x in 0..width / downscale {
let x_src = x * downscale;
let y_src = y * downscale;
let uv_i = uv_start + width * (y_src / 2) + x_src / 2 * 2;
let y = yuv[(y_src * width + x_src) as usize] as f32;
let u = yuv[uv_i as usize] as f32;
let v = yuv[(uv_i + 1) as usize] as f32;
let r = 1.164 * (y - 16.0) + 1.596 * (v - 128.0);
let g = 1.164 * (y - 16.0) - 0.813 * (v - 128.0) - 0.391 * (u - 128.0);
let b = 1.164 * (y - 16.0) + 2.018 * (u - 128.0);
rgba.push(r as u8);
rgba.push(g as u8);
rgba.push(b as u8);
rgba.push(0xFF);
}
}
rgba
}

View File

@@ -0,0 +1,305 @@
use crate::{pipeline::VideoPrimitive, video::Video};
use gstreamer as gst;
use iced::{
advanced::{self, layout, widget, Widget},
Element,
};
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
use log::error;
use std::{marker::PhantomData, sync::atomic::Ordering};
use std::{sync::Arc, time::Instant};
/// Video player widget which displays the current frame of a [`Video`](crate::Video).
pub struct VideoPlayer<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer>
where
Renderer: PrimitiveRenderer,
{
video: &'a Video,
content_fit: iced::ContentFit,
width: iced::Length,
height: iced::Length,
on_end_of_stream: Option<Message>,
on_new_frame: Option<Message>,
on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message + 'a>>,
on_error: Option<Box<dyn Fn(&glib::Error) -> Message + 'a>>,
_phantom: PhantomData<(Theme, Renderer)>,
}
impl<'a, Message, Theme, Renderer> VideoPlayer<'a, Message, Theme, Renderer>
where
Renderer: PrimitiveRenderer,
{
/// Creates a new video player widget for a given video.
pub fn new(video: &'a Video) -> Self {
VideoPlayer {
video,
content_fit: iced::ContentFit::default(),
width: iced::Length::Shrink,
height: iced::Length::Shrink,
on_end_of_stream: None,
on_new_frame: None,
on_subtitle_text: None,
on_error: None,
_phantom: Default::default(),
}
}
/// Sets the width of the `VideoPlayer` boundaries.
pub fn width(self, width: impl Into<iced::Length>) -> Self {
VideoPlayer {
width: width.into(),
..self
}
}
/// Sets the height of the `VideoPlayer` boundaries.
pub fn height(self, height: impl Into<iced::Length>) -> Self {
VideoPlayer {
height: height.into(),
..self
}
}
/// Sets the `ContentFit` of the `VideoPlayer`.
pub fn content_fit(self, content_fit: iced::ContentFit) -> Self {
VideoPlayer {
content_fit,
..self
}
}
/// Message to send when the video reaches the end of stream (i.e., the video ends).
pub fn on_end_of_stream(self, on_end_of_stream: Message) -> Self {
VideoPlayer {
on_end_of_stream: Some(on_end_of_stream),
..self
}
}
/// Message to send when the video receives a new frame.
pub fn on_new_frame(self, on_new_frame: Message) -> Self {
VideoPlayer {
on_new_frame: Some(on_new_frame),
..self
}
}
/// Message to send when the video receives a new frame.
pub fn on_subtitle_text<F>(self, on_subtitle_text: F) -> Self
where
F: 'a + Fn(Option<String>) -> Message,
{
VideoPlayer {
on_subtitle_text: Some(Box::new(on_subtitle_text)),
..self
}
}
/// Message to send when the video playback encounters an error.
pub fn on_error<F>(self, on_error: F) -> Self
where
F: 'a + Fn(&glib::Error) -> Message,
{
VideoPlayer {
on_error: Some(Box::new(on_error)),
..self
}
}
}
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for VideoPlayer<'_, Message, Theme, Renderer>
where
Message: Clone,
Renderer: PrimitiveRenderer,
{
fn size(&self) -> iced::Size<iced::Length> {
iced::Size {
width: iced::Length::Shrink,
height: iced::Length::Shrink,
}
}
fn layout(
&mut self,
_tree: &mut widget::Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let (video_width, video_height) = self.video.size();
// based on `Image::layout`
let image_size = iced::Size::new(video_width as f32, video_height as f32);
let raw_size = limits.resolve(self.width, self.height, image_size);
let full_size = self.content_fit.fit(image_size, raw_size);
let final_size = iced::Size {
width: match self.width {
iced::Length::Shrink => f32::min(raw_size.width, full_size.width),
_ => raw_size.width,
},
height: match self.height {
iced::Length::Shrink => f32::min(raw_size.height, full_size.height),
_ => raw_size.height,
},
};
layout::Node::new(final_size)
}
fn draw(
&self,
_tree: &widget::Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &advanced::renderer::Style,
layout: advanced::Layout<'_>,
_cursor: advanced::mouse::Cursor,
_viewport: &iced::Rectangle,
) {
let mut inner = self.video.write();
// bounds based on `Image::draw`
let image_size = iced::Size::new(inner.width as f32, inner.height as f32);
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
let scale = iced::Vector::new(
adjusted_fit.width / image_size.width,
adjusted_fit.height / image_size.height,
);
let final_size = image_size * scale;
let position = match self.content_fit {
iced::ContentFit::None => iced::Point::new(
bounds.x + (image_size.width - adjusted_fit.width) / 2.0,
bounds.y + (image_size.height - adjusted_fit.height) / 2.0,
),
_ => iced::Point::new(
bounds.center_x() - final_size.width / 2.0,
bounds.center_y() - final_size.height / 2.0,
),
};
let drawing_bounds = iced::Rectangle::new(position, final_size);
let upload_frame = inner.upload_frame.swap(false, Ordering::SeqCst);
if upload_frame {
let last_frame_time = inner
.last_frame_time
.lock()
.map(|time| *time)
.unwrap_or_else(|_| Instant::now());
inner.set_av_offset(Instant::now() - last_frame_time);
}
let render = |renderer: &mut Renderer| {
renderer.draw_primitive(
drawing_bounds,
VideoPrimitive::new(
inner.id,
Arc::clone(&inner.alive),
Arc::clone(&inner.frame),
(inner.width as _, inner.height as _),
upload_frame,
),
);
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height {
renderer.with_layer(bounds, render);
} else {
render(renderer);
}
}
fn update(
&mut self,
_state: &mut widget::Tree,
event: &iced::Event,
_layout: advanced::Layout<'_>,
_cursor: advanced::mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn advanced::Clipboard,
shell: &mut advanced::Shell<'_, Message>,
_viewport: &iced::Rectangle,
) {
let mut inner = self.video.write();
if let iced::Event::Window(iced::window::Event::RedrawRequested(_)) = event {
if inner.restart_stream || (!inner.is_eos && !inner.paused()) {
let mut restart_stream = false;
if inner.restart_stream {
restart_stream = true;
// Set flag to false to avoid potentially multiple seeks
inner.restart_stream = false;
}
let mut eos_pause = false;
while let Some(msg) = inner
.bus
.pop_filtered(&[gst::MessageType::Error, gst::MessageType::Eos])
{
match msg.view() {
gst::MessageView::Error(err) => {
error!("bus returned an error: {err}");
if let Some(ref on_error) = self.on_error {
shell.publish(on_error(&err.error()))
};
}
gst::MessageView::Eos(_eos) => {
if let Some(on_end_of_stream) = self.on_end_of_stream.clone() {
shell.publish(on_end_of_stream);
}
if inner.looping {
restart_stream = true;
} else {
eos_pause = true;
}
}
_ => {}
}
}
// Don't run eos_pause if restart_stream is true; fixes "pausing" after restarting a stream
if restart_stream {
if let Err(err) = inner.restart_stream() {
error!("cannot restart stream (can't seek): {err:#?}");
}
} else if eos_pause {
inner.is_eos = true;
inner.set_paused(true);
}
if inner.upload_frame.load(Ordering::SeqCst) {
if let Some(on_new_frame) = self.on_new_frame.clone() {
shell.publish(on_new_frame);
}
}
if let Some(on_subtitle_text) = &self.on_subtitle_text {
if inner.upload_text.swap(false, Ordering::SeqCst) {
if let Ok(text) = inner.subtitle_text.try_lock() {
shell.publish(on_subtitle_text(text.clone()));
}
}
}
shell.request_redraw();
} else {
shell.request_redraw();
}
}
}
}
impl<'a, Message, Theme, Renderer> From<VideoPlayer<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Theme: 'a,
Renderer: 'a + PrimitiveRenderer,
{
fn from(video_player: VideoPlayer<'a, Message, Theme, Renderer>) -> Self {
Self::new(video_player)
}
}

View File

@@ -74,18 +74,36 @@
buildInputs = with pkgs; buildInputs = with pkgs;
[ [
vulkan-loader gst_all_1.gst-editing-services
gst_all_1.gst-libav
gst_all_1.gst-plugins-bad
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-rs
gst_all_1.gst-plugins-ugly
gst_all_1.gst-rtsp-server
gst_all_1.gstreamer
openssl openssl
vulkan-loader
glib
] ]
++ (lib.optionals pkgs.stdenv.isLinux [ ++ (lib.optionals pkgs.stdenv.isLinux [
gst_all_1.gstreamermm
gst_all_1.gst-vaapi
alsa-lib-with-plugins alsa-lib-with-plugins
libxkbcommon libxkbcommon
udev udev
wayland wayland
wayland-protocols
xorg.libX11
xorg.libXi
xorg.libXrandr
]) ])
++ (lib.optionals pkgs.stdenv.isDarwin [ ++ (lib.optionals pkgs.stdenv.isDarwin [
libiconv libiconv
apple-sdk_13 apple-sdk_26
]); ]);
}; };
cargoArtifacts = craneLib.buildPackage commonArgs; cargoArtifacts = craneLib.buildPackage commonArgs;
@@ -144,11 +162,10 @@
devShells = { devShells = {
default = default =
pkgs.mkShell.override { pkgs.mkShell.override {
stdenv = pkgs.clangStdenv; stdenv =
# stdenv = if pkgs.stdenv.isLinux
# if pkgs.stdenv.isLinux then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv)
# then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv) else pkgs.clangStdenv;
# else pkgs.clangStdenv;
} (commonArgs } (commonArgs
// { // {
packages = with pkgs; packages = with pkgs;
@@ -162,9 +179,10 @@
cargo-hack cargo-hack
cargo-outdated cargo-outdated
lld lld
lldb
] ]
++ (lib.optionals pkgs.stdenv.isDarwin [ ++ (lib.optionals pkgs.stdenv.isDarwin [
apple-sdk_13 apple-sdk_26
]) ])
++ (lib.optionals pkgs.stdenv.isLinux [ ++ (lib.optionals pkgs.stdenv.isLinux [
mold mold

View File

@@ -1,5 +1,5 @@
mod errors; mod errors;
use api::{JellyfinClient, JellyfinConfig}; use api::JellyfinConfig;
use errors::*; use errors::*;
fn jellyfin_config_try() -> Result<JellyfinConfig> { fn jellyfin_config_try() -> Result<JellyfinConfig> {

View File

@@ -184,7 +184,6 @@ fn main() {
quote::quote! { quote::quote! {
#[doc = #desc] #[doc = #desc]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum #key { pub enum #key {
#(#variants),* #(#variants),*
} }
@@ -192,7 +191,6 @@ fn main() {
} else { } else {
quote::quote! { quote::quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum #key { pub enum #key {
#(#variants),* #(#variants),*
} }

View File

@@ -6,12 +6,12 @@ edition = "2024"
[dependencies] [dependencies]
api = { version = "0.1.0", path = "../api" } api = { version = "0.1.0", path = "../api" }
blurhash = "0.2.3" blurhash = "0.2.3"
bytes = "1.11.0"
gpui_util = "0.2.2" gpui_util = "0.2.2"
iced = { git = "https://github.com/iced-rs/iced", features = [ iced = { workspace = true }
"advanced", iced_video_player = { workspace = true }
"canvas", reqwest = "0.12.24"
"image", tap = "1.0.1"
"tokio",
] }
tracing = "0.1.41" tracing = "0.1.41"
url = "2.5.7"
uuid = "1.18.1" uuid = "1.18.1"

View File

@@ -1,6 +1,5 @@
use std::sync::{Arc, LazyLock, atomic::AtomicBool}; use iced::{Element, Length, advanced::Widget};
use std::sync::Arc;
use iced::{Element, advanced::Widget, widget::Image};
use crate::shared_string::SharedString; use crate::shared_string::SharedString;
@@ -8,16 +7,27 @@ use crate::shared_string::SharedString;
pub struct BlurHash { pub struct BlurHash {
hash: SharedString, hash: SharedString,
handle: Arc<iced::advanced::image::Handle>, handle: Arc<iced::advanced::image::Handle>,
width: u32, width: iced::Length,
height: u32, height: iced::Length,
punch: f32, punch: f32,
} }
impl core::fmt::Debug for BlurHash {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BlurHash")
.field("hash", &self.hash)
.field("width", &self.width)
.field("height", &self.height)
.field("punch", &self.punch)
.finish()
}
}
impl BlurHash { impl BlurHash {
pub fn recompute(&mut self) { pub fn recompute(&mut self, width: u32, height: u32, punch: f32) {
let pixels = blurhash::decode(&self.hash, self.width, self.height, self.punch) let pixels = blurhash::decode(&self.hash, width, height, punch)
.unwrap_or_else(|_| vec![0; (self.width * self.height * 4) as usize]); .unwrap_or_else(|_| vec![0; (width * height * 4) as usize]);
let handle = iced::advanced::image::Handle::from_rgba(self.width, self.height, pixels); let handle = iced::advanced::image::Handle::from_rgba(width, height, pixels);
self.handle = Arc::new(handle); self.handle = Arc::new(handle);
} }
@@ -29,27 +39,24 @@ impl BlurHash {
BlurHash { BlurHash {
hash, hash,
handle, handle,
width: 32, width: 32.into(),
height: 32, height: 32.into(),
punch: 1.0, punch: 1.0,
} }
} }
pub fn width(mut self, height: u32) -> Self { pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = height; self.width = width.into();
self.recompute();
self self
} }
pub fn height(mut self, height: u32) -> Self { pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height; self.height = height.into();
self.recompute();
self self
} }
pub fn punch(mut self, punch: f32) -> Self { pub fn punch(mut self, punch: f32) -> Self {
self.punch = punch; self.punch = punch;
self.recompute();
self self
} }
} }
@@ -60,8 +67,8 @@ where
{ {
fn size(&self) -> iced::Size<iced::Length> { fn size(&self) -> iced::Size<iced::Length> {
iced::Size { iced::Size {
width: iced::Length::Fixed(self.width as f32), width: self.width,
height: iced::Length::Fixed(self.height as f32), height: self.height,
} }
} }
@@ -71,17 +78,21 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &iced::advanced::layout::Limits, limits: &iced::advanced::layout::Limits,
) -> iced::advanced::layout::Node { ) -> iced::advanced::layout::Node {
iced::widget::image::layout( let layout = iced::widget::image::layout(
renderer, renderer,
limits, limits,
&self.handle, &self.handle,
self.width.into(), self.width,
self.height.into(), self.height,
None, None,
iced::ContentFit::default(), iced::ContentFit::default(),
iced::Rotation::default(), iced::Rotation::default(),
false, false,
) );
let height = layout.bounds().height;
let width = layout.bounds().width;
self.recompute(width as u32, height as u32, self.punch);
layout
} }
fn draw( fn draw(

View File

@@ -1,17 +1,19 @@
mod shared_string; mod shared_string;
use iced_video_player::{Video, VideoPlayer};
use shared_string::SharedString; use shared_string::SharedString;
use std::sync::Arc;
mod blur_hash; mod blur_hash;
use blur_hash::BlurHash; use blur_hash::BlurHash;
use iced::{Alignment, Element, Length, Task, widget::*}; mod preview;
use preview::Preview;
use iced::{Alignment, Element, Length, Shadow, Task, widget::*};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Loading { pub struct Loading {}
to: Screen,
from: Screen,
}
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct ItemCache { pub struct ItemCache {
@@ -25,6 +27,7 @@ impl ItemCache {
self.tree.entry(parent).or_default().insert(item.id); self.tree.entry(parent).or_default().insert(item.id);
self.items.insert(item.id, item); self.items.insert(item.id, item);
} }
pub fn extend<I: IntoIterator<Item = Item>>( pub fn extend<I: IntoIterator<Item = Item>>(
&mut self, &mut self,
parent: impl Into<Option<uuid::Uuid>>, parent: impl Into<Option<uuid::Uuid>>,
@@ -35,6 +38,7 @@ impl ItemCache {
self.insert(parent, item); self.insert(parent, item);
}); });
} }
pub fn items_of(&self, parent: impl Into<Option<uuid::Uuid>>) -> Vec<&Item> { pub fn items_of(&self, parent: impl Into<Option<uuid::Uuid>>) -> Vec<&Item> {
let parent = parent.into(); let parent = parent.into();
self.tree.get(&None); self.tree.get(&None);
@@ -47,6 +51,10 @@ impl ItemCache {
}) })
.unwrap_or_default() .unwrap_or_default()
} }
pub fn get(&self, id: &uuid::Uuid) -> Option<&Item> {
self.items.get(id)
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -72,6 +80,7 @@ impl From<api::jellyfin::BaseItemDto> for Item {
.and_then(|hashes| hashes.get(&tag).cloned()) .and_then(|hashes| hashes.get(&tag).cloned())
.map(|s| s.clone().into()), .map(|s| s.clone().into()),
}), }),
_type: dto._type,
} }
} }
} }
@@ -82,6 +91,7 @@ pub struct Item {
pub parent_id: Option<uuid::Uuid>, pub parent_id: Option<uuid::Uuid>,
pub name: Option<SharedString>, pub name: Option<SharedString>,
pub thumbnail: Option<Thumbnail>, pub thumbnail: Option<Thumbnail>,
pub _type: api::jellyfin::BaseItemKind,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@@ -89,7 +99,8 @@ pub enum Screen {
#[default] #[default]
Home, Home,
Settings, Settings,
Profile, User,
Video,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct State { struct State {
@@ -98,6 +109,15 @@ struct State {
cache: ItemCache, cache: ItemCache,
jellyfin_client: api::JellyfinClient, jellyfin_client: api::JellyfinClient,
messages: Vec<String>, messages: Vec<String>,
history: Vec<Option<uuid::Uuid>>,
query: Option<String>,
screen: Screen,
// Login form state
username_input: String,
password_input: String,
is_authenticated: bool,
// Video
video: Option<Arc<Video>>,
} }
impl State { impl State {
@@ -108,6 +128,13 @@ impl State {
cache: ItemCache::default(), cache: ItemCache::default(),
jellyfin_client, jellyfin_client,
messages: Vec::new(), messages: Vec::new(),
history: Vec::new(),
query: None,
screen: Screen::Home,
username_input: String::new(),
password_input: String::new(),
is_authenticated: false,
video: None,
} }
} }
} }
@@ -115,21 +142,117 @@ impl State {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
OpenSettings, OpenSettings,
CloseSettings,
Refresh, Refresh,
Search,
SearchQueryChanged(String),
OpenItem(Option<uuid::Uuid>), OpenItem(Option<uuid::Uuid>),
LoadedItem(Option<uuid::Uuid>, Vec<Item>), LoadedItem(Option<uuid::Uuid>, Vec<Item>),
Error(String), Error(String),
SetToken(String), SetToken(String),
Back,
Home,
// Login-related messages
UsernameChanged(String),
PasswordChanged(String),
Login,
LoginSuccess(String),
Logout,
Video(VideoMessage),
}
#[derive(Debug, Clone)]
pub enum VideoMessage {
EndOfStream,
Open(url::Url),
Pause,
Play,
Seek(f64),
Stop,
Test,
} }
fn update(state: &mut State, message: Message) -> Task<Message> { fn update(state: &mut State, message: Message) -> Task<Message> {
match message { match message {
Message::OpenSettings => { Message::OpenSettings => {
// Foo state.screen = Screen::Settings;
Task::none()
}
Message::CloseSettings => {
state.screen = Screen::Home;
Task::none()
}
Message::UsernameChanged(username) => {
state.username_input = username;
Task::none()
}
Message::PasswordChanged(password) => {
state.password_input = password;
Task::none()
}
Message::Login => {
let username = state.username_input.clone();
let password = state.password_input.clone();
// Update the client config with the new credentials
let mut config = (*state.jellyfin_client.config).clone();
config.username = username;
config.password = password;
Task::perform(
async move {
let mut client = api::JellyfinClient::new(config);
client.authenticate().await
},
|result| match result {
Ok(auth_result) => {
if let Some(token) = auth_result.access_token {
Message::LoginSuccess(token)
} else {
Message::Error("Authentication failed: No token received".to_string())
}
}
Err(e) => Message::Error(format!("Login failed: {}", e)),
},
)
}
Message::LoginSuccess(token) => {
state.jellyfin_client.set_token(token.clone());
state.is_authenticated = true;
state.password_input.clear();
state.messages.push("Login successful!".to_string());
state.screen = Screen::Home;
// Save token and refresh items
let client = state.jellyfin_client.clone();
Task::perform(
async move {
let _ = client.save_token(".session").await;
},
|_| Message::Refresh,
)
}
Message::Logout => {
state.is_authenticated = false;
state.jellyfin_client.set_token("");
state.cache = ItemCache::default();
state.current = None;
state.username_input.clear();
state.password_input.clear();
state.messages.push("Logged out successfully".to_string());
Task::none() Task::none()
} }
Message::OpenItem(id) => { Message::OpenItem(id) => {
let client = state.jellyfin_client.clone(); let client = state.jellyfin_client.clone();
use api::jellyfin::BaseItemKind::*;
if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id))
&& matches!(cached._type, Video | Movie | Episode)
{
let url = client
.stream_url(id.expect("ID exists"))
.expect("Failed to get stream URL");
Task::done(Message::Video(VideoMessage::Open(url)))
} else {
Task::perform( Task::perform(
async move { async move {
let items: Result<Vec<Item>, api::JellyfinApiError> = client let items: Result<Vec<Item>, api::JellyfinApiError> = client
@@ -144,8 +267,10 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
}, },
) )
} }
}
Message::LoadedItem(id, items) => { Message::LoadedItem(id, items) => {
state.cache.extend(id, items); state.cache.extend(id, items);
state.history.push(state.current);
state.current = id; state.current = id;
Task::none() Task::none()
} }
@@ -175,34 +300,148 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
Message::SetToken(token) => { Message::SetToken(token) => {
tracing::info!("Authenticated with token: {}", token); tracing::info!("Authenticated with token: {}", token);
state.jellyfin_client.set_token(token); state.jellyfin_client.set_token(token);
state.is_authenticated = true;
Task::none() Task::none()
} }
Message::Back => {
state.current = state.history.pop().unwrap_or(None);
Task::none()
}
Message::Home => {
state.current = None;
Task::done(Message::Refresh)
}
Message::SearchQueryChanged(query) => {
state.query = Some(query);
// Handle search query change
Task::none()
}
Message::Search => {
// Handle search action
let client = state.jellyfin_client.clone();
let query = state.query.clone().unwrap_or_default();
Task::perform(async move { client.search(query).await }, |r| match r {
Err(e) => Message::Error(format!("Search failed: {}", e)),
Ok(items) => {
let items = items.into_iter().map(Item::from).collect();
Message::LoadedItem(None, items)
}
})
}
Message::Video(msg) => match msg {
VideoMessage::EndOfStream => {
state.video = None;
Task::none()
}
VideoMessage::Open(url) => {
state.video = Video::new(&url)
.inspect_err(|err| {
tracing::error!("Failed to play video at {}: {:?}", url, err);
})
.ok()
.map(Arc::new);
Task::none()
}
VideoMessage::Pause => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(true);
}
Task::none()
}
VideoMessage::Play => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
video.set_paused(false);
}
Task::none()
}
VideoMessage::Seek(position) => {
// if let Some(ref video) = state.video {
// // video.seek(position, true);
// }
Task::none()
}
VideoMessage::Stop => {
state.video = None;
Task::none()
}
VideoMessage::Test => {
let url = url::Url::parse(
// "file:///home/servius/Projects/jello/crates/iced_video_player/.media/test.mp4",
"https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
)
.unwrap();
state.video = Video::new(&url)
.inspect_err(|err| {
dbg!(err);
})
.ok()
.map(Arc::new);
Task::none()
}
},
} }
} }
fn view(state: &State) -> Element<'_, Message> { fn view(state: &State) -> Element<'_, Message> {
column([header(state), body(state), footer(state)]).into() match state.screen {
Screen::Settings => settings(state),
Screen::Home | _ => home(state),
}
}
fn home(state: &State) -> Element<'_, Message> {
column([header(state), body(state), footer(state)])
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn player(video: &Video) -> Element<'_, Message> {
container(
VideoPlayer::new(video)
.width(Length::Fill)
.height(Length::Fill)
.content_fit(iced::ContentFit::Contain)
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
)
.style(|_| container::background(iced::Color::BLACK))
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
} }
fn body(state: &State) -> Element<'_, Message> { fn body(state: &State) -> Element<'_, Message> {
if let Some(ref video) = state.video {
player(video)
} else {
scrollable(
container( container(
Grid::with_children(state.cache.items_of(state.current).into_iter().map(card)).spacing(70), Grid::with_children(state.cache.items_of(state.current).into_iter().map(card))
.fluid(400)
.spacing(50),
) )
.padding(70) .padding(50)
.align_x(Alignment::Center) .align_x(Alignment::Center)
// .align_y(Alignment::Center) // .align_y(Alignment::Center)
.height(Length::Fill) .height(Length::Fill)
.width(Length::Fill) .width(Length::Fill),
)
.height(Length::Fill)
.into() .into()
} }
}
fn header(state: &State) -> Element<'_, Message> { fn header(state: &State) -> Element<'_, Message> {
row([ row([
container( container(
Button::new(
Text::new(state.jellyfin_client.config.server_url.as_str()) Text::new(state.jellyfin_client.config.server_url.as_str())
.width(Length::Fill)
.align_x(Alignment::Start), .align_x(Alignment::Start),
) )
.on_press(Message::Home),
)
.padding(10) .padding(10)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
@@ -210,10 +449,14 @@ fn header(state: &State) -> Element<'_, Message> {
.align_y(Alignment::Center) .align_y(Alignment::Center)
.style(container::rounded_box) .style(container::rounded_box)
.into(), .into(),
search(state),
container( container(
row([ row([
button("Settings").on_press(Message::OpenSettings).into(),
button("Refresh").on_press(Message::Refresh).into(), button("Refresh").on_press(Message::Refresh).into(),
button("Settings").on_press(Message::OpenSettings).into(),
button("TestVideo")
.on_press(Message::Video(VideoMessage::Test))
.into(),
]) ])
.spacing(10), .spacing(10),
) )
@@ -231,6 +474,22 @@ fn header(state: &State) -> Element<'_, Message> {
.into() .into()
} }
fn search(state: &State) -> Element<'_, Message> {
container(
TextInput::new("Search...", state.query.as_deref().unwrap_or_default())
.padding(10)
.size(16)
.width(Length::Fill)
.on_input(Message::SearchQueryChanged)
.on_submit(Message::Search),
)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(container::rounded_box)
.into()
}
fn footer(state: &State) -> Element<'_, Message> { fn footer(state: &State) -> Element<'_, Message> {
container( container(
column( column(
@@ -249,12 +508,130 @@ fn footer(state: &State) -> Element<'_, Message> {
.into() .into()
} }
fn settings(state: &State) -> Element<'_, Message> {
let content = if state.is_authenticated {
// Authenticated view - show user info and logout
column([
Text::new("Settings").size(32).into(),
container(
column([
Text::new("Account").size(24).into(),
Text::new("Server URL").size(14).into(),
Text::new(state.jellyfin_client.config.server_url.as_str())
.size(12)
.into(),
container(Text::new("Status: Logged In").size(14))
.padding(10)
.width(Length::Fill)
.into(),
container(
row([
Button::new(Text::new("Logout"))
.padding(10)
.on_press(Message::Logout)
.into(),
Button::new(Text::new("Close"))
.padding(10)
.on_press(Message::CloseSettings)
.into(),
])
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.into(),
])
.spacing(10)
.max_width(400)
.align_x(Alignment::Center),
)
.padding(20)
.width(Length::Fill)
.align_x(Alignment::Center)
.style(container::rounded_box)
.into(),
])
.spacing(20)
.padding(50)
.align_x(Alignment::Center)
} else {
// Not authenticated view - show login form
column([
Text::new("Settings").size(32).into(),
container(
column([
Text::new("Login to Jellyfin").size(24).into(),
Text::new("Server URL").size(14).into(),
Text::new(state.jellyfin_client.config.server_url.as_str())
.size(12)
.into(),
container(
TextInput::new("Username", &state.username_input)
.padding(10)
.size(16)
.on_input(Message::UsernameChanged),
)
.padding(10)
.width(Length::Fill)
.into(),
container(
TextInput::new("Password", &state.password_input)
.padding(10)
.size(16)
.secure(true)
.on_input(Message::PasswordChanged)
.on_submit(Message::Login),
)
.padding(10)
.width(Length::Fill)
.into(),
container(
row([
Button::new(Text::new("Login"))
.padding(10)
.on_press(Message::Login)
.into(),
Button::new(Text::new("Cancel"))
.padding(10)
.on_press(Message::CloseSettings)
.into(),
])
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.into(),
])
.spacing(10)
.max_width(400)
.align_x(Alignment::Center),
)
.padding(20)
.width(Length::Fill)
.align_x(Alignment::Center)
.style(container::rounded_box)
.into(),
])
.spacing(20)
.padding(50)
.align_x(Alignment::Center)
};
container(content)
.width(Length::Fill)
.height(Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center)
.into()
}
fn card(item: &Item) -> Element<'_, Message> { fn card(item: &Item) -> Element<'_, Message> {
let name = item let name = item
.name .name
.as_ref() .as_ref()
.map(|s| s.as_ref()) .map(|s| s.as_ref())
.unwrap_or("Unnamed Item"); .unwrap_or("Unnamed Item");
MouseArea::new(
container( container(
column([ column([
BlurHash::new( BlurHash::new(
@@ -264,22 +641,27 @@ fn card(item: &Item) -> Element<'_, Message> {
.map(|s| s.as_ref()) .map(|s| s.as_ref())
.unwrap_or(""), .unwrap_or(""),
) )
.width(200) .width(Length::Fill)
.height(400) .height(Length::FillPortion(5))
.into(),
Text::new(name)
.size(16)
.align_y(Alignment::Center)
.height(Length::FillPortion(1))
.into(), .into(),
Text::new(name).size(16).into(),
]) ])
.align_x(Alignment::Center) .align_x(Alignment::Center)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill), .height(Length::Fill),
) )
.padding(70) .style(container::rounded_box),
.width(Length::FillPortion(5)) )
.height(Length::FillPortion(5)) .on_press(Message::OpenItem(Some(item.id)))
.style(container::rounded_box)
.into() .into()
} }
// fn video(url: &str
fn init(config: impl Fn() -> api::JellyfinConfig + 'static) -> impl Fn() -> (State, Task<Message>) { fn init(config: impl Fn() -> api::JellyfinConfig + 'static) -> impl Fn() -> (State, Task<Message>) {
move || { move || {
let mut jellyfin = api::JellyfinClient::new(config()); let mut jellyfin = api::JellyfinClient::new(config());
@@ -291,7 +673,8 @@ fn init(config: impl Fn() -> api::JellyfinConfig + 'static) -> impl Fn() -> (Sta
Ok(token) => Message::SetToken(token), Ok(token) => Message::SetToken(token),
Err(e) => Message::Error(format!("Authentication failed: {}", e)), Err(e) => Message::Error(format!("Authentication failed: {}", e)),
}, },
), )
.chain(Task::done(Message::Refresh)),
) )
} }
} }

224
ui-iced/src/preview.rs Normal file
View File

@@ -0,0 +1,224 @@
use iced::{Animation, Function, advanced::image::Handle, widget::image};
use reqwest::Method;
use std::sync::Arc;
use crate::blur_hash::BlurHash;
#[derive(Clone)]
pub struct ImageDownloader {
client: reqwest::Client,
request_modifier:
Option<Arc<dyn Fn(reqwest::RequestBuilder) -> reqwest::RequestBuilder + Send + Sync>>,
}
impl core::fmt::Debug for ImageDownloader {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ImageDownloader")
.field("client", &self.client)
.field("request_modifier", &self.request_modifier.is_some())
.finish()
}
}
impl ImageDownloader {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
request_modifier: None,
}
}
pub fn with_modifier<F>(mut self, f: F) -> Self
where
F: Fn(reqwest::RequestBuilder) -> reqwest::RequestBuilder + Send + Sync + 'static,
{
self.request_modifier = Some(Arc::new(f));
self
}
pub async fn download(&self, url: &str) -> reqwest::Result<bytes::Bytes> {
use ::tap::*;
let response = self
.client
.request(Method::GET, url)
.pipe(|builder| {
if let Some(ref modifier) = self.request_modifier {
modifier(builder)
} else {
builder
}
})
.send()
.await?;
let bytes = response.bytes().await?;
Ok(bytes)
}
}
#[derive(Clone, Debug)]
pub enum Preview {
Thumbnail {
thumbnail: Image,
blur_hash: BlurHash,
},
BlurHash {
blur_hash: BlurHash,
},
}
#[derive(Debug, Clone)]
pub struct ImageCache {
cache: std::collections::HashMap<uuid::Uuid, Preview>,
downloader: ImageDownloader,
}
impl Preview {
pub fn thumbnail(image: Image, blur_hash: BlurHash) -> Self {
Preview::Thumbnail {
thumbnail: image,
blur_hash,
}
}
pub fn blur_hash(blur_hash: BlurHash) -> Self {
Preview::BlurHash { blur_hash }
}
// pub fn upgrade(
// self,
// fut: impl core::future::Future<Output = bytes::Bytes> + 'static + Send,
// ) -> iced::Task<PreviewMessage> {
// // let sip = iced::task::sipper(async move |mut sender| {
// // let bytes = fut.await;
// // let handle = Handle::from_bytes(bytes.clone());
// // let allocation = image::allocate(handle);
// // let image = Image {
// // bytes,
// // handle,
// // allocation,
// // fade_in: Animation::new(false),
// // };
// // let _ = sender.send(image).await;
// // });
// // iced::Task::sip(sip, ||)
// Task::
// }
}
enum PreviewMessage {
BlurHashLoaded(uuid::Uuid, BlurHash),
ThumbnailDownloaded(uuid::Uuid, bytes::Bytes),
// ThumbnailAllocated(
// uuid::Uuid,
// Result<image::Allocation, iced::advanced::image::Error>,
// ),
ThumbnailLoaded(uuid::Uuid, Image),
Failed(uuid::Uuid, String),
}
#[derive(Clone, Debug)]
pub struct Image {
bytes: bytes::Bytes,
handle: Handle,
allocation: image::Allocation,
fade_in: Animation<bool>,
}
#[derive(Clone, Debug)]
pub struct PreviewSource {
pub id: uuid::Uuid,
pub url: String,
pub width: iced::Length,
pub height: iced::Length,
pub blur_hash: Option<String>,
}
impl PreviewSource {
pub fn new(id: uuid::Uuid, url: String) -> Self {
Self {
id,
url,
width: iced::Length::Fill,
height: iced::Length::Fill,
blur_hash: None,
}
}
pub fn blur_hash(mut self, blur_hash: String) -> Self {
self.blur_hash = Some(blur_hash);
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
pub fn width(mut self, width: iced::Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: iced::Length) -> Self {
self.height = height;
self
}
// pub fn upgrade(self) -> Task<PreviewMessage> {
// let sip = iced::task::sipper(async move |mut sender| {
// let bytes = fut.await;
// let handle = Handle::from_bytes(bytes.clone());
// let allocation = image::allocate(handle);
// let image = Image {
// bytes,
// handle,
// allocation,
// fade_in: Animation::new(false),
// };
// let _ = sender.send(image).await;
// });
// // iced::Task::sip(sip, ||)
// }
}
impl ImageCache {
pub fn upgrade(&self, source: PreviewSource) -> iced::Task<PreviewMessage> {
let downloader = self.downloader.clone();
let sipper = iced::task::sipper(async move |mut sender| {
if let Some(blur_hash_str) = source.blur_hash {
let blur_hash = BlurHash::new(&blur_hash_str);
let _ = sender.send(blur_hash).await;
}
let bytes = downloader.download(&source.url).await?;
reqwest::Result::<bytes::Bytes>::Ok(bytes)
});
iced::Task::sip(
sipper,
move |progress| PreviewMessage::BlurHashLoaded(source.id, progress),
move |output: reqwest::Result<bytes::Bytes>| match output {
Ok(bytes) => PreviewMessage::ThumbnailDownloaded(source.id, bytes),
Err(e) => PreviewMessage::Failed(source.id, e.to_string()),
},
)
.then(|message| match message {
PreviewMessage::ThumbnailDownloaded(id, bytes) => {
let handle = Handle::from_bytes(bytes.clone());
let allocation = image::allocate(&handle);
allocation.map(move |output| match output {
Ok(allocation) => {
let image = Image {
bytes: bytes.clone(),
handle: handle.clone(),
allocation,
fade_in: Animation::new(false),
};
PreviewMessage::ThumbnailLoaded(id, image)
}
Err(e) => PreviewMessage::Failed(id, e.to_string()),
})
}
other => iced::Task::done(other),
})
}
}