From 5d0b795ba52d3254b9291f67f30afd613f6d1099 Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Thu, 25 Dec 2025 02:15:43 +0530 Subject: [PATCH] feat: Added readme and forgotten id.rs --- README.md | 65 ++++++++++++++++++++++ crates/iced-video/examples/minimal.rs | 77 +++++++++++++++++++++++++++ crates/iced-video/src/id.rs | 55 +++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 README.md create mode 100644 crates/iced-video/examples/minimal.rs create mode 100644 crates/iced-video/src/id.rs diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb8f736 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Jello + +A WIP video client for jellyfin. + +(Planned) Features + +1. Integrate with jellyfin +2. HDR video playback +3. Audio Track selection +4. Chapter selection + +Libraries and frameworks used for this +1. iced -> primary gui toolkit +2. gstreamer -> primary video + audio decoding library +3. wgpu -> rendering the video from gstreamer in iced + + +### HDR +I'll try to document all my findings about HDR here. +I'm making this project to mainly learn about videos, color-spaces and gpu programming. And so very obviously I'm bound to make mistakes in either the code or the fundamental understanding of a concept. Please don't take anything in this text as absolute. + +```rust +let window = ... // use winnit to get a window handle, check the example in this repo +let instance = wgpu::Instance::default(); +let surface = instance.create_surface(window).unwrap(); +let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .context("Failed to request wgpu adapter")?; +let caps = surface.get_capabilities(); +println!("{:#?}", caps.formats); +``` + +This should print out all the texture formats that can be used by your current hardware +Among these the formats that support hdr (afaik) are +``` +wgpu::TextureFormat::Rgba16Float +wgpu::TextureFormat::Rgba32Float +wgpu::TextureFormat::Rgb10a2Unorm +wgpu::TextureFormat::Rgb10a2Uint // (unsure) +``` + +My display supports Rgb10a2Unorm so I'll be going forward with that texture format. + +`Rgb10a2Unorm` is still the same size as a `Rgba8Unorm` but data is in a different representation in each of them + +`Rgb10a2Unorm`: +R, G, B => 10 bits each (2^10 = 1024 [0..=1023]) +A => 2 bits (2^2 = 4 [0..=3]) + +Whereas in a normal pixel +`Rgba8Unorm` +R, G, B, A => 8 bits each (2^8 = 256 [0..=255]) + + +For displaying videos the alpha components is not really used (I don't know of any) so we can use re-allocate 6 bits from the alpha channel and put them in the r,g and b components. +In the shader the components get uniformly normalized from [0..=1023] integer to [0..=1] in float so we can compute them properly + +Videos however are generally not stored in this format or any rgb format in general because it is not as efficient for (lossy) compression as YUV formats. + +Right now I don't want to deal with yuv formats so I'll use gstreamer caps to convert the video into `Rgba10a2` format diff --git a/crates/iced-video/examples/minimal.rs b/crates/iced-video/examples/minimal.rs new file mode 100644 index 0000000..fab2a45 --- /dev/null +++ b/crates/iced-video/examples/minimal.rs @@ -0,0 +1,77 @@ +use iced_video::{Video, VideoHandle}; + +pub fn main() -> iced::Result { + use tracing_subscriber::prelude::*; + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_thread_ids(true) + .with_file(true), + ) + .with(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + iced::application(State::new, update, view).run() +} + +#[derive(Debug, Clone)] +pub struct State { + video: VideoHandle, +} + +impl State { + pub fn new() -> Self { + let video = VideoHandle::new("https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c") + .expect("Failed to create video handle"); + Self { video } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + Play, + Pause, + Loaded, + Load, +} + +pub fn update(state: &mut State, message: Message) -> iced::Task { + match message { + Message::Load => { + // does stuff + let src = state.video.source().clone(); + iced::Task::perform(src.wait(), |_| Message::Loaded) + } + Message::Play => { + state.video.source().play().expect("Failed to play video"); + iced::Task::none() + } + Message::Pause => { + state.video.source().pause().expect("Failed to pause video"); + iced::Task::none() + } + Message::Loaded => { + // Video loaded + iced::Task::none() + } + } +} + +pub fn view<'a>(state: &'a State) -> iced::Element<'a, Message> { + let video_widget = Video::new(&state.video) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .content_fit(iced::ContentFit::Contain); + + iced::widget::Column::new() + .push(video_widget) + .push( + iced::widget::Row::new() + .push(iced::widget::Button::new("Play").on_press(Message::Play)) + .push(iced::widget::Button::new("Pause").on_press(Message::Pause)) + .spacing(5) + .padding(10) + .align_y(iced::Alignment::Center), + ) + .align_x(iced::Alignment::Center) + .into() +} diff --git a/crates/iced-video/src/id.rs b/crates/iced-video/src/id.rs new file mode 100644 index 0000000..ae6f054 --- /dev/null +++ b/crates/iced-video/src/id.rs @@ -0,0 +1,55 @@ +use std::borrow; +use std::sync::atomic::{self, AtomicUsize}; + +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Id(Internal); + +impl Id { + /// Creates a new [`Id`] from a static `str`. + pub const fn new(id: &'static str) -> Self { + Self(Internal::Custom(borrow::Cow::Borrowed(id))) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); + + Self(Internal::Unique(id)) + } +} + +impl From<&'static str> for Id { + fn from(value: &'static str) -> Self { + Self::new(value) + } +} + +impl From for Id { + fn from(value: String) -> Self { + Self(Internal::Custom(borrow::Cow::Owned(value))) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +enum Internal { + Unique(usize), + Custom(borrow::Cow<'static, str>), +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +}