Compare commits
2 Commits
99853167df
...
2b2e8060e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b2e8060e7 | ||
|
|
584495453f |
908
Cargo.lock
generated
908
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,14 +13,15 @@ members = [
|
||||
]
|
||||
[workspace.dependencies]
|
||||
iced = { version = "0.14.0" }
|
||||
iced_video_player = "0.6"
|
||||
gst = { version = "0.1.0", path = "gst" }
|
||||
iced_wgpu = { version = "0.14.0" }
|
||||
iced-video = { version = "0.1.0", path = "crates/iced-video" }
|
||||
|
||||
[patch.crates-io]
|
||||
iced_wgpu = { 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_futures = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||
iced = { git = "https://github.com/uttarayan21/iced", branch = "0.14" }
|
||||
|
||||
[package]
|
||||
|
||||
@@ -5,6 +5,7 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
error-stack = "0.6.0"
|
||||
futures-lite = "2.6.1"
|
||||
gst.workspace = true
|
||||
iced_core = "0.14.0"
|
||||
iced_futures = "0.14.0"
|
||||
|
||||
@@ -11,87 +11,154 @@ pub fn main() -> iced::Result {
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.init();
|
||||
iced::application(State::new, update, view)
|
||||
.subscription(keyboard_event)
|
||||
.subscription(|state| {
|
||||
// Foo
|
||||
match &state.video {
|
||||
Some(video) => video.subscription_with(state, keyboard_event),
|
||||
None => keyboard_event(state),
|
||||
}
|
||||
})
|
||||
.run()
|
||||
}
|
||||
|
||||
fn keyboard_event(state: &State) -> iced::Subscription<Message> {
|
||||
fn keyboard_event(_state: &State) -> iced::Subscription<Message> {
|
||||
use iced::keyboard::{Key, key::Named};
|
||||
iced::keyboard::listen().map(move |event| match event {
|
||||
iced::keyboard::Event::KeyPressed { key, .. } => {
|
||||
let key = key.as_ref();
|
||||
match key {
|
||||
Key::Named(Named::Escape) | Key::Character("q") => Message::Quit,
|
||||
Key::Character("f") => Message::Fullscreen,
|
||||
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)]
|
||||
pub struct State {
|
||||
video: VideoHandle,
|
||||
video: Option<VideoHandle<Message>>,
|
||||
fullscreen: bool,
|
||||
}
|
||||
|
||||
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 }
|
||||
pub fn new() -> (Self, iced::Task<Message>) {
|
||||
(
|
||||
Self {
|
||||
video: None,
|
||||
fullscreen: false,
|
||||
},
|
||||
iced::Task::done(Message::Load),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Play,
|
||||
Pause,
|
||||
Toggle,
|
||||
Noop,
|
||||
Load,
|
||||
Fullscreen,
|
||||
OnLoad(VideoHandle<Message>),
|
||||
OnError(String),
|
||||
NewFrame,
|
||||
Eos,
|
||||
Quit,
|
||||
}
|
||||
|
||||
pub fn update(state: &mut State, message: Message) -> iced::Task<Message> {
|
||||
match message {
|
||||
Message::Load => {
|
||||
// does stuff
|
||||
Message::NewFrame => {
|
||||
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 => {
|
||||
state.video.source().play().expect("Failed to play video");
|
||||
state
|
||||
.video
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.source()
|
||||
.play()
|
||||
.expect("Failed to play video");
|
||||
iced::Task::none()
|
||||
}
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
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);
|
||||
}
|
||||
Message::Noop => iced::Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.height(iced::Length::Fill)
|
||||
.content_fit(iced::ContentFit::Contain);
|
||||
|
||||
8
crates/iced-video/justfile
Normal file
8
crates/iced-video/justfile
Normal 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
|
||||
@@ -1,13 +1,10 @@
|
||||
pub mod id;
|
||||
pub mod primitive;
|
||||
pub mod source;
|
||||
use iced_core as iced;
|
||||
use iced_renderer::Renderer as RendererWithFallback;
|
||||
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
|
||||
pub mod widget;
|
||||
pub use widget::Video;
|
||||
|
||||
use error_stack::{Report, ResultExt};
|
||||
use iced::Length;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use gst::plugins::app::AppSink;
|
||||
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 should be keps in the application state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VideoHandle {
|
||||
pub struct VideoHandle<Message> {
|
||||
id: id::Id,
|
||||
source: source::VideoSource,
|
||||
is_metadata_loaded: Arc<AtomicBool>,
|
||||
is_playing: Arc<AtomicBool>,
|
||||
is_eos: Arc<AtomicBool>,
|
||||
pub source: source::VideoSource,
|
||||
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 {
|
||||
&self.id
|
||||
}
|
||||
@@ -47,206 +44,75 @@ impl VideoHandle {
|
||||
Ok(Self {
|
||||
id: id::Id::unique(),
|
||||
source: source,
|
||||
is_metadata_loaded: Arc::new(AtomicBool::new(false)),
|
||||
is_playing: Arc::new(AtomicBool::new(false)),
|
||||
is_eos: Arc::new(AtomicBool::new(false)),
|
||||
on_new_frame: None,
|
||||
on_end_of_stream: None,
|
||||
on_about_to_finish: None,
|
||||
frame_ready,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
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 async fn wait(self) -> Result<Self> {
|
||||
self.source.wait().await?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: Length) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
pub fn subscription(&self) -> iced_futures::subscription::Subscription<Message> {
|
||||
let sub = widget::VideoSubscription {
|
||||
id: self.id.clone(),
|
||||
on_end_of_stream: self.on_end_of_stream.clone(),
|
||||
on_new_frame: self.on_new_frame.clone(),
|
||||
on_about_to_finish: self.on_about_to_finish.clone(),
|
||||
bus: self.source.bus.clone(),
|
||||
};
|
||||
iced_futures::subscription::from_recipe(sub)
|
||||
}
|
||||
|
||||
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(
|
||||
pub fn subscription_with<State>(
|
||||
&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,
|
||||
),
|
||||
};
|
||||
state: &State,
|
||||
f: impl FnOnce(&State) -> iced_futures::subscription::Subscription<Message> + 'static,
|
||||
) -> iced_futures::subscription::Subscription<Message>
|
||||
where
|
||||
State: Send + Sync + 'static,
|
||||
{
|
||||
let sub = self.subscription();
|
||||
iced_futures::subscription::Subscription::batch([sub, f(state)])
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
pub fn on_new_frame(self, message: Message) -> Self {
|
||||
Self {
|
||||
on_new_frame: Some(Box::new(message)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
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(_)) = 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),
|
||||
));
|
||||
}
|
||||
pub fn on_end_of_stream(self, message: Message) -> Self {
|
||||
Self {
|
||||
on_end_of_stream: Some(Box::new(message)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,22 @@ impl iced_wgpu::Primitive for VideoFrame {
|
||||
let data = buffer
|
||||
.map_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);
|
||||
video
|
||||
.ready
|
||||
@@ -118,29 +133,29 @@ impl iced_wgpu::Primitive for VideoFrame {
|
||||
pipeline: &Self::Pipeline,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
||||
encoder.copy_buffer_to_texture(
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &video.buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * self.size.width),
|
||||
rows_per_image: Some(self.size.height),
|
||||
},
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &video.texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
self.size,
|
||||
);
|
||||
// encoder.copy_buffer_to_texture(
|
||||
// wgpu::TexelCopyBufferInfo {
|
||||
// buffer: &video.buffer,
|
||||
// layout: wgpu::TexelCopyBufferLayout {
|
||||
// offset: 0,
|
||||
// bytes_per_row: Some(4 * self.size.width),
|
||||
// rows_per_image: Some(self.size.height),
|
||||
// },
|
||||
// },
|
||||
// wgpu::TexelCopyTextureInfo {
|
||||
// texture: &video.texture,
|
||||
// mip_level: 0,
|
||||
// origin: wgpu::Origin3d::ZERO,
|
||||
// aspect: wgpu::TextureAspect::All,
|
||||
// },
|
||||
// self.size,
|
||||
// );
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("iced-video-render-pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
@@ -159,6 +174,12 @@ impl iced_wgpu::Primitive for VideoFrame {
|
||||
|
||||
render_pass.set_pipeline(&pipeline.pipeline);
|
||||
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);
|
||||
// self.ready
|
||||
// .store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct VideoSource {
|
||||
pub(crate) bus: Bus,
|
||||
pub(crate) ready: Arc<AtomicBool>,
|
||||
pub(crate) frame: Arc<Mutex<gst::Sample>>,
|
||||
pub(crate) size: std::sync::OnceLock<(i32, i32)>,
|
||||
}
|
||||
|
||||
impl VideoSource {
|
||||
@@ -79,15 +80,17 @@ impl VideoSource {
|
||||
bus,
|
||||
ready,
|
||||
frame,
|
||||
size: std::sync::OnceLock::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> Result<()> {
|
||||
pub async fn wait(&self) -> Result<()> {
|
||||
self.playbin
|
||||
.wait_for_states(&[gst::State::Paused, gst::State::Playing])
|
||||
.await
|
||||
.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> {
|
||||
@@ -126,14 +129,20 @@ impl VideoSource {
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Result<(i32, i32)> {
|
||||
if let Some(size) = self.size.get() {
|
||||
return Ok(*size);
|
||||
}
|
||||
let caps = self
|
||||
.appsink
|
||||
.sink("sink")
|
||||
.current_caps()
|
||||
.change_context(Error)?;
|
||||
caps.width()
|
||||
let out = caps
|
||||
.width()
|
||||
.and_then(|width| caps.height().map(|height| (width, height)))
|
||||
.ok_or(Error)
|
||||
.attach("Failed to get width, height")
|
||||
.attach("Failed to get width, height")?;
|
||||
self.size.set(out);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
253
crates/iced-video/src/widget.rs
Normal file
253
crates/iced-video/src/widget.rs
Normal 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,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
# gst = { workspace = true }
|
||||
wgpu = "*"
|
||||
wgpu = "27"
|
||||
gstreamer = { 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"] }
|
||||
|
||||
@@ -9,17 +9,18 @@ api = { version = "0.1.0", path = "../api" }
|
||||
blurhash = "0.2.3"
|
||||
bytes = "1.11.0"
|
||||
gpui_util = "0.2.2"
|
||||
iced = { workspace = true, default-features = true, features = [
|
||||
iced = { workspace = true, features = [
|
||||
"advanced",
|
||||
"canvas",
|
||||
"image",
|
||||
"sipper",
|
||||
"tokio",
|
||||
"debug",
|
||||
] }
|
||||
"hot",
|
||||
], default-features = true }
|
||||
|
||||
|
||||
iced_video_player = { workspace = true }
|
||||
iced-video = { workspace = true }
|
||||
iced_wgpu = "0.14.0"
|
||||
iced_winit = "0.14.0"
|
||||
reqwest = "0.12.24"
|
||||
|
||||
@@ -2,7 +2,7 @@ mod settings;
|
||||
mod video;
|
||||
|
||||
mod shared_string;
|
||||
use iced_video_player::{Video, VideoPlayer};
|
||||
use iced_video::{Video, VideoHandle};
|
||||
use shared_string::SharedString;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -140,7 +140,7 @@ struct State {
|
||||
screen: Screen,
|
||||
settings: settings::SettingsState,
|
||||
is_authenticated: bool,
|
||||
video: Option<Arc<Video>>,
|
||||
video: Option<Arc<VideoHandle<Message>>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -187,9 +187,8 @@ pub enum Message {
|
||||
}
|
||||
|
||||
fn update(state: &mut State, message: Message) -> Task<Message> {
|
||||
// if let Some(client) = state.jellyfin_client.clone() {
|
||||
match message {
|
||||
Message::Settings(msg) => settings::update(&mut state.settings, msg),
|
||||
Message::Settings(msg) => settings::update(state, msg),
|
||||
Message::OpenItem(id) => {
|
||||
if let Some(client) = state.jellyfin_client.clone() {
|
||||
use api::jellyfin::BaseItemKind::*;
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
use crate::*;
|
||||
use iced::Element;
|
||||
// mod widget;
|
||||
|
||||
pub fn settings(state: &State) -> Element<'_, Message> {
|
||||
empty()
|
||||
screens::settings(state)
|
||||
}
|
||||
|
||||
pub fn update(_state: &mut SettingsState, message: SettingsMessage) -> Task<Message> {
|
||||
pub fn update(state: &mut State, message: SettingsMessage) -> Task<Message> {
|
||||
match message {
|
||||
SettingsMessage::Open => {}
|
||||
SettingsMessage::Close => {}
|
||||
SettingsMessage::Open => {
|
||||
tracing::trace!("Opening settings");
|
||||
state.screen = Screen::Settings;
|
||||
}
|
||||
SettingsMessage::Close => {
|
||||
tracing::trace!("Closing settings");
|
||||
state.screen = Screen::Home;
|
||||
}
|
||||
SettingsMessage::Select(screen) => {
|
||||
tracing::trace!("Switching settings screen to {:?}", screen);
|
||||
state.settings.screen = screen;
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
@@ -70,13 +78,80 @@ pub struct ServerForm {
|
||||
|
||||
mod screens {
|
||||
use super::*;
|
||||
pub fn settings(state: &State) -> Element<'_, Message> {
|
||||
row([settings_list(state), settings_screen(state)]).into()
|
||||
}
|
||||
|
||||
pub fn settings_screen(state: &State) -> Element<'_, Message> {
|
||||
container(match state.settings.screen {
|
||||
SettingsScreen::Main => main(state),
|
||||
SettingsScreen::Servers => server(state),
|
||||
SettingsScreen::Users => user(state),
|
||||
})
|
||||
.width(Length::FillPortion(10))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn settings_list(state: &State) -> Element<'_, Message> {
|
||||
scrollable(
|
||||
column(
|
||||
[
|
||||
button(center_text("Main")).on_press(Message::Settings(
|
||||
SettingsMessage::Select(SettingsScreen::Main),
|
||||
)),
|
||||
button(center_text("Servers")).on_press(Message::Settings(
|
||||
SettingsMessage::Select(SettingsScreen::Servers),
|
||||
)),
|
||||
button(center_text("Users")).on_press(Message::Settings(
|
||||
SettingsMessage::Select(SettingsScreen::Users),
|
||||
)),
|
||||
]
|
||||
.map(|p| p.clip(true).width(Length::Fill).into()),
|
||||
)
|
||||
.width(Length::FillPortion(2))
|
||||
// .max_width(Length::FillPortion(3))
|
||||
.spacing(10)
|
||||
.padding(10),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn main(state: &State) -> Element<'_, Message> {
|
||||
empty()
|
||||
// placeholder for now
|
||||
container(
|
||||
Column::new()
|
||||
.push(text("Main Settings"))
|
||||
.push(toggler(true).label("Foobar"))
|
||||
.spacing(20)
|
||||
.padding(20),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
pub fn server(state: &State) -> Element<'_, Message> {
|
||||
empty()
|
||||
container(
|
||||
Column::new()
|
||||
.push(text("Server Settings"))
|
||||
.push(toggler(false).label("Enable Server"))
|
||||
.spacing(20)
|
||||
.padding(20),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
pub fn user(state: &State) -> Element<'_, Message> {
|
||||
empty()
|
||||
container(
|
||||
Column::new()
|
||||
.push(text("User Settings"))
|
||||
.push(toggler(true).label("Enable User"))
|
||||
.spacing(20)
|
||||
.padding(20),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_text(content: &str) -> Element<'_, Message> {
|
||||
text(content)
|
||||
.align_x(Alignment::Center)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use super::*;
|
||||
pub enum VideoMessage {
|
||||
EndOfStream,
|
||||
Open(url::Url),
|
||||
Loaded(VideoHandle<Message>),
|
||||
Pause,
|
||||
Play,
|
||||
Seek(f64),
|
||||
@@ -17,34 +18,26 @@ pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Open(url) => {
|
||||
match Video::new(&url)
|
||||
.inspect_err(|err| {
|
||||
tracing::error!("Failed to play 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
|
||||
))),
|
||||
}
|
||||
Task::perform(VideoHandle::load(url.clone()), move |result| match result {
|
||||
Ok(video) => Message::Video(VideoMessage::Loaded(video)),
|
||||
Err(err) => Message::Error(format!("Error opening video at {}: {:?}", url, err)),
|
||||
})
|
||||
}
|
||||
VideoMessage::Loaded(video) => {
|
||||
state.video = Some(Arc::new(
|
||||
video.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
||||
));
|
||||
Task::done(VideoMessage::Play).map(Message::Video)
|
||||
}
|
||||
VideoMessage::Pause => {
|
||||
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
||||
video.set_paused(true);
|
||||
if let Some(ref video) = state.video {
|
||||
video.pause();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Play => {
|
||||
if let Some(video) = state.video.as_mut().and_then(Arc::get_mut) {
|
||||
video.set_paused(false);
|
||||
if let Some(ref video) = state.video {
|
||||
video.play();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -55,28 +48,28 @@ pub fn update(state: &mut State, message: VideoMessage) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
VideoMessage::Stop => {
|
||||
state.video.as_ref().map(|video| {
|
||||
video.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",
|
||||
// "https://www.youtube.com/watch?v=QbUUaXGA3C4",
|
||||
)
|
||||
.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<Message>) -> Element<'_, Message> {
|
||||
container(
|
||||
VideoPlayer::new(video)
|
||||
Video::new(video)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.content_fit(iced::ContentFit::Contain)
|
||||
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
||||
.content_fit(iced::ContentFit::Contain),
|
||||
)
|
||||
.style(|_| container::background(iced::Color::BLACK))
|
||||
.width(Length::Fill)
|
||||
|
||||
Reference in New Issue
Block a user