mxl_player_components/ui/player/
widget.rs

1use gst_video::VideoRectangle;
2use log::*;
3use mxl_relm4_components::relm4::{self, css as adw_css, gtk::glib, gtk::prelude::*, prelude::*};
4use std::{borrow::BorrowMut, rc::Rc, sync::Mutex};
5
6use glib::clone;
7
8use super::{
9    messages::{
10        PlaybackState, PlayerComponentCommand, PlayerComponentInput, PlayerComponentOutput, internal::PrivateMsg,
11    },
12    model::{PlayerComponentInit, PlayerComponentModel, ViewData},
13};
14use crate::{
15    localization::helper::fl,
16    player::{MaxLateness, PlayerBuilder},
17};
18
19const SCALE_MULTIPLIER: f64 = 2.0;
20
21#[relm4::component(pub)]
22impl Component for PlayerComponentModel {
23    type Init = PlayerComponentInit;
24    type Input = PlayerComponentInput;
25    type Output = PlayerComponentOutput;
26    type CommandOutput = PlayerComponentCommand;
27
28    view! {
29        #[name = "video_view"]
30        gtk::Overlay {
31            #[name = "video_scrolled_window"]
32            gtk::ScrolledWindow {
33                // Set scrollbar policy to external, to disable them (Never disables scrolling at all):
34                set_hscrollbar_policy: gtk::PolicyType::External,
35                set_vscrollbar_policy: gtk::PolicyType::External,
36                set_vexpand: true,
37                set_hexpand: true,
38
39
40                gtk::Overlay {
41                    #[name = "video_picture"]
42                    gtk::Picture {
43                        set_content_fit: gtk::ContentFit::Fill,
44                    },
45
46
47                    add_overlay = drawing_overlay = &gtk::DrawingArea {
48                        #[watch]
49                        set_visible: model.show_drawing_overlay && model.playback_state != PlaybackState::Stopped && model.playback_state != PlaybackState::Error,
50                        set_vexpand: true,
51                        set_hexpand: true,
52                        set_can_target: true,
53
54                    },
55                },
56            },
57
58            add_overlay = overlay = &gtk::Box {
59                #[watch]
60                set_visible: model.show_seeking_overlay && model.playback_state == PlaybackState::Buffering,
61                add_css_class: adw_css::OSD,
62                set_vexpand: true,
63                set_hexpand: true,
64                set_can_target: false,
65
66                gtk::Box {
67                    set_orientation: gtk::Orientation::Vertical,
68                    set_hexpand: true,
69                    set_vexpand: true,
70                    set_halign: gtk::Align::Center,
71                    set_valign: gtk::Align::Center,
72                    set_spacing: 8,
73
74                    gtk::Label {
75                        #[watch]
76                        set_label: if model.seeking {
77                            fl!("seeking").clone()
78                        } else {
79                            fl!("buffering").clone()
80                        }.as_ref(),
81                        set_css_classes: &[adw_css::TITLE_4],
82                    },
83
84                    gtk::Spinner {
85                        #[watch]
86                        set_spinning: overlay.is_visible(),
87                        set_size_request: (20, 20),
88                    },
89                },
90            },
91        }
92    }
93
94    // Initialize the component.
95    fn init(init: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
96        let mut player_builder = PlayerBuilder::new();
97
98        player_builder
99            .seek_accurate(init.seek_accurate)
100            .compositor(init.compositor);
101
102        let player = match player_builder.build(sender.command_sender().clone()) {
103            Ok(player) => {
104                sender
105                    .output_sender()
106                    .send(PlayerComponentOutput::PlayerInitialized(None))
107                    .unwrap_or_default();
108                player.set_qos(init.qos);
109                player.set_max_lateness(&init.max_lateness);
110                Some(player)
111            }
112            Err(error) => {
113                sender
114                    .output_sender()
115                    .send(PlayerComponentOutput::PlayerInitialized(Some(error)))
116                    .unwrap_or_default();
117                None
118            }
119        };
120
121        let model = PlayerComponentModel {
122            player_builder,
123            player,
124            playback_state: PlaybackState::Stopped,
125            show_seeking_overlay: init.show_seeking_overlay,
126            seeking: false,
127            show_drawing_overlay: false,
128            view_data: Rc::new(Mutex::new(ViewData::default())),
129            drag_position: None,
130            mouse_position: None,
131        };
132
133        // Insert the code generation of the view! macro here
134        let widgets = view_output!();
135
136        if let Some(player) = &model.player {
137            widgets.video_picture.set_paintable(Some(player.paintable()).as_ref());
138        }
139
140        {
141            let mut view_data = model.view_data.lock().unwrap();
142            view_data.video_view.set_cursor_widgets(vec![
143                widgets.video_view.clone().upcast(),
144                widgets.drawing_overlay.clone().upcast(),
145                widgets.video_picture.clone().upcast(),
146            ]);
147        }
148
149        if let Some(draw_callback) = init.draw_callback {
150            widgets.drawing_overlay.set_draw_func(clone!(
151                #[weak(rename_to = view_data)]
152                model.view_data,
153                #[strong(rename_to = video_scrolled_window)]
154                widgets.video_scrolled_window,
155                #[strong(rename_to = video_picture)]
156                widgets.video_picture,
157                move |_drawing_area, context, w, h| {
158                    trace!("Drawing func called... w={w} h={h}");
159                    let mut view_data = view_data.lock().unwrap();
160                    if view_data.video_view.drawing_area.is_none() {
161                        view_data.video_view.drawing_area = Some(VideoRectangle::new(0, 0, w, h));
162                    } else if let Some(drawing_area) = &mut view_data.video_view.drawing_area
163                        && (drawing_area.w != w || drawing_area.h != h)
164                    {
165                        drawing_area.w = w;
166                        drawing_area.h = h;
167                    }
168                    view_data
169                        .video_view
170                        .update(None, &video_scrolled_window, &video_picture);
171                    (draw_callback)(context, view_data.video_view.borrow_mut());
172                }
173            ));
174        }
175
176        if let Some(drag_gesture) = init.drag_gesture {
177            widgets.drawing_overlay.add_controller(drag_gesture);
178        }
179
180        if let Some(motion_tracker) = init.motion_tracker {
181            widgets.drawing_overlay.add_controller(motion_tracker);
182        }
183
184        widgets
185            .video_scrolled_window
186            .add_controller(model.new_gesture_drag(sender.clone()));
187
188        widgets
189            .video_scrolled_window
190            .add_controller(model.new_wheel_zoom(sender.clone()));
191
192        widgets
193            .video_scrolled_window
194            .add_controller(model.new_motion_tracker(sender));
195
196        ComponentParts { model, widgets }
197    }
198
199    fn update_with_view(
200        &mut self,
201        widgets: &mut Self::Widgets,
202        msg: Self::Input,
203        sender: ComponentSender<Self>,
204        _: &Self::Root,
205    ) {
206        if let Some(player) = &self.player {
207            match msg {
208                PlayerComponentInput::UpdateUri(uri) => {
209                    player.set_uri(&uri);
210                }
211                PlayerComponentInput::ChangeState(state) => match state {
212                    PlaybackState::Playing => player.play(),
213                    PlaybackState::Paused => player.pause(),
214                    PlaybackState::Stopped => player.stop(),
215                    PlaybackState::Buffering => panic!("Cannot explicitly change playback state to buffering"),
216                    PlaybackState::Error => panic!("Cannot explicitly change playback state to error"),
217                },
218                PlayerComponentInput::SwitchAudioTrack(track) => {
219                    if let Err(error) = player.set_audio_track(track) {
220                        sender.output(PlayerComponentOutput::Error(error)).unwrap_or_default();
221                    }
222                }
223                PlayerComponentInput::Seek(to) => {
224                    self.seeking = true;
225                    player.seek(&to);
226                }
227                PlayerComponentInput::NextFrame => {
228                    player.next_frame();
229                }
230                PlayerComponentInput::SetVolume(vol) => {
231                    player.set_volume(vol);
232                }
233                PlayerComponentInput::SetSpeed(speed) => {
234                    player.set_speed(speed);
235                    sender
236                        .output(PlayerComponentOutput::SpeedChanged(speed))
237                        .unwrap_or_default();
238                }
239                PlayerComponentInput::DumpPipeline(label) => {
240                    player.dump_pipeline(&label);
241                }
242                PlayerComponentInput::SetZoomRelative(scale) => {
243                    trace!("New zoom: {scale}");
244                    let scale = {
245                        let view_data = self.view_data.lock().unwrap();
246                        view_data.video_view.zoom_factor + scale
247                    };
248                    self.set_zoom(
249                        Some(scale),
250                        &mut widgets.video_scrolled_window,
251                        &mut widgets.video_picture,
252                    );
253                    widgets.drawing_overlay.queue_draw();
254                }
255                PlayerComponentInput::SetZoom(scale) => {
256                    self.set_zoom(scale, &mut widgets.video_scrolled_window, &mut widgets.video_picture);
257                    widgets.drawing_overlay.queue_draw();
258                }
259                PlayerComponentInput::SetAudioVideoOffset(offset) => {
260                    self.player_builder.audio_offset(offset);
261                    player.set_audio_video_offset(offset);
262                }
263                PlayerComponentInput::SetSubtitleVideoOffset(offset) => {
264                    self.player_builder.subtitle_offset(offset);
265                    player.set_subtitle_video_offset(offset);
266                }
267                PlayerComponentInput::SetOverlayVisible(visible) => {
268                    self.show_drawing_overlay = visible;
269                    widgets.drawing_overlay.queue_draw();
270                }
271                PlayerComponentInput::RequestOverlayRedraw => widgets.drawing_overlay.queue_draw(),
272                PlayerComponentInput::ReloadPlayer => {
273                    self.player = match self.player_builder.build(sender.command_sender().clone()) {
274                        Ok(player) => {
275                            widgets.video_picture.set_paintable(Some(player.paintable()).as_ref());
276                            Some(player)
277                        }
278                        Err(error) => {
279                            sender.output_sender().emit(PlayerComponentOutput::Error(error));
280                            None
281                        }
282                    };
283                }
284                PlayerComponentInput::PrivateMessage(msg) => match msg {
285                    PrivateMsg::MotionDetected(x, y) => {
286                        self.mouse_position = Some((x, y));
287                    }
288                    PrivateMsg::DragBegin(_, _) => {
289                        // Start the drag position at 0.0, 0.0:
290                        self.drag_position = Some((0.0, 0.0));
291                        let mut view_data = self.view_data.lock().unwrap();
292                        if view_data.video_view.zoom_factor != 1.0 {
293                            view_data.video_view.set_cursor(Some("grabbing"));
294                        }
295                    }
296                    PrivateMsg::DragUpdate(x, y) => {
297                        if let Some((old_x, old_y)) = self.drag_position {
298                            // Calculate offset relative to the last darg_position:
299                            let x_offset = old_x - x;
300                            let y_offset = old_y - y;
301
302                            let ha = widgets.video_scrolled_window.hadjustment();
303                            let va = widgets.video_scrolled_window.vadjustment();
304                            // Update scrolled window position:
305                            ha.set_value(ha.value() + x_offset);
306                            va.set_value(va.value() + y_offset);
307
308                            // Set the current position:
309                            self.drag_position = Some((x, y));
310                        }
311                    }
312                    PrivateMsg::DragEnd(_, _) => {
313                        // Remove drag position:
314                        self.drag_position = None;
315                        if widgets.video_picture.cursor().is_some() {
316                            let mut view_data = self.view_data.lock().unwrap();
317                            if view_data.video_view.zoom_factor != 1.0 {
318                                view_data.video_view.set_cursor(Some("grab"));
319                            } else {
320                                view_data.video_view.set_cursor(None);
321                            }
322                        }
323                    }
324                },
325            }
326        }
327        self.update_view(widgets, sender)
328    }
329
330    fn update_cmd(&mut self, msg: Self::CommandOutput, sender: ComponentSender<Self>, _root: &Self::Root) {
331        match msg {
332            PlayerComponentCommand::VideoDimensionsChanged(width, height) => {
333                if width != 0 && height != 0 {
334                    let mut view_data = self.view_data.lock().unwrap();
335                    let new_dimensions = Some(gst_video::VideoRectangle::new(0, 0, width, height));
336                    if new_dimensions != view_data.video_view.video_dimensions {
337                        view_data.video_view.video_dimensions = new_dimensions;
338                        debug!("video dimensions changed: {width}x{height}");
339                        sender.input(PlayerComponentInput::SetZoom(None));
340                    }
341                }
342            }
343            PlayerComponentCommand::MediaInfoUpdated(info) => {
344                sender
345                    .output(PlayerComponentOutput::MediaInfoUpdated(info))
346                    .unwrap_or_default();
347            }
348            PlayerComponentCommand::DurationChanged(duration) => {
349                sender
350                    .output(PlayerComponentOutput::DurationChanged(duration))
351                    .unwrap_or_default();
352            }
353            PlayerComponentCommand::PositionUpdated(pos) => {
354                sender
355                    .output(PlayerComponentOutput::PositionUpdated(pos))
356                    .unwrap_or_default();
357            }
358            PlayerComponentCommand::SeekDone => {
359                self.seeking = false;
360                sender.output(PlayerComponentOutput::SeekDone).unwrap_or_default();
361            }
362            PlayerComponentCommand::EndOfStream(val) => {
363                sender
364                    .output(PlayerComponentOutput::EndOfStream(val))
365                    .unwrap_or_default();
366            }
367            PlayerComponentCommand::StateChanged(old_state, new_state) => {
368                self.playback_state = new_state;
369                let reset_states = match new_state {
370                    PlaybackState::Stopped => true,
371                    PlaybackState::Paused => false,
372                    PlaybackState::Playing => false,
373                    PlaybackState::Buffering => false,
374                    PlaybackState::Error => true,
375                };
376                if reset_states {
377                    self.seeking = false;
378                }
379                sender.input_sender().emit(PlayerComponentInput::RequestOverlayRedraw);
380                sender
381                    .output(PlayerComponentOutput::StateChanged(old_state, new_state))
382                    .unwrap_or_default();
383            }
384            PlayerComponentCommand::VolumeChanged(vol) => {
385                sender
386                    .output(PlayerComponentOutput::VolumeChanged(vol))
387                    .unwrap_or_default();
388            }
389            PlayerComponentCommand::AudioVideoOffsetChanged(offset) => {
390                sender
391                    .output(PlayerComponentOutput::AudioVideoOffsetChanged(offset))
392                    .unwrap_or_default();
393            }
394            PlayerComponentCommand::SubtitleVideoOffsetChanged(offset) => {
395                sender
396                    .output(PlayerComponentOutput::SubtitleVideoOffsetChanged(offset))
397                    .unwrap_or_default();
398            }
399            PlayerComponentCommand::Warning(error) => {
400                sender.output(PlayerComponentOutput::Warning(error)).unwrap_or_default();
401            }
402            PlayerComponentCommand::Error(error) => {
403                sender.output(PlayerComponentOutput::Error(error)).unwrap_or_default();
404            }
405        }
406    }
407}
408
409impl PlayerComponentModel {
410    pub fn set_qos(&self, qos: bool) {
411        if let Some(player) = &self.player {
412            player.set_qos(qos);
413        } else {
414            debug!("Cannot set QOS no player instance")
415        }
416    }
417
418    pub fn set_max_lateness(&self, max_lateness: &MaxLateness) {
419        if let Some(player) = &self.player {
420            player.set_max_lateness(max_lateness);
421        } else {
422            debug!("Cannot set max lateness no player instance")
423        }
424    }
425
426    fn set_zoom(
427        &mut self,
428        new_scale: Option<f64>,
429        video_scrolled_window: &mut gtk::ScrolledWindow,
430        video_picture: &mut gtk::Picture,
431    ) {
432        let mut view_data = self.view_data.lock().unwrap();
433
434        let old_zoom = view_data.video_view.zoom_factor;
435        let new_scale = new_scale.unwrap_or(1.0).clamp(1.0, 10.0);
436        trace!("New zoom: {new_scale}");
437
438        view_data
439            .video_view
440            .update(Some(new_scale), video_scrolled_window, video_picture);
441
442        if view_data.video_view.zoom_factor == 1.0 {
443            video_picture.set_width_request(0);
444            video_picture.set_height_request(0);
445            view_data.video_view.set_cursor(None);
446        } else {
447            trace!("paintable rectangle: {:?}", view_data.video_view.scaled_paintable_rect);
448            trace!("view rectangle: {:?}", view_data.video_view.view_rect);
449            trace!(
450                "scrolled window: ha_upper={} va_upper={}",
451                video_scrolled_window.hadjustment().upper(),
452                video_scrolled_window.vadjustment().upper()
453            );
454
455            let fitted_paintable_rect = view_data.video_view.fitted_paintable_rect.clone().unwrap();
456
457            trace!("Zoom video to {fitted_paintable_rect:?}");
458
459            view_data.video_view.set_cursor(Some("grab"));
460            video_picture.set_width_request(fitted_paintable_rect.w);
461            video_picture.set_height_request(fitted_paintable_rect.h);
462
463            // Adjust scrolled window viewport to the mouse position:
464            if let Some((x, y)) = self.mouse_position {
465                let ha = video_scrolled_window.hadjustment();
466                let va = video_scrolled_window.vadjustment();
467
468                // Adjust the scrollbar range to the new zoom level.
469                // It is very important to have one step for rescaling and updating
470                // the viewport. If we wait for the upper value of each scrollbar to be
471                // updated, the video image flickers on each zoom.
472                ha.set_upper(fitted_paintable_rect.w as f64);
473                va.set_upper(fitted_paintable_rect.h as f64);
474
475                // Translate the relative pointer position to the actual video image coordinates:
476                let view_point = video_scrolled_window
477                    .compute_point(video_picture, &gtk::graphene::Point::new(x as f32, y as f32))
478                    .expect("Cannot translate x/y");
479                let dst_x = view_point.x() as f64;
480                let dst_y = view_point.y() as f64;
481
482                // Make the pointer position unscaled:
483                let dst_x = dst_x / old_zoom;
484                let dst_y = dst_y / old_zoom;
485
486                // Calculate the new x and y values of the scrolled video view:
487                let new_content_x = ha.value() - (dst_x * old_zoom - dst_x * view_data.video_view.zoom_factor);
488                let new_content_y = va.value() - (dst_y * old_zoom - dst_y * view_data.video_view.zoom_factor);
489
490                trace!("└── move viewport to x={new_content_x} y={new_content_y}");
491
492                // Update scrolled window position:
493                ha.set_value(new_content_x);
494                va.set_value(new_content_y);
495            }
496        }
497    }
498
499    fn new_gesture_drag(&self, sender: ComponentSender<Self>) -> gtk::GestureDrag {
500        let drag = gtk::GestureDrag::builder().button(gtk::gdk::BUTTON_PRIMARY).build();
501
502        drag.connect_drag_begin(clone!(
503            #[strong]
504            sender,
505            move |_, x, y| {
506                trace!("Scrolling: Drag begin x={x} y={y}");
507                sender.input(PlayerComponentInput::PrivateMessage(PrivateMsg::DragBegin(x, y)));
508            }
509        ));
510        drag.connect_drag_update(clone!(
511            #[strong]
512            sender,
513            move |_, x, y| {
514                sender.input(PlayerComponentInput::PrivateMessage(PrivateMsg::DragUpdate(x, y)));
515            }
516        ));
517
518        drag.connect_drag_end(clone!(
519            #[strong]
520            sender,
521            move |_, x, y| {
522                sender.input(PlayerComponentInput::PrivateMessage(PrivateMsg::DragEnd(x, y)));
523            }
524        ));
525
526        drag
527    }
528
529    fn new_motion_tracker(&self, sender: ComponentSender<Self>) -> gtk::EventControllerMotion {
530        let tracker = gtk::EventControllerMotion::builder().build();
531
532        tracker.connect_motion(clone!(
533            #[strong]
534            sender,
535            move |_, x, y| {
536                sender.input(PlayerComponentInput::PrivateMessage(PrivateMsg::MotionDetected(x, y)));
537            }
538        ));
539
540        tracker
541    }
542
543    fn new_wheel_zoom(&self, sender: ComponentSender<Self>) -> gtk::EventControllerScroll {
544        let zoom = gtk::EventControllerScroll::builder()
545            .flags(gtk::EventControllerScrollFlags::VERTICAL)
546            .build();
547
548        zoom.connect_scroll(clone!(
549            #[strong]
550            sender,
551            move |_, _, y| {
552                let scale = (y / 10.0/* smooth scaling */) * SCALE_MULTIPLIER;
553                // Invert scale to get a natural zoom experience:
554                let scale = -scale;
555                sender.input(PlayerComponentInput::SetZoomRelative(scale));
556                gtk::glib::Propagation::Stop
557            }
558        ));
559
560        zoom
561    }
562}