feat: Many more improvements to video player now with a subscription

This commit is contained in:
uttarayan21
2025-12-26 19:06:40 +05:30
parent 99853167df
commit 584495453f
13 changed files with 800 additions and 801 deletions

824
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,14 +13,15 @@ members = [
] ]
[workspace.dependencies] [workspace.dependencies]
iced = { version = "0.14.0" } iced = { version = "0.14.0" }
iced_video_player = "0.6"
gst = { version = "0.1.0", path = "gst" } gst = { version = "0.1.0", path = "gst" }
iced_wgpu = { version = "0.14.0" } iced_wgpu = { version = "0.14.0" }
iced-video = { version = "0.1.0", path = "crates/iced-video" }
[patch.crates-io] [patch.crates-io]
iced_wgpu = { git = "https://github.com/uttarayan21/iced", branch = "0.14" } iced_wgpu = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
iced_core = { git = "https://github.com/uttarayan21/iced", branch = "0.14" } iced_core = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
iced_renderer = { git = "https://github.com/uttarayan21/iced", branch = "0.14" } iced_renderer = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
iced_futures = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
iced = { git = "https://github.com/uttarayan21/iced", branch = "0.14" } iced = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
[package] [package]

View File

@@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
error-stack = "0.6.0" error-stack = "0.6.0"
futures-lite = "2.6.1"
gst.workspace = true gst.workspace = true
iced_core = "0.14.0" iced_core = "0.14.0"
iced_futures = "0.14.0" iced_futures = "0.14.0"

View File

