feat: Added stuff
Some checks failed
build / checks-matrix (push) Has been cancelled
build / checks-build (push) Has been cancelled
build / codecov (push) Has been cancelled
docs / docs (push) Has been cancelled

This commit is contained in:
uttarayan21
2025-11-22 04:39:42 +05:30
parent b1cfc19b96
commit 61a2ea1733
8 changed files with 398 additions and 117 deletions

42
Cargo.lock generated
View File

@@ -655,6 +655,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.71.1" version = "0.71.1"
@@ -3271,6 +3280,7 @@ source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276
dependencies = [ dependencies = [
"iced_core", "iced_core",
"iced_debug", "iced_debug",
"iced_devtools",
"iced_futures", "iced_futures",
"iced_renderer", "iced_renderer",
"iced_runtime", "iced_runtime",
@@ -3280,6 +3290,21 @@ dependencies = [
"thiserror 2.0.17", "thiserror 2.0.17",
] ]
[[package]]
name = "iced_beacon"
version = "0.14.0-dev"
source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498"
dependencies = [
"bincode",
"futures",
"iced_core",
"log",
"semver",
"serde",
"thiserror 2.0.17",
"tokio",
]
[[package]] [[package]]
name = "iced_core" name = "iced_core"
version = "0.14.0-dev" version = "0.14.0-dev"
@@ -3292,6 +3317,7 @@ dependencies = [
"log", "log",
"num-traits", "num-traits",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"serde",
"smol_str", "smol_str",
"thiserror 2.0.17", "thiserror 2.0.17",
"web-time", "web-time",
@@ -3302,11 +3328,23 @@ name = "iced_debug"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498" source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498"
dependencies = [ dependencies = [
"iced_beacon",
"iced_core", "iced_core",
"iced_futures", "iced_futures",
"log", "log",
] ]
[[package]]
name = "iced_devtools"
version = "0.14.0-dev"
source = "git+https://github.com/iced-rs/iced#645643bfd63ed4c01aa281f97992e3c276e71498"
dependencies = [
"iced_debug",
"iced_program",
"iced_widget",
"log",
]
[[package]] [[package]]
name = "iced_futures" name = "iced_futures"
version = "0.14.0-dev" version = "0.14.0-dev"
@@ -6294,6 +6332,10 @@ name = "semver"
version = "1.0.27" version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
dependencies = [
"serde",
"serde_core",
]
[[package]] [[package]]
name = "serde" name = "serde"

View File

@@ -14,6 +14,7 @@ iced = { git = "https://github.com/iced-rs/iced", features = [
"image", "image",
"sipper", "sipper",
"tokio", "tokio",
"debug",
] } ] }
iced_wgpu = { git = "https://github.com/iced-rs/iced" } iced_wgpu = { git = "https://github.com/iced-rs/iced" }
iced_video_player = { path = "crates/iced_video_player" } iced_video_player = { path = "crates/iced_video_player" }

View File

@@ -11,12 +11,14 @@ authors = ["jazzfool"]
edition = "2021" edition = "2021"
resolver = "2" resolver = "2"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
exclude = [ exclude = [".media/test.mp4"]
".media/test.mp4"
]
[dependencies] [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" } iced_wgpu = { git = "https://github.com/iced-rs/iced" }
gstreamer = "0.23" gstreamer = "0.23"
gstreamer-app = "0.23" # appsink gstreamer-app = "0.23" # appsink
@@ -37,9 +39,23 @@ runtimeLibs = [
"libxkbcommon", "libxkbcommon",
"xorg.libX11", "xorg.libX11",
"xorg.libXrandr", "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", "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] [package.metadata.docs.rs]
rustc-args = ["--cfg", "docsrs"] rustc-args = ["--cfg", "docsrs"]

View File

@@ -6,7 +6,7 @@ use iced_video_player::{Video, VideoPlayer};
use std::time::Duration; use std::time::Duration;
fn main() -> iced::Result { fn main() -> iced::Result {
iced::run("Iced Video Player", App::update, App::view) iced::run(App::update, App::view)
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -29,17 +29,10 @@ impl Default for App {
fn default() -> Self { fn default() -> Self {
App { App {
video: Video::new( video: Video::new(
&url::Url::from_file_path( &url::Url::parse("https://jellyfin.tsuba.darksailor.dev/Videos/1d7e2012-e17d-edbb-25c3-2dbcc803d6b6/stream?static=true")
std::path::PathBuf::from(file!()) .expect("Failed to parse URL"),
.parent()
.unwrap()
.join("../.media/test.mp4")
.canonicalize()
.unwrap(),
) )
.unwrap(), .expect("Failed to create video"),
)
.unwrap(),
position: 0.0, position: 0.0,
dragging: false, dragging: false,
} }

View File

@@ -273,6 +273,8 @@ impl Video {
let pad = video_sink.pads().first().cloned().unwrap(); let pad = video_sink.pads().first().cloned().unwrap();
dbg!(&pad);
dbg!(&pipeline);
cleanup!(pipeline.set_state(gst::State::Playing))?; cleanup!(pipeline.set_state(gst::State::Playing))?;
// wait for up to 5 seconds until the decoder gets the source capabilities // wait for up to 5 seconds until the decoder gets the source capabilities

View File

@@ -74,10 +74,28 @@
buildInputs = with pkgs; 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 openssl
gst_all_1.gstreamer.dev vulkan-loader
gst_all_1.gst-plugins-base.dev wayland
wayland-protocols
glib
] ]
++ (lib.optionals pkgs.stdenv.isLinux [ ++ (lib.optionals pkgs.stdenv.isLinux [
alsa-lib-with-plugins alsa-lib-with-plugins
@@ -146,11 +164,10 @@
devShells = { devShells = {
default = default =
pkgs.mkShell.override { pkgs.mkShell.override {
stdenv = pkgs.clangStdenv; stdenv =
# stdenv = if pkgs.stdenv.isLinux
# if pkgs.stdenv.isLinux then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv)
# then (pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv) else pkgs.clangStdenv;
# else pkgs.clangStdenv;
} (commonArgs } (commonArgs
// { // {
packages = with pkgs; packages = with pkgs;
@@ -164,6 +181,7 @@
cargo-hack cargo-hack
cargo-outdated cargo-outdated
lld lld
lldb
] ]
++ (lib.optionals pkgs.stdenv.isDarwin [ ++ (lib.optionals pkgs.stdenv.isDarwin [
apple-sdk_13 apple-sdk_13

View File

@@ -6,10 +6,10 @@ use std::sync::Arc;
mod blur_hash; mod blur_hash;
use blur_hash::BlurHash; use blur_hash::BlurHash;
// mod preview; mod preview;
// use preview::Preview; use preview::Preview;
use iced::{Alignment, Element, Length, Task, widget::*}; use iced::{Alignment, Element, Length, Shadow, Task, widget::*};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -38,6 +38,7 @@ impl ItemCache {
self.insert(parent, item); self.insert(parent, item);
}); });
} }
pub fn items_of(&self, parent: impl Into<Option<uuid::Uuid>>) -> Vec<&Item> { pub fn items_of(&self, parent: impl Into<Option<uuid::Uuid>>) -> Vec<&Item> {
let parent = parent.into(); let parent = parent.into();
self.tree.get(&None); self.tree.get(&None);
@@ -50,6 +51,10 @@ impl ItemCache {
}) })
.unwrap_or_default() .unwrap_or_default()
} }
pub fn get(&self, id: &uuid::Uuid) -> Option<&Item> {
self.items.get(id)
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -75,6 +80,7 @@ impl From<api::jellyfin::BaseItemDto> for Item {
.and_then(|hashes| hashes.get(&tag).cloned()) .and_then(|hashes| hashes.get(&tag).cloned())
.map(|s| s.clone().into()), .map(|s| s.clone().into()),
}), }),
_type: dto._type,
} }
} }
} }
@@ -85,16 +91,16 @@ pub struct Item {
pub parent_id: Option<uuid::Uuid>, pub parent_id: Option<uuid::Uuid>,
pub name: Option<SharedString>, pub name: Option<SharedString>,
pub thumbnail: Option<Thumbnail>, pub thumbnail: Option<Thumbnail>,
pub _type: api::jellyfin::BaseItemKind,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub enum Screen { pub enum Screen {
#[default] #[default]
Home, Home,
Item(Option<uuid::Uuid>),
Search(String),
Settings, Settings,
User, User,
Video,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct State { struct State {
@@ -146,13 +152,24 @@ pub enum Message {
SetToken(String), SetToken(String),
Back, Back,
Home, Home,
OpenVideo(url::Url),
// Login-related messages // Login-related messages
UsernameChanged(String), UsernameChanged(String),
PasswordChanged(String), PasswordChanged(String),
Login, Login,
LoginSuccess(String), LoginSuccess(String),
Logout, Logout,
Video(VideoMessage),
}
#[derive(Debug, Clone)]
pub enum VideoMessage {
EndOfStream,
Open(url::Url),
Pause,
Play,
Seek(f64),
Stop,
Test,
} }
fn update(state: &mut State, message: Message) -> Task<Message> { fn update(state: &mut State, message: Message) -> Task<Message> {
@@ -227,6 +244,15 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
} }
Message::OpenItem(id) => { Message::OpenItem(id) => {
let client = state.jellyfin_client.clone(); let client = state.jellyfin_client.clone();
use api::jellyfin::BaseItemKind::*;
if let Some(cached) = id.as_ref().and_then(|id| state.cache.get(id))
&& matches!(cached._type, Video | Movie | Episode)
{
let url = client
.stream_url(id.expect("ID exists"))
.expect("Failed to get stream URL");
Task::done(Message::Video(VideoMessage::Open(url)))
} else {
Task::perform( Task::perform(
async move { async move {
let items: Result<Vec<Item>, api::JellyfinApiError> = client let items: Result<Vec<Item>, api::JellyfinApiError> = client
@@ -241,6 +267,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
}, },
) )
} }
}
Message::LoadedItem(id, items) => { Message::LoadedItem(id, items) => {
state.cache.extend(id, items); state.cache.extend(id, items);
state.history.push(state.current); state.history.push(state.current);
@@ -301,10 +328,57 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
} }
}) })
} }
Message::OpenVideo(url) => { Message::Video(msg) => match msg {
state.video = Video::new(&url).ok().map(Arc::new); VideoMessage::EndOfStream => {
state.video = None;
Task::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,18 +390,32 @@ fn view(state: &State) -> Element<'_, Message> {
} }
fn home(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 body(state: &State) -> Element<'_, Message> { fn player(video: &Video) -> Element<'_, Message> {
if let Some(video) = &state.video { container(
return container(VideoPlayer::new(video)) 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) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.align_x(Alignment::Center) .align_x(Alignment::Center)
.align_y(Alignment::Center) .align_y(Alignment::Center)
.into(); .into()
} }
fn body(state: &State) -> Element<'_, Message> {
if let Some(ref video) = state.video {
player(video)
} else {
scrollable( scrollable(
container( container(
Grid::with_children(state.cache.items_of(state.current).into_iter().map(card)) Grid::with_children(state.cache.items_of(state.current).into_iter().map(card))
@@ -343,6 +431,7 @@ fn body(state: &State) -> Element<'_, Message> {
.height(Length::Fill) .height(Length::Fill)
.into() .into()
} }
}
fn header(state: &State) -> Element<'_, Message> { fn header(state: &State) -> Element<'_, Message> {
row([ row([
@@ -365,6 +454,9 @@ fn header(state: &State) -> Element<'_, Message> {
row([ row([
button("Refresh").on_press(Message::Refresh).into(), button("Refresh").on_press(Message::Refresh).into(),
button("Settings").on_press(Message::OpenSettings).into(), button("Settings").on_press(Message::OpenSettings).into(),
button("TestVideo")
.on_press(Message::Video(VideoMessage::Test))
.into(),
]) ])
.spacing(10), .spacing(10),
) )
@@ -539,7 +631,7 @@ fn card(item: &Item) -> Element<'_, Message> {
.as_ref() .as_ref()
.map(|s| s.as_ref()) .map(|s| s.as_ref())
.unwrap_or("Unnamed Item"); .unwrap_or("Unnamed Item");
Button::new( MouseArea::new(
container( container(
column([ column([
BlurHash::new( BlurHash::new(
@@ -562,8 +654,6 @@ fn card(item: &Item) -> Element<'_, Message> {
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill), .height(Length::Fill),
) )
// .width(Length::FillPortion(5))
// .height(Length::FillPortion(3))
.style(container::rounded_box), .style(container::rounded_box),
) )
.on_press(Message::OpenItem(Some(item.id))) .on_press(Message::OpenItem(Some(item.id)))

View File

@@ -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 reqwest::Method;
use std::sync::Arc; use std::sync::Arc;
@@ -11,6 +11,15 @@ pub struct ImageDownloader {
Option<Arc<dyn Fn(reqwest::RequestBuilder) -> reqwest::RequestBuilder + Send + Sync>>, Option<Arc<dyn Fn(reqwest::RequestBuilder) -> reqwest::RequestBuilder + Send + Sync>>,
} }
impl core::fmt::Debug for ImageDownloader {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ImageDownloader")
.field("client", &self.client)
.field("request_modifier", &self.request_modifier.is_some())
.finish()
}
}
impl ImageDownloader { impl ImageDownloader {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -57,18 +66,24 @@ pub enum Preview {
}, },
} }
// impl Preview { #[derive(Debug, Clone)]
// pub fn thumbnail(image: Image, blur_hash: BlurHash) -> Self { pub struct ImageCache {
// Preview::Thumbnail { cache: std::collections::HashMap<uuid::Uuid, Preview>,
// thumbnail: image, downloader: ImageDownloader,
// blur_hash, }
// }
// } impl Preview {
// pub fn thumbnail(image: Image, blur_hash: BlurHash) -> Self {
// pub fn blur_hash(blur_hash: BlurHash) -> Self { Preview::Thumbnail {
// Preview::BlurHash { blur_hash } thumbnail: image,
// } blur_hash,
// }
}
pub fn blur_hash(blur_hash: BlurHash) -> Self {
Preview::BlurHash { blur_hash }
}
// pub fn upgrade( // pub fn upgrade(
// self, // self,
// fut: impl core::future::Future<Output = bytes::Bytes> + 'static + Send, // fut: impl core::future::Future<Output = bytes::Bytes> + 'static + Send,
@@ -88,14 +103,21 @@ pub enum Preview {
// // iced::Task::sip(sip, ||) // // iced::Task::sip(sip, ||)
// Task:: // Task::
// } // }
// } }
//
// enum PreviewMessage { enum PreviewMessage {
// BlurHashLoaded(BlurHash), BlurHashLoaded(uuid::Uuid, BlurHash),
// ThumbnailLoaded(Image),
// ThumbnailAllocated(image::Allocation), ThumbnailDownloaded(uuid::Uuid, bytes::Bytes),
// } // ThumbnailAllocated(
// // uuid::Uuid,
// Result<image::Allocation, iced::advanced::image::Error>,
// ),
ThumbnailLoaded(uuid::Uuid, Image),
Failed(uuid::Uuid, String),
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Image { pub struct Image {
bytes: bytes::Bytes, bytes: bytes::Bytes,
@@ -103,3 +125,100 @@ pub struct Image {
allocation: image::Allocation, allocation: image::Allocation,
fade_in: Animation<bool>, fade_in: Animation<bool>,
} }
#[derive(Clone, Debug)]
pub struct PreviewSource {
pub id: uuid::Uuid,
pub url: String,
pub width: iced::Length,
pub height: iced::Length,
pub blur_hash: Option<String>,
}
impl PreviewSource {
pub fn new(id: uuid::Uuid, url: String) -> Self {
Self {
id,
url,
width: iced::Length::Fill,
height: iced::Length::Fill,
blur_hash: None,
}
}
pub fn blur_hash(mut self, blur_hash: String) -> Self {
self.blur_hash = Some(blur_hash);
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
pub fn width(mut self, width: iced::Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: iced::Length) -> Self {
self.height = height;
self
}
// pub fn upgrade(self) -> Task<PreviewMessage> {
// let sip = iced::task::sipper(async move |mut sender| {
// let bytes = fut.await;
// let handle = Handle::from_bytes(bytes.clone());
// let allocation = image::allocate(handle);
// let image = Image {
// bytes,
// handle,
// allocation,
// fade_in: Animation::new(false),
// };
// let _ = sender.send(image).await;
// });
// // iced::Task::sip(sip, ||)
// }
}
impl ImageCache {
pub fn upgrade(&self, source: PreviewSource) -> iced::Task<PreviewMessage> {
let downloader = self.downloader.clone();
let sipper = iced::task::sipper(async move |mut sender| {
if let Some(blur_hash_str) = source.blur_hash {
let blur_hash = BlurHash::new(&blur_hash_str);
let _ = sender.send(blur_hash).await;
}
let bytes = downloader.download(&source.url).await?;
reqwest::Result::<bytes::Bytes>::Ok(bytes)
});
iced::Task::sip(
sipper,
move |progress| PreviewMessage::BlurHashLoaded(source.id, progress),
move |output: reqwest::Result<bytes::Bytes>| match output {
Ok(bytes) => PreviewMessage::ThumbnailDownloaded(source.id, bytes),
Err(e) => PreviewMessage::Failed(source.id, e.to_string()),
},
)
.then(|message| match message {
PreviewMessage::ThumbnailDownloaded(id, bytes) => {
let handle = Handle::from_bytes(bytes.clone());
let allocation = image::allocate(&handle);
allocation.map(move |output| match output {
Ok(allocation) => {
let image = Image {
bytes: bytes.clone(),
handle: handle.clone(),
allocation,
fade_in: Animation::new(false),
};
PreviewMessage::ThumbnailLoaded(id, image)
}
Err(e) => PreviewMessage::Failed(id, e.to_string()),
})
}
other => iced::Task::done(other),
})
}
}