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), SubtitleSelectionButtonPressed, } 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 gtk::Button { #[watch] set_icon_name: if model.is_playing { "media-playback-pause-symbolic" } else { "media-playback-start-symbolic" }, connect_clicked => PlayerMsg::PlayPause, }, // 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, }, // Subtitle selection button gtk::Button { set_icon_name: "media-view-subtitles-symbolic", connect_clicked[sender] => move |_| { sender.output(PlayerOutput::SubtitleSelectionButtonPressed).unwrap(); }, }, } } } fn init( _init: Self::Init, _window: Self::Root, sender: ComponentSender, ) -> ComponentParts { 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::("paintable"); let sink = if paintable .property::>("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::(), )); 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::().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) { 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; } }