@@ -11,87 +11,151 @@ pub fn main() -> iced::Result {
.with(tracing_subscriber::EnvFilter::from_default_env()) .with(tracing_subscriber::EnvFilter::from_default_env())
.init(); .init();
iced::application(State::new, update, view) iced::application(State::new, update, view)
.subscription(keyboard_event) .subscription(|state| match &state.video {
Some(video) => video.subscription_with(state, keyboard_event),
None => keyboard_event(state),
})
.run() .run()
} }
fn keyboard_event(state: &State) -> iced::Subscription<Message> { fn keyboard_event(_state: &State) -> iced::Subscription<Message> {
use iced::keyboard::{Key, key::Named}; use iced::keyboard::{Key, key::Named};
iced::keyboard::listen().map(move |event| match event { iced::keyboard::listen().map(move |event| match event {
iced::keyboard::Event::KeyPressed { key, .. } => { iced::keyboard::Event::KeyPressed { key, .. } => {
let key = key.as_ref(); let key = key.as_ref();
match key { match key {
Key::Named(Named::Escape) | Key::Character("q") => Message::Quit, Key::Named(Named::Escape) | Key::Character("q") => Message::Quit,
Key::Character("f") => Message::Fullscreen,
Key::Named(Named::Space) => Message::Toggle, Key::Named(Named::Space) => Message::Toggle,
_ => Message::Load, _ => Message::Noop,
} }
// if key == &space {
// // Toggle play/pause
// let is_playing = state
// .video
// .source()
// .is_playing()
// .expect("Failed to get playing state");
// if is_playing {
// Message::Pause
// } else {
// Message::Play
// }
// }
} }
_ => Message::Load, _ => Message::Noop,
}) })
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct State { pub struct State {
video: VideoHandle, video: Option<VideoHandle<Message>>,
fullscreen: bool,
} }
impl State { impl State {
pub fn new() -> Self { pub fn new() -> (Self, iced::Task<Message>) {
let video = VideoHandle::new("https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c") (
.expect("Failed to create video handle"); Self {
Self { video } video: None,
fullscreen: false,
},
iced::Task::done(Message::Load),
)
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
Play, Play,
Pause, Pause,
Toggle, Toggle,
Noop,
Load, Load,
Fullscreen,
OnLoad(VideoHandle<Message>),
OnError(String),
NewFrame,
Eos,
Quit, Quit,
} }
pub fn update(state: &mut State, message: Message) -> iced::Task<Message> { pub fn update(state: &mut State, message: Message) -> iced::Task<Message> {
match message { match message {
Message::Load => { Message::NewFrame => {
// does stuff
iced::Task::none() iced::Task::none()
} }
Message::Eos => {
iced::Task::done(Message::Pause)
}
Message::Load => {
iced::Task::perform(
VideoHandle::load(
"https://jellyfin.tsuba.darksailor.dev/Items/6010382cf25273e624d305907010d773/Download?api_key=036c140222464878862231ef66a2bc9c",
),
|result| match result {
Ok(video) => Message::OnLoad(video),
Err(err) => Message::OnError(format!("Error loading video: {:?}", err)),
},
).chain(iced::Task::done(Message::Play))
}
Message::OnError(err) => {
eprintln!("Error: {}", err);
iced::Task::none()
}
Message::OnLoad(video) => {
state.video = Some(video.on_new_frame(Message::NewFrame).on_end_of_stream(Message::Eos));
iced::Task::none()
}
Message::Fullscreen => {
state.fullscreen = !state.fullscreen;
let fullscreen = state.fullscreen;
let mode = if fullscreen {
iced::window::Mode::Fullscreen
} else {
iced::window::Mode::Windowed
};
iced::window::oldest().and_then(move |id| iced::window::set_mode::<Message>(id, mode))
}
Message::Play => { Message::Play => {
state.video.source().play().expect("Failed to play video"); state
.video
.as_ref()
.unwrap()
.source()
.play()
.expect("Failed to play video");
iced::Task::none() iced::Task::none()
} }
Message::Pause => { Message::Pause => {
state.video.source().pause().expect("Failed to pause video"); state
.video
.as_ref()
.unwrap()
.source()
.pause()
.expect("Failed to pause video");
iced::Task::none() iced::Task::none()
} }
Message::Toggle => { Message::Toggle => {
state.video.source().toggle().expect("Failed to stop video"); state
.video
.as_ref()
.unwrap()
.source()
.toggle()
.expect("Failed to stop video");
iced::Task::none() iced::Task::none()
} }
Message::Quit => { Message::Quit => {
state.video.source().stop().expect("Failed to stop video"); state
.video
.as_ref()
.unwrap()
.source()
.stop()
.expect("Failed to stop video");
std::process::exit(0); std::process::exit(0);
} }
Message::Noop => iced::Task::none(),
} }
} }
pub fn view<'a>(state: &'a State) -> iced::Element<'a, Message> { pub fn view<'a>(state: &'a State) -> iced::Element<'a, Message> {
let video_widget = Video::new(&state.video) if let None = &state.video {
return iced::widget::Column::new()
.push(iced::widget::Text::new("Press any key to load video"))
.align_x(iced::Alignment::Center)
.into();
}
let video_widget = Video::new(&state.video.as_ref().unwrap())
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Fill) .height(iced::Length::Fill)
.content_fit(iced::ContentFit::Contain); .content_fit(iced::ContentFit::Contain);

View File

@@ -0,0 +1,8 @@
info:
RUST_LOG=info,wgpu_core=warn,wgpu_hal=warn cargo run --release --example minimal
# GST_DEBUG=5 RUST_LOG="" cargo run --release --example minimal
flame:
cargo flamegraph run --release --example minimal
heaptrack:
cargo build --release --example minimal
RUST_LOG="info,wgpu_hal=info" heaptrack $CARGO_TARGET_DIR/release/examples/minimal

View File

@@ -1,13 +1,10 @@
pub mod id; pub mod id;
pub mod primitive; pub mod primitive;
pub mod source; pub mod source;
use iced_core as iced; pub mod widget;
use iced_renderer::Renderer as RendererWithFallback; pub use widget::Video;
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
use error_stack::{Report, ResultExt}; use error_stack::{Report, ResultExt};
use iced::Length;
use std::marker::PhantomData;
use gst::plugins::app::AppSink; use gst::plugins::app::AppSink;
use gst::plugins::playback::Playbin3; use gst::plugins::playback::Playbin3;
@@ -23,16 +20,16 @@ use std::sync::{Arc, Mutex, atomic::AtomicBool};
/// This is the video handle that is used to control the video playback. /// This is the video handle that is used to control the video playback.
/// This should be keps in the application state. /// This should be keps in the application state.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct VideoHandle { pub struct VideoHandle<Message> {
id: id::Id, id: id::Id,
source: source::VideoSource, pub source: source::VideoSource,
is_metadata_loaded: Arc<AtomicBool>,
is_playing: Arc<AtomicBool>,
is_eos: Arc<AtomicBool>,
frame_ready: Arc<AtomicBool>, frame_ready: Arc<AtomicBool>,
on_new_frame: Option<Box<Message>>,
on_end_of_stream: Option<Box<Message>>,
on_about_to_finish: Option<Box<Message>>,
} }
impl VideoHandle { impl<Message: Send + Sync + Clone + 'static> VideoHandle<Message> {
pub fn id(&self) -> &id::Id { pub fn id(&self) -> &id::Id {
&self.id &self.id
} }
@@ -47,206 +44,75 @@ impl VideoHandle {
Ok(Self { Ok(Self {
id: id::Id::unique(), id: id::Id::unique(),
source: source, source: source,
is_metadata_loaded: Arc::new(AtomicBool::new(false)), on_new_frame: None,
is_playing: Arc::new(AtomicBool::new(false)), on_end_of_stream: None,
is_eos: Arc::new(AtomicBool::new(false)), on_about_to_finish: None,
frame_ready, frame_ready,
}) })
} }
pub async fn wait(self) -> Result<Self> {
self.source.wait().await?;
Ok(self)
} }
/// This is the Video widget that displays a video. pub fn subscription(&self) -> iced_futures::subscription::Subscription<Message> {
/// This should be used in the view function. let sub = widget::VideoSubscription {
pub struct Video<'a, Message, Theme = iced::Theme, Renderer = iced_wgpu::Renderer>
where
Renderer: PrimitiveRenderer,
{
id: id::Id,
handle: &'a VideoHandle,
content_fit: iced::ContentFit,
width: iced::Length,
height: iced::Length,
on_end_of_stream: Option<Message>,
on_new_frame: Option<Message>,
looping: bool,
// on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message + 'a>>,
// on_error: Option<Box<dyn Fn(&glib::Error) -> Message + 'a>>,
// theme: Theme,
__marker: PhantomData<(Renderer, Theme)>,
}
impl<'a, Message, Theme, Renderer> Video<'a, Message, Theme, Renderer>
where
Renderer: PrimitiveRenderer,
{
pub fn new(handle: &'a VideoHandle) -> Self {
Self {
id: handle.id.clone(),
handle: &handle,
content_fit: iced::ContentFit::Contain,
width: Length::Shrink,
height: Length::Shrink,
on_end_of_stream: None,
on_new_frame: None,
looping: false,
// theme: Theme::default(),
__marker: PhantomData,
}
}
}
impl<'a, Message, Theme, Renderer> Video<'a, Message, Theme, Renderer>
where
Renderer: PrimitiveRenderer,
{
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn content_fit(mut self, fit: iced::ContentFit) -> Self {
self.content_fit = fit;
self
}
pub fn on_end_of_stream(mut self, message: Message) -> Self {
self.on_end_of_stream = Some(message);
self
}
pub fn on_new_frame(mut self, message: Message) -> Self {
self.on_new_frame = Some(message);
self
}
pub fn looping(mut self, looping: bool) -> Self {
self.looping = looping;
self
}
}
impl<Message, Theme, Renderer> iced::Widget<Message, Theme, Renderer>
for Video<'_, Message, Theme, Renderer>
where
Message: Clone,
Renderer: PrimitiveRenderer,
{
fn size(&self) -> iced::Size<Length> {
iced::Size {
width: self.width,
height: self.height,
}
}
// The video player should take max space by default
fn layout(
&mut self,
_tree: &mut iced::widget::Tree,
_renderer: &Renderer,
limits: &iced::layout::Limits,
) -> iced::layout::Node {
iced::layout::Node::new(limits.max())
}
fn draw(
&self,
tree: &iced::widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced::renderer::Style,
layout: iced::Layout<'_>,
cursor: iced::mouse::Cursor,
viewport: &iced::Rectangle,
) {
if let Ok((width, height)) = self.handle.source.size() {
let video_size = iced::Size {
width: width as f32,
height: height as f32,
};
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(video_size, bounds.size());
let scale = iced::Vector::new(
adjusted_fit.width / video_size.width,
adjusted_fit.height / video_size.height,
);
let final_size = video_size * scale;
let position = match self.content_fit {
iced::ContentFit::None => iced::Point::new(
bounds.x + (video_size.width - adjusted_fit.width) / 2.0,
bounds.y + (video_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 render = |renderer: &mut Renderer| {
renderer.draw_primitive(
drawing_bounds,
primitive::VideoFrame {
id: self.id.clone(), id: self.id.clone(),
size: iced_wgpu::wgpu::Extent3d { on_end_of_stream: self.on_end_of_stream.clone(),
width: width as u32, on_new_frame: self.on_new_frame.clone(),
height: height as u32, on_about_to_finish: self.on_about_to_finish.clone(),
depth_or_array_layers: 1, bus: self.source.bus.clone(),
},
ready: Arc::clone(&self.handle.frame_ready),
frame: Arc::clone(&self.handle.source.frame),
},
);
}; };
iced_futures::subscription::from_recipe(sub)
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height {
renderer.with_layer(bounds, render);
} else {
render(renderer);
}
}
} }
fn update( pub fn subscription_with<State>(
&mut self, &self,
_tree: &mut iced_core::widget::Tree, state: &State,
event: &iced::Event, f: impl FnOnce(&State) -> iced_futures::subscription::Subscription<Message> + 'static,
_layout: iced_core::Layout<'_>, ) -> iced_futures::subscription::Subscription<Message>
_cursor: iced_core::mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn iced_core::Clipboard,
shell: &mut iced_core::Shell<'_, Message>,
_viewport: &iced::Rectangle,
) {
if let iced::Event::Window(iced::window::Event::RedrawRequested(_)) = event {
if self
.handle
.frame_ready
.load(std::sync::atomic::Ordering::SeqCst)
{
shell.request_redraw();
} else {
shell.request_redraw_at(iced::window::RedrawRequest::At(
iced_core::time::Instant::now() + core::time::Duration::from_millis(32),
));
}
}
}
}
impl<'a, Message, Theme, Renderer> From<Video<'a, Message, Theme, Renderer>>
for iced::Element<'a, Message, Theme, Renderer>
where where
Message: 'a + Clone, State: Send + Sync + 'static,
Theme: 'a,
Renderer: 'a + iced_wgpu::primitive::Renderer,
{ {
fn from(video: Video<'a, Message, Theme, Renderer>) -> Self { let sub = self.subscription();
Self::new(video) iced_futures::subscription::Subscription::batch([sub, f(state)])
}
pub fn on_new_frame(self, message: Message) -> Self {
Self {
on_new_frame: Some(Box::new(message)),
..self
}
}
pub fn on_end_of_stream(self, message: Message) -> Self {
Self {
on_end_of_stream: Some(Box::new(message)),
..self
}
}
pub fn on_about_to_finish(self, message: Message) -> Self {
Self {
on_about_to_finish: Some(Box::new(message)),
..self
}
}
pub fn play(&self) {
self.source.play();
}
pub fn pause(&self) {
self.source.pause();
}
pub fn stop(&self) {
self.source.stop();
}
/// Creates a new video handle and waits for the metadata to be loaded.
pub async fn load(url: impl AsRef<str>) -> Result<Self> {
let handle = Self::new(url)?;
handle.wait().await
} }
} }

View File

@@ -105,7 +105,22 @@ impl iced_wgpu::Primitive for VideoFrame {
let data = buffer let data = buffer
.map_readable() .map_readable()
.expect("BUG: Failed to map gst::Buffer readable"); .expect("BUG: Failed to map gst::Buffer readable");
queue.write_buffer(&video.buffer, 0, &data); // queue.write_buffer(&video.buffer, 0, &data);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &video.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * self.size.width),
rows_per_image: Some(self.size.height),
},
self.size,
);
drop(data); drop(data);
video video
.ready .ready
@@ -118,29 +133,29 @@ impl iced_wgpu::Primitive for VideoFrame {
pipeline: &Self::Pipeline, pipeline: &Self::Pipeline,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView, target: &wgpu::TextureView,
_clip_bounds: &iced_wgpu::core::Rectangle<u32>, bounds: &iced_wgpu::core::Rectangle<u32>,
) { ) {
let Some(video) = pipeline.videos.get(&self.id) else { let Some(video) = pipeline.videos.get(&self.id) else {
return; return;
}; };
encoder.copy_buffer_to_texture( // encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo { // wgpu::TexelCopyBufferInfo {
buffer: &video.buffer, // buffer: &video.buffer,
layout: wgpu::TexelCopyBufferLayout { // layout: wgpu::TexelCopyBufferLayout {
offset: 0, // offset: 0,
bytes_per_row: Some(4 * self.size.width), // bytes_per_row: Some(4 * self.size.width),
rows_per_image: Some(self.size.height), // rows_per_image: Some(self.size.height),
}, // },
}, // },
wgpu::TexelCopyTextureInfo { // wgpu::TexelCopyTextureInfo {
texture: &video.texture, // texture: &video.texture,
mip_level: 0, // mip_level: 0,
origin: wgpu::Origin3d::ZERO, // origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All, // aspect: wgpu::TextureAspect::All,
}, // },
self.size, // self.size,
); // );
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced-video-render-pass"), label: Some("iced-video-render-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment { color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -159,6 +174,12 @@ impl iced_wgpu::Primitive for VideoFrame {
render_pass.set_pipeline(&pipeline.pipeline); render_pass.set_pipeline(&pipeline.pipeline);
render_pass.set_bind_group(0, &video.bind_group, &[]); render_pass.set_bind_group(0, &video.bind_group, &[]);
render_pass.set_scissor_rect(
bounds.x as _,
bounds.y as _,
bounds.width as _,
bounds.height as _,
);
render_pass.draw(0..3, 0..1); render_pass.draw(0..3, 0..1);
// self.ready // self.ready
// .store(false, std::sync::atomic::Ordering::Relaxed); // .store(false, std::sync::atomic::Ordering::Relaxed);

View File

@@ -18,6 +18,7 @@ pub struct VideoSource {
pub(crate) bus: Bus, pub(crate) bus: Bus,
pub(crate) ready: Arc<AtomicBool>, pub(crate) ready: Arc<AtomicBool>,
pub(crate) frame: Arc<Mutex<gst::Sample>>, pub(crate) frame: Arc<Mutex<gst::Sample>>,
pub(crate) size: std::sync::OnceLock<(i32, i32)>,
} }
impl VideoSource { impl VideoSource {
@@ -79,15 +80,17 @@ impl VideoSource {
bus, bus,
ready, ready,
frame, frame,
size: std::sync::OnceLock::new(),
}) })
} }
pub async fn wait(self) -> Result<()> { pub async fn wait(&self) -> Result<()> {
self.playbin self.playbin
.wait_for_states(&[gst::State::Paused, gst::State::Playing]) .wait_for_states(&[gst::State::Paused, gst::State::Playing])
.await .await
.change_context(Error) .change_context(Error)
.attach("Failed to wait for video initialisation") .attach("Failed to wait for video initialisation")?;
Ok(())
} }
pub fn is_playing(&self) -> Result<bool> { pub fn is_playing(&self) -> Result<bool> {
@@ -126,14 +129,20 @@ impl VideoSource {
} }
pub fn size(&self) -> Result<(i32, i32)> { pub fn size(&self) -> Result<(i32, i32)> {
if let Some(size) = self.size.get() {
return Ok(*size);
}
let caps = self let caps = self
.appsink .appsink
.sink("sink") .sink("sink")
.current_caps() .current_caps()
.change_context(Error)?; .change_context(Error)?;
caps.width() let out = caps
.width()
.and_then(|width| caps.height().map(|height| (width, height))) .and_then(|width| caps.height().map(|height| (width, height)))
.ok_or(Error) .ok_or(Error)
.attach("Failed to get width, height") .attach("Failed to get width, height")?;
self.size.set(out);
Ok(out)
} }
} }

