diff options
Diffstat (limited to 'src/player.rs')
-rw-r--r-- | src/player.rs | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..c784a04 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,298 @@ +use gst::bus::BusWatchGuard; +use gst::prelude::*; +use gst_play::{Play, PlayMessage, PlayVideoOverlayVideoRenderer}; +use gtk::gdk; +use gtk::glib::{self, clone}; +use gtk::prelude::*; +use relm4::{ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent}; + +#[allow(dead_code)] +pub struct Player { + // GStreamer-related + gtksink: gst::Element, + renderer: PlayVideoOverlayVideoRenderer, + player: Play, + bus_watch: BusWatchGuard, + // UI state + is_playing: bool, + duration: gst::ClockTime, + position: gst::ClockTime, + seeking: bool, +} + +#[derive(Debug)] +pub enum PlayerMsg { + SetUrl(String), + PlayPause, + Play, + Pause, + SeekTo(gst::ClockTime), + StartSeeking, + StopSeeking, + // messages from GStreamer + UpdatePosition(gst::ClockTime), + UpdateDuration(gst::ClockTime), +} + +#[derive(Debug)] +pub enum PlayerOutput { + PositionUpdate(gst::ClockTime), +} + +fn format_time(time: gst::ClockTime) -> String { + let seconds = time.seconds(); + let minutes = seconds / 60; + let hours = minutes / 60; + + if hours > 0 { + format!("{}:{:02}:{:02}", hours, minutes % 60, seconds % 60) + } else { + format!("{}:{:02}", minutes, seconds % 60) + } +} + +#[relm4::component(pub)] +impl SimpleComponent for Player { + type Input = PlayerMsg; + type Output = PlayerOutput; + type Init = (); + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + + // Video area + gtk::Picture { + set_paintable: Some(&paintable), + set_vexpand: true, + }, + + // Control bar + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 10, + set_margin_all: 10, + + // Play/Pause button + #[name = "play_pause_btn"] + gtk::Button { + #[watch] + set_icon_name: if model.is_playing { "media-playback-pause" } else { "media-playback-start" }, + connect_clicked => PlayerMsg::PlayPause, + add_css_class: "circular", + }, + + // Current time label + gtk::Label { + #[watch] + set_text: &format_time(model.position), + set_width_chars: 8, + }, + + // Seek slider + #[name = "seek_scale"] + gtk::Scale { + set_hexpand: true, + set_orientation: gtk::Orientation::Horizontal, + #[watch] + set_range: (0.0, model.duration.mseconds() as f64), + #[watch] + set_value?: if !model.seeking { + Some(model.position.mseconds() as f64) + } else { + None + }, + connect_change_value[sender] => move |_, _, value| { + let position = gst::ClockTime::from_mseconds(value as u64); + sender.input(PlayerMsg::SeekTo(position)); + glib::Propagation::Proceed + }, + }, + + // Duration label + #[name = "duration_label"] + gtk::Label { + #[watch] + set_text: &format_time(model.duration), + set_width_chars: 8, + }, + } + } + } + + fn init( + _init: Self::Init, + _window: Self::Root, + sender: ComponentSender<Self>, + ) -> ComponentParts<Self> { + let gtksink = gst::ElementFactory::make("gtk4paintablesink") + .build() + .expect("Failed to create gtk4paintablesink"); + + // Need to set state to Ready to get a GL context + gtksink + .set_state(gst::State::Ready) + .expect("Failed to set GTK sink state to ready"); + + let paintable = gtksink.property::<gdk::Paintable>("paintable"); + + let sink = if paintable + .property::<Option<gdk::GLContext>>("gl-context") + .is_some() + { + gst::ElementFactory::make("glsinkbin") + .property("sink", >ksink) + .build() + .expect("Failed to build glsinkbin") + } else { + gtksink.clone() + }; + + let renderer = PlayVideoOverlayVideoRenderer::with_sink(&sink); + + let player = Play::new(Some( + renderer.clone().upcast::<gst_play::PlayVideoRenderer>(), + )); + + let mut config = player.config(); + config.set_position_update_interval(5); + player.set_config(config).unwrap(); + + // 100MiB ring buffer to improve seek performance + player + .pipeline() + .set_property("ring-buffer-max-size", 100 * 1024 * 1024 as u64); + + let bus_watch = player + .message_bus() + .add_watch_local(clone!( + #[strong] + sender, + move |_, message| { + let play_message = if let Ok(msg) = PlayMessage::parse(message) { + msg + } else { + return glib::ControlFlow::Continue; + }; + + match play_message { + PlayMessage::Error(error_msg) => { + eprintln!("Playback error: {:?}", error_msg.error()); + if let Some(details) = error_msg.details() { + eprintln!("Error details: {:?}", details); + } + } + PlayMessage::PositionUpdated(pos) => { + if let Some(position) = pos.position() { + sender.input(PlayerMsg::UpdatePosition(position)); + sender + .output(PlayerOutput::PositionUpdate(position)) + .unwrap(); + } + } + PlayMessage::DurationChanged(dur) => { + if let Some(duration) = dur.duration() { + sender.input(PlayerMsg::UpdateDuration(duration)); + } + } + PlayMessage::Buffering(_) => { + // TODO + } + _ => {} + } + + glib::ControlFlow::Continue + } + )) + .expect("Failed to add message bus watch"); + + let model = Player { + gtksink, + renderer, + player, + bus_watch, + is_playing: false, + duration: gst::ClockTime::ZERO, + position: gst::ClockTime::ZERO, + seeking: false, + }; + + let widgets = view_output!(); + + // find the existing GestureClick controller in the Scale widget + // instead of adding a new one to avoid this bug: + // https://gitlab.gnome.org/GNOME/gtk/-/issues/4939 + let gesture = widgets + .seek_scale + .observe_controllers() + .into_iter() + .find_map(|controller| controller.unwrap().downcast::<gtk::GestureClick>().ok()) + .unwrap(); + + gesture.connect_pressed(clone!( + #[strong] + sender, + move |_, _, _, _| { + sender.input(PlayerMsg::StartSeeking); + } + )); + gesture.connect_released(clone!( + #[strong] + sender, + move |_, _, _, _| { + sender.input(PlayerMsg::StopSeeking); + } + )); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) { + match msg { + PlayerMsg::SetUrl(url) => { + self.player.set_uri(Some(&url)); + self.play(); + } + PlayerMsg::PlayPause => { + if self.is_playing { + self.pause(); + } else { + self.play(); + } + } + PlayerMsg::Play => self.play(), + PlayerMsg::Pause => self.pause(), + PlayerMsg::SeekTo(position) => { + //self.seek_position = position_ms; + self.player.seek(position); + self.position = position; + } + PlayerMsg::StartSeeking => { + self.seeking = true; + } + PlayerMsg::StopSeeking => { + self.seeking = false; + } + PlayerMsg::UpdatePosition(position) => { + if !self.seeking { + self.position = position; + } + } + PlayerMsg::UpdateDuration(duration) => { + self.duration = duration; + } + } + } +} + +impl Player { + fn play(&mut self) { + self.player.play(); + self.is_playing = true; + } + + fn pause(&mut self) { + self.player.pause(); + self.is_playing = false; + } +} |