mxl_player_components/ui/player/
widget.rs1use 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_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 = >k::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 = >k::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 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 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 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 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 ha.set_value(ha.value() + x_offset);
306 va.set_value(va.value() + y_offset);
307
308 self.drag_position = Some((x, y));
310 }
311 }
312 PrivateMsg::DragEnd(_, _) => {
313 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 if let Some((x, y)) = self.mouse_position {
465 let ha = video_scrolled_window.hadjustment();
466 let va = video_scrolled_window.vadjustment();
467
468 ha.set_upper(fitted_paintable_rect.w as f64);
473 va.set_upper(fitted_paintable_rect.h as f64);
474
475 let view_point = video_scrolled_window
477 .compute_point(video_picture, >k::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 let dst_x = dst_x / old_zoom;
484 let dst_y = dst_y / old_zoom;
485
486 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 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) * SCALE_MULTIPLIER;
553 let scale = -scale;
555 sender.input(PlayerComponentInput::SetZoomRelative(scale));
556 gtk::glib::Propagation::Stop
557 }
558 ));
559
560 zoom
561 }
562}