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]
|
[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]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -11,87 +11,154 @@ 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| {
|
||||||
|
// Foo
|
||||||
|
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);
|
||||||
|
|||||||
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 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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// This is the Video widget that displays a video.
|
pub async fn wait(self) -> Result<Self> {
|
||||||
/// This should be used in the view function.
|
self.source.wait().await?;
|
||||||
pub struct Video<'a, Message, Theme = iced::Theme, Renderer = iced_wgpu::Renderer>
|
Ok(self)
|
||||||
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 {
|
pub fn subscription(&self) -> iced_futures::subscription::Subscription<Message> {
|
||||||
self.height = height;
|
let sub = widget::VideoSubscription {
|
||||||
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,
|
where
|
||||||
_renderer: &Renderer,
|
State: Send + Sync + 'static,
|
||||||
_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();
|
let sub = self.subscription();
|
||||||
} else {
|
iced_futures::subscription::Subscription::batch([sub, f(state)])
|
||||||
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>>
|
pub fn on_new_frame(self, message: Message) -> Self {
|
||||||
for iced::Element<'a, Message, Theme, Renderer>
|
Self {
|
||||||
where
|
on_new_frame: Some(Box::new(message)),
|
||||||
Message: 'a + Clone,
|
..self
|
||||||
Theme: 'a,
|
}
|
||||||
Renderer: 'a + iced_wgpu::primitive::Renderer,
|
}
|
||||||
{
|
|
||||||
fn from(video: Video<'a, Message, Theme, Renderer>) -> Self {
|
pub fn on_end_of_stream(self, message: Message) -> Self {
|
||||||
Self::new(video)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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]
|
[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"] }
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ api = { version = "0.1.0", path = "../api" }
|
|||||||
blurhash = "0.2.3"
|
blurhash = "0.2.3"
|
||||||
bytes = "1.11.0"
|
bytes = "1.11.0"
|
||||||
gpui_util = "0.2.2"
|
gpui_util = "0.2.2"
|
||||||
iced = { workspace = true, default-features = true, features = [
|
iced = { workspace = true, features = [
|
||||||
"advanced",
|
"advanced",
|
||||||
"canvas",
|
"canvas",
|
||||||
"image",
|
"image",
|
||||||
"sipper",
|
"sipper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"debug",
|
"debug",
|
||||||
] }
|
"hot",
|
||||||
|
], default-features = true }
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
|
|||||||
@@ -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<Message>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
@@ -187,9 +187,8 @@ pub enum Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update(state: &mut State, message: Message) -> Task<Message> {
|
fn update(state: &mut State, message: Message) -> Task<Message> {
|
||||||
// if let Some(client) = state.jellyfin_client.clone() {
|
|
||||||
match message {
|
match message {
|
||||||
Message::Settings(msg) => settings::update(&mut state.settings, msg),
|
Message::Settings(msg) => settings::update(state, msg),
|
||||||
Message::OpenItem(id) => {
|
Message::OpenItem(id) => {
|
||||||
if let Some(client) = state.jellyfin_client.clone() {
|
if let Some(client) = state.jellyfin_client.clone() {
|
||||||
use api::jellyfin::BaseItemKind::*;
|
use api::jellyfin::BaseItemKind::*;
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
use iced::Element;
|
use iced::Element;
|
||||||
|
// mod widget;
|
||||||
|
|
||||||
pub fn settings(state: &State) -> Element<'_, Message> {
|
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 {
|
match message {
|
||||||
SettingsMessage::Open => {}
|
SettingsMessage::Open => {
|
||||||
SettingsMessage::Close => {}
|
tracing::trace!("Opening settings");
|
||||||
|
state.screen = Screen::Settings;
|
||||||
|
}
|
||||||
|
SettingsMessage::Close => {
|
||||||
|
tracing::trace!("Closing settings");
|
||||||
|
state.screen = Screen::Home;
|
||||||
|
}
|
||||||
SettingsMessage::Select(screen) => {
|
SettingsMessage::Select(screen) => {
|
||||||
tracing::trace!("Switching settings screen to {:?}", screen);
|
tracing::trace!("Switching settings screen to {:?}", screen);
|
||||||
|
state.settings.screen = screen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -70,13 +78,80 @@ pub struct ServerForm {
|
|||||||
|
|
||||||
mod screens {
|
mod screens {
|
||||||
use super::*;
|
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> {
|
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> {
|
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> {
|
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 {
|
pub enum VideoMessage {
|
||||||
EndOfStream,
|
EndOfStream,
|
||||||
Open(url::Url),
|
Open(url::Url),
|
||||||
|
Loaded(VideoHandle<Message>),
|
||||||
Pause,
|
Pause,
|
||||||
Play,
|
Play,
|
||||||
Seek(f64),
|
Seek(f64),
|
||||||
@@ -17,34 +18,26 @@ 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.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
||||||
|
));
|
||||||
|
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,28 +48,28 @@ 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<Message>) -> 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),
|
||||||
.on_end_of_stream(Message::Video(VideoMessage::EndOfStream)),
|
|
||||||
)
|
)
|
||||||
.style(|_| container::background(iced::Color::BLACK))
|
.style(|_| container::background(iced::Color::BLACK))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|||||||
Reference in New Issue
Block a user