View File

@@ -0,0 +1,253 @@
use super::*;
use iced::Length;
use iced_core as iced;
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
use std::marker::PhantomData;
/// This is the Video widget that displays a video.
/// This should be used in the view function.
pub struct Video<'a, Message, Theme = iced::Theme, Renderer = iced_wgpu::Renderer>
where
Renderer: PrimitiveRenderer,
{
id: id::Id,
handle: &'a VideoHandle<Message>,
content_fit: iced::ContentFit,
width: iced::Length,
height: iced::Length,
looping: bool,
__marker: PhantomData<(Renderer, Theme)>,
}
impl<'a, Message, Theme, Renderer> Video<'a, Message, Theme, Renderer>
where
Renderer: PrimitiveRenderer,
Message: Clone,
{
pub fn new(handle: &'a VideoHandle<Message>) -> Self {
Self {
id: handle.id.clone(),
handle: &handle,
content_fit: iced::ContentFit::Contain,
width: Length::Shrink,
height: Length::Shrink,
looping: false,
__marker: PhantomData,
}
}
}
impl<'a, Message, Theme, Renderer> Video<'a, Message, Theme, Renderer>
where
Renderer: PrimitiveRenderer,
{
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn content_fit(mut self, fit: iced::ContentFit) -> Self {
self.content_fit = fit;
self
}
// pub fn on_end_of_stream(mut self, message: Message) -> Self {
// self.on_end_of_stream = Some(message);
// self
// }
//
// pub fn on_new_frame(mut self, message: Message) -> Self {
// self.on_new_frame = Some(message);
// self
// }
pub fn looping(mut self, looping: bool) -> Self {
self.looping = looping;
self
}
}
impl<Message, Theme, Renderer> iced::Widget<Message, Theme, Renderer>
for Video<'_, Message, Theme, Renderer>
where
Message: Clone,
Renderer: PrimitiveRenderer,
{
fn size(&self) -> iced::Size<Length> {
iced::Size {
width: self.width,
height: self.height,
}
}
// The video player should take max space by default
fn layout(
&mut self,
_tree: &mut iced::widget::Tree,
_renderer: &Renderer,
limits: &iced::layout::Limits,
) -> iced::layout::Node {
iced::layout::Node::new(limits.max())
}
fn draw(
&self,
tree: &iced::widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced::renderer::Style,
layout: iced::Layout<'_>,
cursor: iced::mouse::Cursor,
viewport: &iced::Rectangle,
) {
if let Ok((width, height)) = self.handle.source.size() {
let video_size = iced::Size {
width: width as f32,
height: height as f32,
};
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(video_size, bounds.size());
let scale = iced::Vector::new(
adjusted_fit.width / video_size.width,
adjusted_fit.height / video_size.height,
);
let final_size = video_size * scale;
let position = match self.content_fit {
iced::ContentFit::None => iced::Point::new(
bounds.x + (video_size.width - adjusted_fit.width) / 2.0,
bounds.y + (video_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 render = |renderer: &mut Renderer| {
renderer.draw_primitive(
drawing_bounds,
primitive::VideoFrame {
id: self.id.clone(),
size: iced_wgpu::wgpu::Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
ready: Arc::clone(&self.handle.frame_ready),
frame: Arc::clone(&self.handle.source.frame),
},
);
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height {
renderer.with_layer(bounds, render);
} else {
render(renderer);
}
}
}
fn update(
&mut self,
_tree: &mut iced_core::widget::Tree,
event: &iced::Event,
_layout: iced_core::Layout<'_>,
_cursor: iced_core::mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn iced_core::Clipboard,
shell: &mut iced_core::Shell<'_, Message>,
_viewport: &iced::Rectangle,
) {
if let iced::Event::Window(iced::window::Event::RedrawRequested(when)) = event {
if self
.handle
.frame_ready
.load(std::sync::atomic::Ordering::SeqCst)
{
shell.request_redraw();
} else {
shell.request_redraw_at(iced::window::RedrawRequest::At(
iced_core::time::Instant::now() + core::time::Duration::from_millis(16)
- when.elapsed(),
));
}
}
}
}
impl<'a, Message, Theme, Renderer> From<Video<'a, Message, Theme, Renderer>>
for iced::Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
Theme: 'a,
Renderer: 'a + iced_wgpu::primitive::Renderer,
{
fn from(video: Video<'a, Message, Theme, Renderer>) -> Self {
Self::new(video)
}
}
#[derive(Debug, Clone)]
pub struct VideoSubscription<Message> {
pub(crate) id: id::Id,
pub(crate) on_end_of_stream: Option<Box<Message>>,
pub(crate) on_new_frame: Option<Box<Message>>,
pub(crate) on_about_to_finish: Option<Box<Message>>,
// on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message>>,
// on_error: Option<Box<dyn Fn(&glib::Error) -> Message>>,
pub(crate) bus: gst::Bus,
}
impl<Message> VideoSubscription<Message> where Message: Clone {}
impl<Message> iced_futures::subscription::Recipe for VideoSubscription<Message>
where
Message: Clone + Send + Sync + 'static,
{
type Output = Message;
fn hash(&self, state: &mut iced_futures::subscription::Hasher) {
use std::hash::Hash;
self.id.hash(state);
}
fn stream(
self: Box<Self>,
_input: core::pin::Pin<
Box<dyn iced_futures::futures::Stream<Item = iced_futures::subscription::Event> + Send>,
>,
) -> core::pin::Pin<Box<dyn iced_futures::futures::Stream<Item = Self::Output> + Send>> {
// use iced_futures::futures::StreamExt;
use futures_lite::stream::StreamExt;
Box::pin(
self.bus
.filtered_stream(&[gst::MessageType::Eos, gst::MessageType::Element])
.filter_map({
let eos = self.on_end_of_stream.clone();
let frame = self.on_new_frame.clone();
move |message: gst::Message| match message.view() {
gst::MessageView::Eos(_) => eos.clone().map(|m| *m),
gst::MessageView::Element(element_msg) => {
let structure = element_msg.structure();
if let Some(structure) = structure {
if structure.name() == "GstVideoFrameReady" {
frame.clone().map(|m| *m)
} else {
None
}
} else {
None
}
}
_ => None,
}
}),
)
}
}

View File

@@ -5,7 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
# gst = { workspace = true } # gst = { workspace = true }
wgpu = "*" wgpu = "27"
gstreamer = { version = "0.24.4", features = ["v1_26"] } gstreamer = { version = "0.24.4", features = ["v1_26"] }
gstreamer-app = { version = "0.24.4", features = ["v1_26"] } gstreamer-app = { version = "0.24.4", features = ["v1_26"] }
gstreamer-base = { version = "0.24.4", features = ["v1_26"] } gstreamer-base = { version = "0.24.4", features = ["v1_26"] }

View File

@@ -19,7 +19,7 @@ iced = { workspace = true, default-features = true, features = [
] } ] }
iced_video_player = { workspace = true } iced-video = { workspace = true }
iced_wgpu = "0.14.0" iced_wgpu = "0.14.0"
iced_winit = "0.14.0" iced_winit = "0.14.0"
reqwest = "0.12.24" reqwest = "0.12.24"

View File

@@ -2,7 +2,7 @@ mod settings;
mod video; mod video;
mod shared_string; mod shared_string;
use iced_video_player::{Video, VideoPlayer}; use iced_video::{Video, VideoHandle};
use shared_string::SharedString; use shared_string::SharedString;
use std::sync::Arc; use std::sync::Arc;
@@ -140,7 +140,7 @@ struct State {
screen: Screen, screen: Screen,
settings: settings::SettingsState, settings: settings::SettingsState,
is_authenticated: bool, is_authenticated: bool,
video: Option<Arc<Video>>, video: Option<Arc<VideoHandle>>,
} }
impl State { impl State {

View File

@@ -3,6 +3,7 @@ use super::*;
pub enum VideoMessage { pub enum VideoMessage {
EndOfStream, EndOfStream,
Open(url::Url), Open(url::Url),
Loaded(VideoHandle),
Pause, Pause,
Play, Play,
Seek(f64), Seek(f64),
@@ -17,34 +18,24 @@ pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
Task::none() Task::none()
} }
VideoMessage::Open(url) => { VideoMessage::Open(url) => {
match Video::new(&url) Task::perform(VideoHandle::load(url.clone()), move |result| match result {
.inspect_err(|err| { Ok(video) => Message::Video(VideoMessage::Loaded(video)),
tracing::error!("Failed to play video at {}: {:?}", url, err); Err(err) => Message::Error(format!("Error opening video at {}: {:?}", url, err)),
}) })
.inspect(|video| {
tracing::error!("Framerate is {}", video.framerate());
})
.map(Arc::new)
{
Ok(video) => {
state.video = Some(video);
Task::none()
}
Err(err) => Task::done(Message::Error(format!(
"Error opening video at {}: {:?}",
url, err
))),
} }
VideoMessage::Loaded(video) => {
state.video = Some(Arc::new(video));
Task::done(VideoMessage::Play).map(Message::Video)
} }
VideoMessage::Pause => { VideoMessage::Pause => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) { if let Some(ref video) = state.video {
video.set_paused(true); video.pause();
} }
Task::none() Task::none()
} }
VideoMessage::Play => { VideoMessage::Play => {
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) { if let Some(ref video) = state.video {
video.set_paused(false); video.play();
} }
Task::none() Task::none()
} }
@@ -55,24 +46,25 @@ pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
Task::none() Task::none()
} }
VideoMessage::Stop => { VideoMessage::Stop => {
state.video.as_ref().map(|video| {
video.stop();
});
state.video = None; state.video = None;
Task::none() Task::none()
} }
VideoMessage::Test => { VideoMessage::Test => {
let url = url::Url::parse( 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", "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm",
// "https://www.youtube.com/watch?v=QbUUaXGA3C4",
) )
.expect("Impossible: Failed to parse hardcoded URL"); .expect("Impossible: Failed to parse hardcoded URL");
Task::done(Message::Video(VideoMessage::Open(url))) Task::done(VideoMessage::Open(url)).map(Message::Video)
} }
} }
} }
pub fn player(video: &Video) -> Element<'_, Message> { pub fn player(video: &VideoHandle) -> Element<'_, Message> {
container( container(
VideoPlayer::new(video) Video::new(video)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.content_fit(iced::ContentFit::Contain) .content_fit(iced::ContentFit::Contain)