From 61a2ea173395322136109c278e715d7595a635b4 Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Sat, 22 Nov 2025 04:39:42 +0530 Subject: [PATCH] feat: Added stuff --- Cargo.lock | 42 ++++ Cargo.toml | 1 + crates/iced_video_player/Cargo.toml | 40 ++-- crates/iced_video_player/examples/minimal.rs | 15 +- crates/iced_video_player/src/video.rs | 2 + flake.nix | 34 +++- ui-iced/src/lib.rs | 182 ++++++++++++----- ui-iced/src/preview.rs | 199 +++++++++++++++---- 8 files changed, 398 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b0e0b4..2f5f000 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -655,6 +655,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.71.1" @@ -3271,6 +3280,7 @@ source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276 dependencies = [ "iced_core", "iced_debug", + "iced_devtools", "iced_futures", "iced_renderer", "iced_runtime", @@ -3280,6 +3290,21 @@ dependencies = [ "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]] name = "iced_core" version = "0.14.0-dev" @@ -3292,6 +3317,7 @@ dependencies = [ "log", "num-traits", "rustc-hash 2.1.1", + "serde", "smol_str", "thiserror 2.0.17", "web-time", @@ -3302,11 +3328,23 @@ name = "iced_debug" version = "0.14.0-dev" source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498" dependencies = [ + "iced_beacon", "iced_core", "iced_futures", "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]] name = "iced_futures" version = "0.14.0-dev" @@ -6294,6 +6332,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" diff --git a/Cargo.toml b/Cargo.toml index 6785cf2..a1f4020 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ iced = { git = "https://github.com/iced-rs/iced", features = [ "image", "sipper", "tokio", + "debug", ] } iced_wgpu = { git = "https://github.com/iced-rs/iced" } iced_video_player = { path = "crates/iced_video_player" } diff --git a/crates/iced_video_player/Cargo.toml b/crates/iced_video_player/Cargo.toml index 9b704d5..e32a3a3 100644 --- a/crates/iced_video_player/Cargo.toml +++ b/crates/iced_video_player/Cargo.toml @@ -11,12 +11,14 @@ authors = ["jazzfool"] edition = "2021" resolver = "2" license = "MIT OR Apache-2.0" -exclude = [ - ".media/test.mp4" -] +exclude = [".media/test.mp4"] [dependencies] -iced = { git = "https://github.com/iced-rs/iced", features = ["image", "advanced", "wgpu"] } +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 @@ -31,15 +33,29 @@ 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", + "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", ] -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"] diff --git a/crates/iced_video_player/examples/minimal.rs b/crates/iced_video_player/examples/minimal.rs index dd49280..3b93195 100644 --- a/crates/iced_video_player/examples/minimal.rs +++ b/crates/iced_video_player/examples/minimal.rs @@ -6,7 +6,7 @@ use iced_video_player::{Video, VideoPlayer}; use std::time::Duration; fn main() -> iced::Result { - iced::run("Iced Video Player", App::update, App::view) + iced::run(App::update, App::view) } #[derive(Clone, Debug)] @@ -29,17 +29,10 @@ 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(), + &url::Url::parse("https://jellyfin.tsuba.darksailor.dev/Videos/1d7e2012-e17d-edbb-25c3-2dbcc803d6b6/stream?static=true") + .expect("Failed to parse URL"), ) - .unwrap(), + .expect("Failed to create video"), position: 0.0, dragging: false, } diff --git a/crates/iced_video_player/src/video.rs b/crates/iced_video_player/src/video.rs index bdeb5ce..f382e63 100644 --- a/crates/iced_video_player/src/video.rs +++ b/crates/iced_video_player/src/video.rs @@ -273,6 +273,8 @@ impl Video { 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 diff --git a/flake.nix b/flake.nix index 85b01d0..ed400ef 100644 --- a/flake.nix +++ b/flake.nix @@ -74,10 +74,28 @@ buildInputs = with pkgs; [ - vulkan-loader + gst_all_1.gst-libav + gst_all_1.gst-plugins-bad + gst_all_1.gst-editing-services + gst_all_1.gst-plugins-rs + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-ugly + gst_all_1.gstreamer + gst_all_1.gstreamermm + gst_all_1.gst-rtsp-server + gst_all_1.gst-vaapi + + xorg.libX11 + xorg.libXrandr + xorg.libXi + + libxkbcommon openssl - gst_all_1.gstreamer.dev - gst_all_1.gst-plugins-base.dev + vulkan-loader + wayland + wayland-protocols + glib ] ++ (lib.optionals pkgs.stdenv.isLinux [ alsa-lib-with-plugins @@ -146,11 +164,10 @@ devShells = { default = pkgs.mkShell.override { - stdenv = pkgs.clangStdenv; - # stdenv = - # if pkgs.stdenv.isLinux - # then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv) - # else pkgs.clangStdenv; + stdenv = + if pkgs.stdenv.isLinux + then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv) + else pkgs.clangStdenv; } (commonArgs // { packages = with pkgs; @@ -164,6 +181,7 @@ cargo-hack cargo-outdated lld + lldb ] ++ (lib.optionals pkgs.stdenv.isDarwin [ apple-sdk_13 diff --git a/ui-iced/src/lib.rs b/ui-iced/src/lib.rs index ef6bb41..3072948 100644 --- a/ui-iced/src/lib.rs +++ b/ui-iced/src/lib.rs @@ -6,10 +6,10 @@ use std::sync::Arc; mod blur_hash; use blur_hash::BlurHash; -// mod preview; -// use preview::Preview; +mod preview; +use preview::Preview; -use iced::{Alignment, Element, Length, Task, widget::*}; +use iced::{Alignment, Element, Length, Shadow, Task, widget::*}; use std::collections::{BTreeMap, BTreeSet}; #[derive(Debug, Clone)] @@ -38,6 +38,7 @@ impl ItemCache { self.insert(parent, item); }); } + pub fn items_of(&self, parent: impl Into>) -> Vec<&Item> { let parent = parent.into(); self.tree.get(&None); @@ -50,6 +51,10 @@ impl ItemCache { }) .unwrap_or_default() } + + pub fn get(&self, id: &uuid::Uuid) -> Option<&Item> { + self.items.get(id) + } } #[derive(Clone, Debug)] @@ -75,6 +80,7 @@ impl From for Item { .and_then(|hashes| hashes.get(&tag).cloned()) .map(|s| s.clone().into()), }), + _type: dto._type, } } } @@ -85,16 +91,16 @@ pub struct Item { pub parent_id: Option, pub name: Option, pub thumbnail: Option, + pub _type: api::jellyfin::BaseItemKind, } #[derive(Debug, Clone, Default)] pub enum Screen { #[default] Home, - Item(Option), - Search(String), Settings, User, + Video, } #[derive(Debug, Clone)] struct State { @@ -146,13 +152,24 @@ pub enum Message { SetToken(String), Back, Home, - OpenVideo(url::Url), // 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 { @@ -227,19 +244,29 @@ fn update(state: &mut State, message: Message) -> Task { } Message::OpenItem(id) => { let client = state.jellyfin_client.clone(); - Task::perform( - async move { - let items: Result, api::JellyfinApiError> = client - .items(id) - .await - .map(|items| items.into_iter().map(Item::from).collect()); - (id, items) - }, - |(msg, items)| match items { - Err(e) => Message::Error(format!("Failed to load item: {}", e)), - Ok(items) => Message::LoadedItem(msg, items), - }, - ) + 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( + async move { + let items: Result, api::JellyfinApiError> = client + .items(id) + .await + .map(|items| items.into_iter().map(Item::from).collect()); + (id, items) + }, + |(msg, items)| match items { + Err(e) => Message::Error(format!("Failed to load item: {}", e)), + Ok(items) => Message::LoadedItem(msg, items), + }, + ) + } } Message::LoadedItem(id, items) => { state.cache.extend(id, items); @@ -301,10 +328,57 @@ fn update(state: &mut State, message: Message) -> Task { } }) } - Message::OpenVideo(url) => { - state.video = Video::new(&url).ok().map(Arc::new); - Task::none() - } + 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() + } + }, } } @@ -316,32 +390,47 @@ fn view(state: &State) -> Element<'_, Message> { } fn home(state: &State) -> Element<'_, Message> { - column([header(state), body(state), footer(state)]).into() + 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> { - if let Some(video) = &state.video { - return container(VideoPlayer::new(video)) - .width(Length::Fill) - .height(Length::Fill) + if let Some(ref video) = state.video { + player(video) + } else { + scrollable( + container( + Grid::with_children(state.cache.items_of(state.current).into_iter().map(card)) + .fluid(400) + .spacing(50), + ) + .padding(50) .align_x(Alignment::Center) - .align_y(Alignment::Center) - .into(); - } - scrollable( - container( - Grid::with_children(state.cache.items_of(state.current).into_iter().map(card)) - .fluid(400) - .spacing(50), + // .align_y(Alignment::Center) + .height(Length::Fill) + .width(Length::Fill), ) - .padding(50) - .align_x(Alignment::Center) - // .align_y(Alignment::Center) .height(Length::Fill) - .width(Length::Fill), - ) - .height(Length::Fill) - .into() + .into() + } } fn header(state: &State) -> Element<'_, Message> { @@ -365,6 +454,9 @@ fn header(state: &State) -> Element<'_, Message> { row([ 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), ) @@ -539,7 +631,7 @@ fn card(item: &Item) -> Element<'_, Message> { .as_ref() .map(|s| s.as_ref()) .unwrap_or("Unnamed Item"); - Button::new( + MouseArea::new( container( column([ BlurHash::new( @@ -562,8 +654,6 @@ fn card(item: &Item) -> Element<'_, Message> { .width(Length::Fill) .height(Length::Fill), ) - // .width(Length::FillPortion(5)) - // .height(Length::FillPortion(3)) .style(container::rounded_box), ) .on_press(Message::OpenItem(Some(item.id))) diff --git a/ui-iced/src/preview.rs b/ui-iced/src/preview.rs index 71a056a..a01097d 100644 --- a/ui-iced/src/preview.rs +++ b/ui-iced/src/preview.rs @@ -1,4 +1,4 @@ -use iced::{Animation, advanced::image::Handle, widget::image}; +use iced::{Animation, Function, advanced::image::Handle, widget::image}; use reqwest::Method; use std::sync::Arc; @@ -11,6 +11,15 @@ pub struct ImageDownloader { Option 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 { @@ -57,45 +66,58 @@ pub enum Preview { }, } -// 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 + 'static + Send, -// ) -> iced::Task { -// // 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(BlurHash), -// ThumbnailLoaded(Image), -// ThumbnailAllocated(image::Allocation), -// } -// +#[derive(Debug, Clone)] +pub struct ImageCache { + cache: std::collections::HashMap, + 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 + 'static + Send, + // ) -> iced::Task { + // // 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, + // ), + ThumbnailLoaded(uuid::Uuid, Image), + + Failed(uuid::Uuid, String), +} + #[derive(Clone, Debug)] pub struct Image { bytes: bytes::Bytes, @@ -103,3 +125,100 @@ pub struct Image { allocation: image::Allocation, fade_in: Animation, } + +#[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, +} + +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) -> 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 { + // 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 { + 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::::Ok(bytes) + }); + iced::Task::sip( + sipper, + move |progress| PreviewMessage::BlurHashLoaded(source.id, progress), + move |output: reqwest::Result| 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), + }) + } +}