local: integrate yoda libcosmic and fix thumbnail hang
Some checks are pending
Validate .desktop files / validate (push) Waiting to run

This commit is contained in:
Lionel DARNIS 2026-05-18 12:22:47 +02:00
parent d1f63c570c
commit 22e5890a7c
16 changed files with 2388 additions and 310 deletions

4
.gitignore vendored
View file

@ -5,4 +5,8 @@
/debian/files
/target/
/vendor/
!/vendor/
/vendor/*
!/vendor/iced_video_player/
!/vendor/iced_video_player/**
/vendor.tar

401
Cargo.lock generated
View file

@ -153,12 +153,6 @@ dependencies = [
"equator",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "almost"
version = "0.2.0"
@ -308,12 +302,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-raw-xcb-connection"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]]
name = "as-slice"
version = "0.2.1"
@ -884,7 +872,7 @@ dependencies = [
[[package]]
name = "clipboard_macos"
version = "0.1.0"
source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c"
source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85"
dependencies = [
"objc",
"objc-foundation",
@ -894,22 +882,13 @@ dependencies = [
[[package]]
name = "clipboard_wayland"
version = "0.2.2"
source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c"
source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85"
dependencies = [
"dnd",
"mime",
"smithay-clipboard",
]
[[package]]
name = "clipboard_x11"
version = "0.4.2"
source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c"
dependencies = [
"thiserror 1.0.69",
"x11rb",
]
[[package]]
name = "cocoa"
version = "0.25.0"
@ -1074,7 +1053,6 @@ dependencies = [
[[package]]
name = "cosmic-config"
version = "1.0.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"atomicwrites",
"cosmic-config-derive",
@ -1095,7 +1073,6 @@ dependencies = [
[[package]]
name = "cosmic-config-derive"
version = "1.0.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"quote",
"syn",
@ -1130,7 +1107,7 @@ dependencies = [
"icu_collator",
"icu_locale",
"image",
"libcosmic",
"libcosmic-yoda",
"log",
"mpris-server",
"rust-embed",
@ -1167,8 +1144,7 @@ dependencies = [
[[package]]
name = "cosmic-text"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73"
source = "git+https://github.com/pop-os/cosmic-text.git#c24886c2471e5606587c46090cd25dbbf209186b"
dependencies = [
"bitflags 2.11.1",
"fontdb",
@ -1191,7 +1167,6 @@ dependencies = [
[[package]]
name = "cosmic-theme"
version = "1.0.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"almost",
"configparser",
@ -1257,7 +1232,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "cryoglyph"
version = "0.1.0"
source = "git+https://github.com/iced-rs/cryoglyph.git?rev=e429a025df36ab8145708acb309080ae3deec17a#e429a025df36ab8145708acb309080ae3deec17a"
source = "git+https://github.com/pop-os/glyphon.git?tag=cosmic-0.14#c49de15bce4d8254ac136d1be9911960cc85ce12"
dependencies = [
"cosmic-text",
"etagere",
@ -1294,15 +1269,6 @@ dependencies = [
"uncased",
]
[[package]]
name = "ctor"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf"
dependencies = [
"dtor",
]
[[package]]
name = "cursor-icon"
version = "1.2.0"
@ -1448,7 +1414,7 @@ dependencies = [
[[package]]
name = "dnd"
version = "0.1.0"
source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c"
source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85"
dependencies = [
"bitflags 2.11.1",
"mime",
@ -1477,51 +1443,6 @@ name = "dpi"
version = "0.1.2"
source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366"
[[package]]
name = "drm"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde"
dependencies = [
"bitflags 2.11.1",
"bytemuck",
"drm-ffi",
"drm-fourcc",
"rustix 0.38.44",
]
[[package]]
name = "drm-ffi"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6"
dependencies = [
"drm-sys",
"rustix 0.38.44",
]
[[package]]
name = "drm-fourcc"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"
[[package]]
name = "drm-sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176"
dependencies = [
"libc",
"linux-raw-sys 0.6.5",
]
[[package]]
name = "dtor"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b"
[[package]]
name = "either"
version = "1.15.0"
@ -2018,16 +1939,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix 1.1.4",
"windows-link 0.2.1",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@ -2190,17 +2101,34 @@ dependencies = [
]
[[package]]
name = "gpu-allocator"
version = "0.28.0"
name = "gpu-alloc"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"
checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
dependencies = [
"bitflags 2.11.1",
"gpu-alloc-types",
]
[[package]]
name = "gpu-alloc-types"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
dependencies = [
"bitflags 2.11.1",
]
[[package]]
name = "gpu-allocator"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
dependencies = [
"ash",
"hashbrown 0.16.1",
"log",
"presser",
"thiserror 2.0.18",
"windows 0.62.2",
"thiserror 1.0.69",
"windows 0.58.0",
]
[[package]]
@ -2486,8 +2414,6 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
@ -2600,7 +2526,6 @@ dependencies = [
[[package]]
name = "iced"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"dnd",
"iced_accessibility",
@ -2621,7 +2546,6 @@ dependencies = [
[[package]]
name = "iced_accessibility"
version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"accesskit",
"accesskit_winit",
@ -2630,7 +2554,6 @@ dependencies = [
[[package]]
name = "iced_core"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"bitflags 2.11.1",
"bytes",
@ -2654,7 +2577,6 @@ dependencies = [
[[package]]
name = "iced_debug"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"iced_core",
"iced_futures",
@ -2664,7 +2586,6 @@ dependencies = [
[[package]]
name = "iced_futures"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"futures",
"iced_core",
@ -2678,7 +2599,6 @@ dependencies = [
[[package]]
name = "iced_graphics"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"bitflags 2.11.1",
"bytemuck",
@ -2699,7 +2619,6 @@ dependencies = [
[[package]]
name = "iced_program"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"iced_graphics",
"iced_runtime",
@ -2708,7 +2627,6 @@ dependencies = [
[[package]]
name = "iced_renderer"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"iced_graphics",
"iced_tiny_skia",
@ -2720,7 +2638,6 @@ dependencies = [
[[package]]
name = "iced_runtime"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"bytes",
"cosmic-client-toolkit",
@ -2735,7 +2652,6 @@ dependencies = [
[[package]]
name = "iced_tiny_skia"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"bytemuck",
"cosmic-text",
@ -2752,7 +2668,6 @@ dependencies = [
[[package]]
name = "iced_video_player"
version = "0.6.0"
source = "git+https://github.com/wash2/iced_video_player.git?branch=iced-rebase#cb3635192f3dec37619855c904a168ca37a3dac3"
dependencies = [
"glib",
"gstreamer",
@ -2761,7 +2676,7 @@ dependencies = [
"gstreamer-pbutils",
"html-escape",
"iced_wgpu",
"libcosmic",
"libcosmic-yoda",
"log",
"thiserror 2.0.18",
"url",
@ -2770,9 +2685,7 @@ dependencies = [
[[package]]
name = "iced_wgpu"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"as-raw-xcb-connection",
"bitflags 2.11.1",
"bytemuck",
"cosmic-client-toolkit",
@ -2789,19 +2702,16 @@ dependencies = [
"rustc-hash 2.1.2",
"rustix 0.38.44",
"thiserror 2.0.18",
"tiny-xlib",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-sys",
"wgpu",
"x11rb",
]
[[package]]
name = "iced_widget"
version = "0.14.2"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"cosmic-client-toolkit",
"dnd",
@ -2819,7 +2729,6 @@ dependencies = [
[[package]]
name = "iced_winit"
version = "0.14.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
dependencies = [
"cosmic-client-toolkit",
"cursor-icon",
@ -3379,9 +3288,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libcosmic"
version = "1.0.0"
source = "git+https://github.com/pop-os/libcosmic.git#4fab6c777dbd1a023440f08fb2b729c86492366c"
name = "libcosmic-yoda"
version = "0.1.0-yoda.2"
dependencies = [
"apply",
"ashpd 0.12.3",
@ -3483,12 +3391,6 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@ -3643,9 +3545,9 @@ dependencies = [
[[package]]
name = "metal"
version = "0.33.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605"
dependencies = [
"bitflags 2.11.1",
"block",
@ -3659,7 +3561,7 @@ dependencies = [
[[package]]
name = "mime"
version = "0.1.0"
source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c"
source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85"
dependencies = [
"smithay-clipboard",
]
@ -3723,9 +3625,9 @@ checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af"
[[package]]
name = "naga"
version = "28.0.0"
version = "27.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135"
checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
dependencies = [
"arrayvec",
"bit-set",
@ -5334,12 +5236,10 @@ name = "softbuffer"
version = "0.4.1"
source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#c2b2c19ddb38ff17495643699f97cb1f2064a1be"
dependencies = [
"as-raw-xcb-connection",
"bytemuck",
"cfg_aliases",
"cocoa",
"core-graphics",
"drm",
"fastrand",
"foreign-types",
"js-sys",
@ -5349,14 +5249,12 @@ dependencies = [
"raw-window-handle",
"redox_syscall 0.5.18",
"rustix 0.38.44",
"tiny-xlib",
"wasm-bindgen",
"wayland-backend",
"wayland-client",
"wayland-sys",
"web-sys",
"windows-sys 0.52.0",
"x11rb",
]
[[package]]
@ -5619,19 +5517,6 @@ dependencies = [
"strict-num",
]
[[package]]
name = "tiny-xlib"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4"
dependencies = [
"as-raw-xcb-connection",
"ctor",
"libloading",
"pkg-config",
"tracing",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@ -6366,13 +6251,12 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "wgpu"
version = "28.0.0"
version = "27.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f"
checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
dependencies = [
"arrayvec",
"bitflags 2.11.1",
"bytemuck",
"cfg-if",
"cfg_aliases",
"document-features",
@ -6396,9 +6280,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "28.0.1"
version = "27.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d23f4642f53f666adcfd2d3218ab174d1e6681101aef18696b90cbe64d1c10f9"
checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
dependencies = [
"arrayvec",
"bit-set",
@ -6428,36 +6312,36 @@ dependencies = [
[[package]]
name = "wgpu-core-deps-apple"
version = "28.0.0"
version = "27.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da"
checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-emscripten"
version = "28.0.0"
version = "27.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79"
checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "28.0.0"
version = "27.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae"
checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
version = "28.0.1"
version = "27.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d6cb474beb218824dcc9e1ce679d973f719262789bfb27407da560cac20eeb"
checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
dependencies = [
"android_system_properties",
"arrayvec",
@ -6471,6 +6355,7 @@ dependencies = [
"core-graphics-types 0.2.0",
"glow",
"glutin_wgl_sys",
"gpu-alloc",
"gpu-allocator",
"gpu-descriptor",
"hashbrown 0.16.1",
@ -6497,20 +6382,21 @@ dependencies = [
"wasm-bindgen",
"web-sys",
"wgpu-types",
"windows 0.62.2",
"windows-core 0.62.2",
"windows 0.58.0",
"windows-core 0.58.0",
]
[[package]]
name = "wgpu-types"
version = "28.0.0"
version = "27.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c"
checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb"
dependencies = [
"bitflags 2.11.1",
"bytemuck",
"js-sys",
"log",
"thiserror 2.0.18",
"web-sys",
]
@ -6548,12 +6434,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "window_clipboard"
version = "0.4.1"
source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c"
source = "git+https://forge.aditua.com/leyoda/window_clipboard.git?branch=yoda-x11-optional#319db02e5219c557c8f03b0e33a8eb4075cabb85"
dependencies = [
"clipboard-win",
"clipboard_macos",
"clipboard_wayland",
"clipboard_x11",
"dnd",
"mime",
"raw-window-handle",
@ -6562,27 +6447,25 @@ dependencies = [
[[package]]
name = "windows"
version = "0.61.3"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-collections 0.2.0",
"windows-core 0.61.2",
"windows-future 0.2.1",
"windows-link 0.1.3",
"windows-numerics 0.2.0",
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.62.2"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link 0.1.3",
"windows-numerics",
]
[[package]]
@ -6595,12 +6478,16 @@ dependencies = [
]
[[package]]
name = "windows-collections"
version = "0.3.2"
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-core 0.62.2",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
@ -6609,26 +6496,13 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-future"
version = "0.2.1"
@ -6637,18 +6511,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading 0.1.0",
"windows-threading",
]
[[package]]
name = "windows-future"
version = "0.3.2"
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -6662,6 +6536,17 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
@ -6696,13 +6581,12 @@ dependencies = [
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-targets 0.52.6",
]
[[package]]
@ -6715,12 +6599,13 @@ dependencies = [
]
[[package]]
name = "windows-result"
version = "0.4.1"
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]]
@ -6732,15 +6617,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -6843,15 +6719,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@ -7013,7 +6880,6 @@ dependencies = [
"winit-wayland",
"winit-web",
"winit-win32",
"winit-x11",
]
[[package]]
@ -7064,7 +6930,6 @@ dependencies = [
"smol_str",
"tracing",
"winit-core",
"x11-dl",
"xkbcommon-dl",
]
@ -7182,29 +7047,6 @@ dependencies = [
"winit-core",
]
[[package]]
name = "winit-x11"
version = "0.31.0-beta.2"
source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#261cda54017f98a12dc55569c864430fe6770366"
dependencies = [
"bitflags 2.11.1",
"bytemuck",
"calloop",
"cursor-icon",
"dpi",
"libc",
"percent-encoding",
"raw-window-handle",
"rustix 1.1.4",
"smol_str",
"tracing",
"winit-common",
"winit-core",
"x11-dl",
"x11rb",
"xkbcommon-dl",
]
[[package]]
name = "winnow"
version = "1.0.2"
@ -7320,39 +7162,6 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "x11-dl"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
"libc",
"once_cell",
"pkg-config",
]
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"as-raw-xcb-connection",
"gethostname",
"libc",
"libloading",
"once_cell",
"rustix 1.1.4",
"x11rb-protocol",
"xcursor",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xcursor"
version = "0.3.10"

View file

@ -32,12 +32,12 @@ env_logger = "0.11"
log = "0.4"
[dependencies.iced_video_player]
git = "https://github.com/wash2/iced_video_player.git"
branch = "iced-rebase"
path = "vendor/iced_video_player"
default-features = false
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic.git"
[dependencies.cosmic]
package = "libcosmic-yoda"
path = "../libcosmic"
default-features = false
features = ["advanced-shaping", "tokio", "winit", "multi-window"]
@ -51,9 +51,9 @@ fork = "0.7"
[features]
default = ["mpris-server", "xdg-portal", "wgpu", "wayland"]
xdg-portal = ["ashpd", "libcosmic/xdg-portal"]
wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"]
wayland = ["libcosmic/wayland"]
xdg-portal = ["ashpd", "cosmic/xdg-portal"]
wgpu = ["iced_video_player/wgpu", "cosmic/wgpu"]
wayland = ["cosmic/wayland"]
[profile.release-with-debug]
inherits = "release"
@ -62,10 +62,6 @@ debug = true
# [patch.'https://github.com/wash2/iced_video_player']
# iced_video_player = { path = "../iced_video_player" }
# [patch.'https://github.com/pop-os/libcosmic']
# libcosmic = { path = "../libcosmic" }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }

View file

@ -1,2 +1,34 @@
# cosmic-player
WIP COSMIC media player
## Yoda local fork notes
This fork is used for the local COSMIC desktop build.
Local integration changes:
- `cosmic-player` depends on the local `libcosmic-yoda` crate from `../libcosmic`
instead of upstream `pop-os/libcosmic`.
- `iced_video_player` is vendored in `vendor/iced_video_player` so it uses the
same local `libcosmic-yoda` and `iced_wgpu` stack. This avoids mixing upstream
and local Iced/libcosmic types.
- The app now runs `App::close()` before application exit and before the Quit
menu action exits the runtime. This lets the GStreamer pipeline be dropped
normally instead of short-circuiting through `process::exit(0)`.
- Thumbnail generation has a bounded wait for video frames. Some files/codecs can
fail to produce a frame; the thumbnailer now returns an error instead of
spinning forever as a background `cosmic-player --thumbnail ...` process.
Validation commands:
```bash
cargo +1.93.0 check
cargo +1.93.0 tree -i libcosmic-yoda --depth 2
cargo +1.93.0 tree -i libcosmic --depth 2 || true
```
Local redeploy:
```bash
COSMIC_PLAYER_REPO=/home/lionel/Projets/COSMIC/cosmic-player ./redeploy.sh
```

View file

@ -7,10 +7,10 @@ use cosmic::cosmic_config::{self, CosmicConfigEntry};
use cosmic::iced::event::{self, Event};
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
use cosmic::iced::mouse::{Event as MouseEvent, ScrollDelta};
use cosmic::iced::window::{self, set_mode};
use cosmic::iced::{
Alignment, Background, Border, Color, ContentFit, Length, Limits, Subscription,
Alignment, Background, Border, Color, ContentFit, Length, Limits, Subscription, exit, window,
};
use cosmic::iced::window::set_mode;
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::{self, Slider, nav_bar, segmented_button};
use cosmic::{Application, ApplicationExt, Element, action, cosmic_theme, executor, font, theme};
@ -951,6 +951,11 @@ impl Application for App {
}
}
fn on_app_exit(&mut self) -> Option<Self::Message> {
self.close();
None
}
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Message> {
// Toggle open state and get clone of node data
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
@ -1606,7 +1611,8 @@ impl Application for App {
return self.update_config();
}
Message::WindowClose => {
process::exit(0);
self.close();
return exit();
}
}
Task::none()

2
vendor/iced_video_player/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.direnv

88
vendor/iced_video_player/Cargo.toml vendored Normal file
View file

@ -0,0 +1,88 @@
[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]
gstreamer = "0.25"
gstreamer-app = "0.25" # appsink
gstreamer-base = "0.25" # basesrc
gstreamer-pbutils = "0.25"
glib = "0.22" # gobject traits and error type
log = "0.4"
thiserror = "2"
url = "2" # media uri
html-escape = "0.2.13"
iced_wgpu = { path = "../../../libcosmic/iced/wgpu", default-features = false, optional = true }
[dependencies.cosmic]
package = "libcosmic-yoda"
path = "../../../libcosmic"
default-features = false
features = ["tokio", "winit"]
[dev-dependencies]
env_logger = "0.11"
[features]
default = ["wgpu"]
wgpu = ["cosmic/wgpu", "iced_wgpu"]
[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"]
# [patch.'https://github.com/pop-os/libcosmic']
# libcosmic = { path = "../libcosmic" }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
[dev_dependencies]
libcosmic = { git = "https://github.com/pop-os/libcosmic", features = [
"wgpu",
"wayland",
] }

176
vendor/iced_video_player/LICENSE-APACHE vendored Normal file
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

23
vendor/iced_video_player/LICENSE-MIT vendored Normal file
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.

64
vendor/iced_video_player/README.md vendored Normal file
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,154 @@
use std::time::Duration;
use cosmic::iced::{self, Program, Task, Theme};
use iced::{
application::Application,
widget::{Button, Column, Container, Row, Slider, Text},
};
use iced_video_player::{Video, VideoPlayer};
pub fn main() -> iced::Result {
application().run()
}
fn application() -> Application<impl Program<Message = Message, Theme = Theme>> {
iced::application(App::default, App::update, App::view).window_size((500.0, 800.0))
}
#[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::from_file_path(
std::path::PathBuf::from(file!())
.parent()
.unwrap()
.join("../.media/test.mp4")
.canonicalize()
.unwrap(),
)
.unwrap(),
)
.unwrap(),
position: 0.0,
dragging: false,
}
}
}
impl App {
fn update(&mut self, message: Message) -> Task<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();
}
}
}
Task::none()
}
fn view(&self) -> iced::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()
}
}

79
vendor/iced_video_player/src/lib.rs vendored Normal file
View file

@ -0,0 +1,79 @@
//! # 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`].
#[cfg(feature = "wgpu")]
mod pipeline;
mod video;
mod video_player;
pub use gstreamer as gst;
pub use gstreamer_app as gst_app;
pub use gstreamer_pbutils as gst_pbutils;
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),
}

447
vendor/iced_video_player/src/pipeline.rs vendored Normal file
View file

@ -0,0 +1,447 @@
use cosmic::iced::wgpu;
use cosmic::iced::widget::shader::{Pipeline, Primitive, Viewport};
use cosmic::iced::{self, Rectangle};
use std::{
collections::{btree_map::Entry, BTreeMap},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use crate::video::Frame;
#[repr(C)]
struct Uniforms {
rect: [f32; 4],
}
struct VideoEntry {
texture_y: wgpu::Texture,
texture_uv: wgpu::Texture,
uniforms: wgpu::Buffer,
bg0: wgpu::BindGroup,
alive: Arc<AtomicBool>,
}
pub 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, queue: &wgpu::Queue) -> 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: false,
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 width == 0 || height == 0 {
return;
}
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: None,
});
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: None,
});
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_video_player uniform buffer"),
size: std::mem::size_of::<Uniforms>() as _,
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: &buffer,
offset: 0,
size: None,
}),
},
],
});
entry.insert(VideoEntry {
texture_y,
texture_uv,
uniforms: buffer,
bg0: bind_group,
alive: Arc::clone(alive),
});
}
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.uniforms.destroy();
}
}
}
fn prepare(&mut self, queue: &wgpu::Queue, video_id: u64, bounds: &iced::Rectangle) {
if let Some(video) = self.videos.get(&video_id) {
let uniforms = Uniforms {
rect: [
bounds.x,
bounds.y,
bounds.x + bounds.width,
bounds.y + bounds.height,
],
};
queue.write_buffer(&video.uniforms, 0, unsafe {
std::slice::from_raw_parts(
&uniforms as *const _ as *const u8,
std::mem::size_of::<Uniforms>(),
)
});
}
self.cleanup();
}
fn draw(
&self,
target: &wgpu::TextureView,
encoder: &mut wgpu::CommandEncoder,
viewport: &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, &[]);
pass.set_viewport(
viewport.x as _,
viewport.y as _,
viewport.width as _,
viewport.height as _,
0.0,
1.0,
);
pass.draw(0..4, 0..1);
}
}
}
impl Pipeline for VideoPipeline {
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self
where
Self: Sized,
{
VideoPipeline::new(device, format, queue)
}
}
#[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 Pipeline = VideoPipeline;
fn prepare(
&self,
pipeline: &mut Self::Pipeline,
device: &wgpu::Device,
queue: &wgpu::Queue,
bounds: &Rectangle,
viewport: &Viewport,
) {
if self.upload_frame {
if let Some(readable) = self.frame.lock().expect("lock frame mutex").readable() {
pipeline.upload(
device,
queue,
self.video_id,
&self.alive,
self.size,
readable.as_slice(),
);
}
}
pipeline.prepare(queue, self.video_id, &bounds);
}
fn render(
&self,
pipeline: &Self::Pipeline,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clip_bounds: &Rectangle<u32>,
) {
pipeline.draw(target, encoder, &clip_bounds, self.video_id);
}
}

View file

@ -0,0 +1,58 @@
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 {
let quad = array<vec2<f32>, 6>(
uniforms.rect.xy,
uniforms.rect.zy,
uniforms.rect.xw,
uniforms.rect.zy,
uniforms.rect.zw,
uniforms.rect.xw,
);
var out: VertexOutput;
out.uv = vec2<f32>(0.0);
out.uv.x = select(0.0, 2.0, in_vertex_index == 1u);
out.uv.y = select(0.0, 2.0, in_vertex_index == 2u);
out.position = vec4<f32>(out.uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 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);
return vec4<f32>(rgb, 1.0);
}

664
vendor/iced_video_player/src/video.rs vendored Normal file
View file

@ -0,0 +1,664 @@
use crate::Error;
use cosmic::iced::widget::image as img;
use gstreamer as gst;
use gstreamer_app as gst_app;
use gstreamer_app::prelude::*;
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};
const THUMBNAIL_FRAME_TIMEOUT: Duration = Duration::from_secs(10);
/// 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 new() -> Self {
Self(gst::Sample::builder().build())
}
pub fn store(&mut self, sample: gst::Sample) -> Option<()> {
if sample.buffer().is_some() {
self.0 = sample;
Some(())
} else {
None
}
}
pub fn readable(&self) -> Option<gst::BufferMap<gst::buffer::Readable>> {
self.0.buffer().map(|x| x.map_readable().ok()).flatten()
}
}
#[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) has_video: bool,
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) redrawing: 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>,
#[cfg(not(feature = "wgpu"))]
pub(crate) handle_opt: Option<img::Handle>,
}
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,
)?,
};
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() {
worker.join().expect("failed to stop video thread");
}
}
}
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 caps=text/x-raw\" 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);
let pad = video_sink.pads().first().cloned().unwrap();
pipeline.set_state(gst::State::Playing)?;
// wait for up to 5 seconds until the decoder gets the source capabilities
pipeline.state(gst::ClockTime::from_seconds(5)).0?;
// extract resolution and framerate
// TODO(jazzfool): maybe we want to extract some other information too?
let (width, height, framerate, has_video) = if let Some(caps) = pad.current_caps() {
let s = caps.structure(0).ok_or(Error::Caps)?;
let width = s.get::<i32>("width").map_err(|_| Error::Caps)?;
let height = s.get::<i32>("height").map_err(|_| Error::Caps)?;
// resolution should be mod4
let width = ((width + 4 - 1) / 4) * 4;
let framerate = s
.get::<gst::Fraction>("framerate")
.map_err(|_| Error::Caps)?;
let framerate = framerate.numer() as f64 / framerate.denom() as f64;
(width, height, framerate, true)
} else {
log::warn!("Video caps not found, falling back to audio only");
(0, 0, 4.0, false)
};
if framerate.is_nan()
|| framerate.is_infinite()
|| framerate < 0.0
|| framerate.abs() < f64::EPSILON
{
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");
// NV12 = 12bpp
let frame = Arc::new(Mutex::new(Frame::new()));
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) {
match (|| -> 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 buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
let pts = buffer.pts().unwrap_or_default();
{
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 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 = text.buffer().ok_or(gst::FlowError::Error)?;
let pts = text.pts().unwrap_or_default();
let duration = text.duration().unwrap_or(gst::ClockTime::ZERO);
let map = text.map_readable().map_err(|_| gst::FlowError::Error)?;
let text = html_escape::decode_html_entities(
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(pts + duration);
}
Ok(())
})() {
Ok(()) => {}
Err(gst::FlowError::Eos) => {
if !has_video {
// Simulate frame upload when there is no video stream
upload_frame_ref.swap(true, Ordering::SeqCst);
std::thread::sleep(Duration::from_secs_f64(1.0 / framerate));
}
}
Err(err) => {
log::error!("error pulling frame: {err}");
}
}
}
});
Ok(Video(RwLock::new(Internal {
id,
bus: pipeline.bus().unwrap(),
source: pipeline,
alive,
worker: Some(worker),
has_video,
width,
height,
framerate,
duration,
speed: 1.0,
sync_av,
frame,
upload_frame,
redrawing: Arc::new(AtomicBool::new(false)),
last_frame_time,
looping: false,
is_eos: false,
restart_stream: false,
sync_av_avg: 0,
sync_av_counter: 0,
subtitle_text,
upload_text,
#[cfg(not(feature = "wgpu"))]
handle_opt: None,
})))
}
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")
}
pub fn has_video(&self) -> bool {
self.read().has_video
}
/// 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::<String>("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);
let started = Instant::now();
while !inner.upload_frame.load(Ordering::SeqCst) {
if started.elapsed() >= THUMBNAIL_FRAME_TIMEOUT {
return Err(Error::Sync);
}
std::thread::sleep(Duration::from_millis(5));
}
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
}
}
pub(crate) 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,476 @@
use crate::{gst, gst_pbutils, video::Video};
use cosmic::iced::{
self,
advanced::{self, layout, widget, Widget},
mouse, Element,
};
use gstreamer_app::prelude::*;
use log::error;
use std::{marker::PhantomData, sync::atomic::Ordering, time::Duration};
use std::{sync::Arc, time::Instant};
#[cfg(feature = "wgpu")]
use crate::pipeline::VideoPrimitive;
#[cfg(feature = "wgpu")]
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
#[cfg(not(feature = "wgpu"))]
use crate::video::yuv_to_rgba;
#[cfg(not(feature = "wgpu"))]
use cosmic::iced::advanced::image::Renderer as ImageRenderer;
#[cfg(not(feature = "wgpu"))]
trait PrimitiveRenderer: ImageRenderer<Handle = advanced::image::Handle> {}
#[cfg(not(feature = "wgpu"))]
impl PrimitiveRenderer for iced::Renderer {}
/// 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,
mouse_hidden: bool,
on_duration_changed: Option<Box<dyn Fn(Duration) -> Message + 'a>>,
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>>,
on_missing_plugin: Option<Box<dyn Fn(gst::Message) -> Message + 'a>>,
on_tags: Option<Box<dyn Fn(gst::TagList) -> Message + 'a>>,
on_warning: Option<Box<dyn Fn(glib::Error) -> Message + 'a>>,
id: Option<cosmic::widget::Id>,
_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::Contain,
width: iced::Length::Shrink,
height: iced::Length::Shrink,
mouse_hidden: false,
on_duration_changed: None,
on_end_of_stream: None,
on_new_frame: None,
on_subtitle_text: None,
on_error: None,
on_missing_plugin: None,
on_tags: None,
on_warning: None,
_phantom: Default::default(),
id: None,
}
}
/// Sets the ID of the `VideoPlayer`.
pub fn id(mut self, id: cosmic::widget::Id) -> Self {
self.id = Some(id);
self
}
/// 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
}
}
pub fn mouse_hidden(self, mouse_hidden: bool) -> Self {
VideoPlayer {
mouse_hidden,
..self
}
}
pub fn on_duration_changed<F>(self, on_duration_changed: F) -> Self
where
F: 'a + Fn(Duration) -> Message,
{
VideoPlayer {
on_duration_changed: Some(Box::new(on_duration_changed)),
..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
}
}
pub fn on_missing_plugin<F>(self, on_missing_plugin: F) -> Self
where
F: 'a + Fn(gst::Message) -> Message,
{
VideoPlayer {
on_missing_plugin: Some(Box::new(on_missing_plugin)),
..self
}
}
pub fn on_tags<F>(self, on_tags: F) -> Self
where
F: 'a + Fn(gst::TagList) -> Message,
{
VideoPlayer {
on_tags: Some(Box::new(on_tags)),
..self
}
}
pub fn on_warning<F>(self, on_warning: F) -> Self
where
F: 'a + Fn(glib::Error) -> Message,
{
VideoPlayer {
on_warning: Some(Box::new(on_warning)),
..self
}
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for VideoPlayer<'a, 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 = iced::Size::new(image_size.width * scale.x, image_size.height * scale.y);
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);
inner.redrawing.store(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);
}
#[cfg(feature = "wgpu")]
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,
),
);
#[cfg(not(feature = "wgpu"))]
{
if upload_frame {
let mut opt = None;
{
let yuv_data_opt = match inner.frame.lock() {
Ok(frame) => Some(frame),
Err(_err) => None,
};
if let Some(yuv_data) = yuv_data_opt.as_ref().and_then(|d| d.readable()) {
//TODO: convert on worker thread?
let rgba_data =
yuv_to_rgba(&yuv_data, inner.width as _, inner.height as _, 1);
opt = Some(advanced::image::Handle::from_rgba(
inner.width as _,
inner.height as _,
rgba_data,
))
};
}
inner.handle_opt = opt;
}
if let Some(handle) = &inner.handle_opt {
use cosmic::iced::Radians;
renderer.draw_image(
handle.clone(),
advanced::image::FilterMethod::Nearest,
drawing_bounds,
Radians(0.),
1.0,
[0.0; 4],
);
}
}
}
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::DurationChanged,
gst::MessageType::Error,
gst::MessageType::Element,
gst::MessageType::Eos,
gst::MessageType::Tag,
gst::MessageType::Warning,
]) {
match msg.view() {
gst::MessageView::DurationChanged(_) => {
inner.duration = Duration::from_nanos(
inner
.source
.query_duration::<gst::ClockTime>()
.map(|duration| duration.nseconds())
.unwrap_or(0),
);
if let Some(ref on_duration_changed) = self.on_duration_changed {
shell.publish(on_duration_changed(inner.duration));
}
}
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::Element(element) => {
if gst_pbutils::MissingPluginMessage::is(&element) {
if let Some(ref on_missing_plugin) = self.on_missing_plugin {
shell.publish(on_missing_plugin(element.copy()));
}
}
}
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;
}
}
gst::MessageView::Tag(tag_msg) => {
if let Some(ref on_tags) = self.on_tags {
shell.publish(on_tags(tag_msg.tags()));
}
}
gst::MessageView::Warning(warn) => {
log::warn!("bus returned a warning: {warn}");
if let Some(ref on_warning) = self.on_warning {
shell.publish(on_warning(warn.error()));
}
}
_ => {}
}
}
// 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.redrawing.load(Ordering::SeqCst)
&& inner.upload_frame.load(Ordering::SeqCst)
{
if let Some(on_new_frame) = self.on_new_frame.clone() {
inner.redrawing.store(true, Ordering::SeqCst);
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_at(iced::window::RedrawRequest::At(
Instant::now() + Duration::from_millis(32),
));
}
}
}
fn mouse_interaction(
&self,
_tree: &widget::Tree,
_layout: advanced::Layout<'_>,
_cursor_position: mouse::Cursor,
_viewport: &iced::Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if self.mouse_hidden {
mouse::Interaction::Hidden
} else {
mouse::Interaction::default()
}
}
fn set_id(&mut self, id: widget::Id) {
self.id = Some(id);
}
fn id(&self) -> Option<widget::Id> {
self.id.clone()
}
}
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)
}
}