summary refs log tree commit diff
path: root/src/player.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/player.rs')
-rw-r--r--src/player.rs298
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", &gtksink)
+                .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;
+    }
+}