1 // player.rs
2 //
3 // Copyright 2018 Jordan Petridis <jpetridis@gnome.org>
4 //
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program. If not, see <http://www.gnu.org/licenses/>.
17 //
18 // SPDX-License-Identifier: GPL-3.0-or-later
19
20 use gst::ClockTime;
21
22 use gtk::prelude::*;
23
24 use libhandy as hdy;
25
26 use gio::File;
27 use glib::clone;
28 use glib::{SignalHandlerId, WeakRef};
29
30 use anyhow::Result;
31 use chrono::{prelude::*, NaiveTime};
32 use fragile::Fragile;
33 use glib::Sender;
34 use url::Url;
35
36 use podcasts_data::{dbqueries, downloader, EpisodeWidgetModel, ShowCoverModel, USER_AGENT};
37
38 use crate::app::Action;
39 use crate::config::APP_ID;
40 use crate::utils::set_image_from_path;
41
42 use std::cell::{RefCell, RefMut};
43 use std::convert::TryInto;
44 use std::ops::Deref;
45 use std::path::Path;
46 use std::rc::Rc;
47 use std::sync::Mutex;
48
49 use crate::i18n::i18n;
50
51 use mpris_player::{Metadata, MprisPlayer, OrgMprisMediaPlayer2Player, PlaybackStatus};
52 use std::sync::Arc;
53
54 #[derive(Debug, Clone, Copy)]
55 enum SeekDirection {
56 Backwards,
57 Forward,
58 }
59
60 trait PlayerExt {
play(&self)61 fn play(&self);
pause(&mut self)62 fn pause(&mut self);
stop(&mut self)63 fn stop(&mut self);
seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()>64 fn seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()>;
fast_forward(&self)65 fn fast_forward(&self);
rewind(&self)66 fn rewind(&self);
set_playback_rate(&self, _: f64)67 fn set_playback_rate(&self, _: f64);
68 }
69
70 #[derive(Debug, Clone)]
71 struct PlayerInfo {
72 container: gtk::Box,
73 show: gtk::Label,
74 episode: gtk::Label,
75 cover: gtk::Image,
76 show_small: gtk::Label,
77 episode_small: gtk::Label,
78 cover_small: gtk::Image,
79 mpris: Arc<MprisPlayer>,
80 finished_restore: bool,
81 ep: Option<EpisodeWidgetModel>,
82 episode_id: RefCell<Option<i32>>,
83 }
84
85 impl PlayerInfo {
create_bindings(&self)86 fn create_bindings(&self) {
87 self.show
88 .bind_property("label", &self.show_small, "label")
89 .flags(glib::BindingFlags::SYNC_CREATE)
90 .build();
91 self.episode
92 .bind_property("label", &self.episode_small, "label")
93 .flags(glib::BindingFlags::SYNC_CREATE)
94 .build();
95 self.cover
96 .bind_property("pixbuf", &self.cover_small, "pixbuf")
97 .flags(glib::BindingFlags::SYNC_CREATE)
98 .build();
99 }
100
101 // FIXME: create a Diesel Model of the joined episode and podcast query instead
init(&mut self, episode: &EpisodeWidgetModel, podcast: &ShowCoverModel)102 fn init(&mut self, episode: &EpisodeWidgetModel, podcast: &ShowCoverModel) {
103 self.ep = Some(episode.clone());
104 self.episode_id.replace(Some(episode.rowid()));
105 self.set_cover_image(podcast);
106 self.set_show_title(podcast);
107 self.set_episode_title(episode);
108
109 let mut metadata = Metadata::new();
110 metadata.artist = Some(vec![podcast.title().to_string()]);
111 metadata.title = Some(episode.title().to_string());
112
113 // Set the cover if it is already cached.
114 if let Ok(path) = downloader::cache_image(&podcast, false) {
115 let url = Url::from_file_path(path);
116 metadata.art_url = url.map(From::from).ok();
117 } else {
118 // fallback: set the cover to the http url if it isn't cached, yet.
119 // TODO we could trigger an async download of the cover here
120 // and update the metadata when it's done.
121 metadata.art_url = podcast.image_uri().clone().map(From::from);
122 }
123
124 self.mpris.set_metadata(metadata);
125 self.mpris.set_can_play(true);
126 }
127
set_episode_title(&self, episode: &EpisodeWidgetModel)128 fn set_episode_title(&self, episode: &EpisodeWidgetModel) {
129 self.episode.set_text(episode.title());
130 self.episode.set_tooltip_text(Some(episode.title()));
131 }
132
set_show_title(&self, show: &ShowCoverModel)133 fn set_show_title(&self, show: &ShowCoverModel) {
134 self.show.set_text(show.title());
135 self.show.set_tooltip_text(Some(show.title()));
136 }
137
set_cover_image(&self, show: &ShowCoverModel)138 fn set_cover_image(&self, show: &ShowCoverModel) {
139 set_image_from_path(&self.cover, show.id(), 34)
140 .map_err(|err| error!("Player Cover: {}", err))
141 .ok();
142 }
143 }
144
145 #[derive(Debug, Clone)]
146 struct PlayerTimes {
147 container: gtk::Box,
148 progressed: gtk::Label,
149 duration: gtk::Label,
150 separator: gtk::Label,
151 slider: gtk::Scale,
152 slider_update: Rc<SignalHandlerId>,
153 progress_bar: gtk::ProgressBar,
154 }
155
156 #[derive(Debug, Clone, Copy)]
157 struct Duration(ClockTime);
158
159 impl Deref for Duration {
160 type Target = ClockTime;
deref(&self) -> &Self::Target161 fn deref(&self) -> &Self::Target {
162 &self.0
163 }
164 }
165
166 #[derive(Debug, Clone, Copy)]
167 struct Position(ClockTime);
168
169 impl Deref for Position {
170 type Target = ClockTime;
deref(&self) -> &Self::Target171 fn deref(&self) -> &Self::Target {
172 &self.0
173 }
174 }
175
176 impl PlayerTimes {
177 /// Update the duration `gtk::Label` and the max range of the `gtk::SclaeBar`.
on_duration_changed(&self, duration: Duration)178 pub(crate) fn on_duration_changed(&self, duration: Duration) {
179 let seconds = duration.seconds();
180
181 self.slider.block_signal(&self.slider_update);
182 self.slider.set_range(0.0, seconds as f64);
183 self.slider.unblock_signal(&self.slider_update);
184
185 self.duration.set_text(&format_duration(seconds as u32));
186
187 self.update_progress_bar();
188 }
189
190 /// Update the `gtk::Scale` bar when the pipeline position is changed.
on_position_updated(&self, position: Position)191 pub(crate) fn on_position_updated(&self, position: Position) {
192 let seconds = position.seconds();
193
194 self.slider.block_signal(&self.slider_update);
195 self.slider.set_value(seconds as f64);
196 self.slider.unblock_signal(&self.slider_update);
197
198 self.progressed.set_text(&format_duration(seconds as u32));
199
200 self.update_progress_bar();
201 }
202
update_progress_bar(&self)203 fn update_progress_bar(&self) {
204 let fraction = self.slider.value() / self.slider.adjustment().upper();
205 self.progress_bar.set_fraction(fraction);
206 }
207 }
208
format_duration(seconds: u32) -> String209 fn format_duration(seconds: u32) -> String {
210 let time = NaiveTime::from_num_seconds_from_midnight(seconds, 0);
211
212 if seconds >= 3600 {
213 time.format("%T").to_string()
214 } else {
215 time.format("%M∶%S").to_string()
216 }
217 }
218
219 #[derive(Debug, Clone)]
220 struct PlayerRate {
221 action: gio::SimpleAction,
222 btn: gtk::MenuButton,
223 label: gtk::Label,
224 }
225
226 impl PlayerRate {
new() -> Self227 fn new() -> Self {
228 let builder = gtk::Builder::from_resource("/org/gnome/Podcasts/gtk/player_rate.ui");
229
230 // This needs to be a string to work with GMenuModel
231 let variant_type = glib::VariantTy::new("s").expect("Could not parse variant type");
232 let action =
233 gio::SimpleAction::new_stateful("set", Some(&variant_type), &"1.00".to_variant());
234 let btn: gtk::MenuButton = builder.object("rate_button").unwrap();
235 let label = builder.object("rate_label").unwrap();
236
237 PlayerRate { action, label, btn }
238 }
239
connect_signals(&self, widget: &Rc<RefCell<PlayerWidget>>)240 fn connect_signals(&self, widget: &Rc<RefCell<PlayerWidget>>) {
241 let group = gio::SimpleActionGroup::new();
242 self.action
243 .connect_activate(clone!(@weak widget => move |action, rate_v| {
244 let variant = rate_v.unwrap();
245 action.set_state(&variant);
246 let rate = variant
247 .get::<String>()
248 .expect("Could not get rate from variant")
249 .parse::<f64>()
250 .expect("Could not parse float from variant string");
251 widget.borrow().on_rate_changed(rate);
252 }));
253 group.add_action(&self.action);
254 widget
255 .borrow()
256 .container
257 .insert_action_group("rate", Some(&group));
258 widget
259 .borrow()
260 .dialog
261 .dialog
262 .insert_action_group("rate", Some(&group));
263 }
264 }
265
266 #[derive(Debug, Clone)]
267 struct PlayerControls {
268 container: gtk::Box,
269 play: gtk::Button,
270 pause: gtk::Button,
271 play_small: gtk::Button,
272 pause_small: gtk::Button,
273 play_pause_small: gtk::Stack,
274 forward: gtk::Button,
275 rewind: gtk::Button,
276 last_pause: RefCell<Option<DateTime<Local>>>,
277 }
278
279 #[derive(Debug, Clone)]
280 struct PlayerDialog {
281 dialog: gtk::Dialog,
282 close: gtk::Button,
283 headerbar: hdy::HeaderBar,
284 cover: gtk::Image,
285 play_pause: gtk::Stack,
286 play: gtk::Button,
287 pause: gtk::Button,
288 duration: gtk::Label,
289 progressed: gtk::Label,
290 slider: gtk::Scale,
291 forward: gtk::Button,
292 rewind: gtk::Button,
293 rate: PlayerRate,
294 show: gtk::Label,
295 episode: gtk::Label,
296 }
297
298 impl PlayerDialog {
new(rate: PlayerRate) -> Self299 fn new(rate: PlayerRate) -> Self {
300 let builder = gtk::Builder::from_resource("/org/gnome/Podcasts/gtk/player_dialog.ui");
301 let dialog = builder.object("dialog").unwrap();
302
303 let close = builder.object("close").unwrap();
304 let headerbar = builder.object("headerbar").unwrap();
305 let cover = builder.object("cover").unwrap();
306 let play_pause = builder.object("play_pause").unwrap();
307 let play = builder.object("play").unwrap();
308 let pause = builder.object("pause").unwrap();
309 let duration = builder.object("duration").unwrap();
310 let progressed = builder.object("progressed").unwrap();
311 let slider = builder.object("slider").unwrap();
312 let rewind = builder.object("rewind").unwrap();
313 let forward = builder.object("forward").unwrap();
314 let bottom: gtk::Box = builder.object("bottom").unwrap();
315 let show = builder.object("show_label").unwrap();
316 let episode = builder.object("episode_label").unwrap();
317
318 bottom.pack_start(&rate.btn, false, true, 0);
319
320 PlayerDialog {
321 dialog,
322 close,
323 headerbar,
324 cover,
325 play_pause,
326 play,
327 pause,
328 duration,
329 progressed,
330 slider,
331 forward,
332 rewind,
333 rate,
334 show,
335 episode,
336 }
337 }
338
initialize_episode(&self, episode: &EpisodeWidgetModel, show: &ShowCoverModel)339 fn initialize_episode(&self, episode: &EpisodeWidgetModel, show: &ShowCoverModel) {
340 self.episode.set_text(episode.title());
341 self.show.set_text(show.title());
342
343 set_image_from_path(&self.cover, show.id(), 256)
344 .map_err(|err| error!("Player Cover: {}", err))
345 .ok();
346 }
347 }
348
349 #[derive(Debug, Clone)]
350 pub(crate) struct PlayerWidget {
351 pub(crate) container: gtk::Box,
352 action_bar: gtk::ActionBar,
353 evbox: gtk::EventBox,
354 player: gst_player::Player,
355 controls: PlayerControls,
356 dialog: PlayerDialog,
357 full: gtk::Box,
358 squeezer: hdy::Squeezer,
359 timer: PlayerTimes,
360 info: PlayerInfo,
361 rate: PlayerRate,
362 sender: Option<Sender<Action>>,
363 }
364
365 impl Default for PlayerWidget {
default() -> Self366 fn default() -> Self {
367 let dispatcher = gst_player::PlayerGMainContextSignalDispatcher::new(None);
368 let player = gst_player::Player::new(
369 None,
370 // Use the gtk main thread
371 Some(&dispatcher.upcast::<gst_player::PlayerSignalDispatcher>()),
372 );
373
374 // A few podcasts have a video track of the thumbnail, which GStreamer displays in a new
375 // window. Make sure it doesn't do that.
376 player.set_video_track_enabled(false);
377
378 let mpris = MprisPlayer::new(
379 APP_ID.to_string(),
380 "GNOME Podcasts".to_string(),
381 format!("{}.desktop", APP_ID),
382 );
383 mpris.set_can_raise(true);
384 mpris.set_can_play(false);
385 mpris.set_can_seek(false);
386 mpris.set_can_set_fullscreen(false);
387
388 let mut config = player.config();
389 config.set_user_agent(USER_AGENT);
390 config.set_position_update_interval(250);
391 player.set_config(config).unwrap();
392
393 let builder = gtk::Builder::from_resource("/org/gnome/Podcasts/gtk/player_toolbar.ui");
394
395 let buttons = builder.object("buttons").unwrap();
396 let play = builder.object("play_button").unwrap();
397 let pause = builder.object("pause_button").unwrap();
398 let play_small = builder.object("play_button_small").unwrap();
399 let pause_small = builder.object("pause_button_small").unwrap();
400 let forward: gtk::Button = builder.object("ff_button").unwrap();
401 let rewind: gtk::Button = builder.object("rewind_button").unwrap();
402 let play_pause_small = builder.object("play_pause_small").unwrap();
403
404 let controls = PlayerControls {
405 container: buttons,
406 play,
407 pause,
408 play_small,
409 pause_small,
410 play_pause_small,
411 forward,
412 rewind,
413 last_pause: RefCell::new(None),
414 };
415
416 let timer_container = builder.object("timer").unwrap();
417 let progressed = builder.object("progress_time_label").unwrap();
418 let duration = builder.object("total_duration_label").unwrap();
419 let separator = builder.object("separator").unwrap();
420 let slider: gtk::Scale = builder.object("seek").unwrap();
421 slider.set_range(0.0, 1.0);
422 let player_weak = player.downgrade();
423 let slider_update = Rc::new(Self::connect_update_slider(&slider, player_weak));
424 let progress_bar = builder.object("progress_bar").unwrap();
425 let timer = PlayerTimes {
426 container: timer_container,
427 progressed,
428 duration,
429 separator,
430 slider,
431 slider_update,
432 progress_bar,
433 };
434
435 let labels = builder.object("info").unwrap();
436 let show = builder.object("show_label").unwrap();
437 let episode = builder.object("episode_label").unwrap();
438 let cover = builder.object("show_cover").unwrap();
439 let show_small = builder.object("show_label_small").unwrap();
440 let episode_small = builder.object("episode_label_small").unwrap();
441 let cover_small = builder.object("show_cover_small").unwrap();
442 let ep = None;
443 let info = PlayerInfo {
444 mpris,
445 container: labels,
446 show,
447 ep,
448 episode,
449 cover,
450 show_small,
451 episode_small,
452 cover_small,
453 finished_restore: false,
454 episode_id: RefCell::new(None),
455 };
456 info.create_bindings();
457
458 let dialog_rate = PlayerRate::new();
459 let dialog = PlayerDialog::new(dialog_rate);
460
461 let container = builder.object("container").unwrap();
462 let action_bar: gtk::ActionBar = builder.object("action_bar").unwrap();
463 let evbox = builder.object("evbox").unwrap();
464 let full: gtk::Box = builder.object("full").unwrap();
465 let squeezer = builder.object("squeezer").unwrap();
466
467 let rate = PlayerRate::new();
468 full.pack_end(&rate.btn, false, true, 0);
469
470 PlayerWidget {
471 player,
472 container,
473 action_bar,
474 evbox,
475 controls,
476 dialog,
477 full,
478 squeezer,
479 timer,
480 info,
481 rate,
482 sender: None,
483 }
484 }
485 }
486
487 impl PlayerWidget {
on_rate_changed(&self, rate: f64)488 fn on_rate_changed(&self, rate: f64) {
489 self.set_playback_rate(rate);
490 self.rate.label.set_text(&format!("{:.2}×", rate));
491 self.dialog.rate.label.set_text(&format!("{:.2}×", rate));
492 }
493
reveal(&self)494 fn reveal(&self) {
495 self.action_bar.show();
496 }
497
initialize_episode(&mut self, rowid: i32) -> Result<()>498 pub(crate) fn initialize_episode(&mut self, rowid: i32) -> Result<()> {
499 let ep = dbqueries::get_episode_widget_from_rowid(rowid)?;
500 let pd = dbqueries::get_podcast_cover_from_id(ep.show_id())?;
501
502 self.dialog.initialize_episode(&ep, &pd);
503
504 self.info.finished_restore = false;
505 self.info.init(&ep, &pd);
506
507 // Currently that will always be the case since the play button is
508 // only shown if the file is downloaded
509 if let Some(ref path) = ep.local_uri() {
510 if Path::new(path).exists() {
511 // path is an absolute fs path ex. "foo/bar/baz".
512 // Convert it so it will have a "file:///"
513 // FIXME: convert it properly
514 let uri = File::for_path(path).uri();
515
516 // If it's not the same file load the uri, otherwise just unpause
517 if self.player.uri().map_or(true, |s| s != uri.as_str()) {
518 self.player.set_uri(uri.as_str());
519 } else {
520 // just unpause, no restore required
521 self.info.finished_restore = true;
522 }
523 // play the file
524 self.play();
525
526 return Ok(());
527 }
528 // TODO: log an error
529 }
530
531 // FIXME: Stream stuff
532 // unimplemented!()
533 Ok(())
534 }
535
connect_update_slider( slider: >k::Scale, player: WeakRef<gst_player::Player>, ) -> SignalHandlerId536 fn connect_update_slider(
537 slider: >k::Scale,
538 player: WeakRef<gst_player::Player>,
539 ) -> SignalHandlerId {
540 slider.connect_value_changed(move |slider| {
541 let player = match player.upgrade() {
542 Some(p) => p,
543 None => return,
544 };
545
546 let value = slider.value() as u64;
547 player.seek(ClockTime::from_seconds(value));
548 })
549 }
550
smart_rewind(&self) -> Option<()>551 fn smart_rewind(&self) -> Option<()> {
552 lazy_static! {
553 static ref LAST_KNOWN_EPISODE: Mutex<Option<i32>> = Mutex::new(None);
554 };
555
556 // Figure out the time delta, in seconds, between the last pause and now
557 let now = Local::now();
558 let last: &Option<DateTime<_>> = &*self.controls.last_pause.borrow();
559 let delta = (now - (*last)?).num_seconds();
560
561 // Get interval passed in the gst stream
562 let seconds_passed = self.player.position()?.seconds();
563 // get the last known episode id
564 let mut last = LAST_KNOWN_EPISODE.lock().unwrap();
565 // get the current playing episode id
566 let current_id = *self.info.episode_id.borrow();
567 // Only rewind on pause if the stream position is passed a certain point,
568 // and the player has been paused for more than a minute,
569 // and the episode id is the same
570 if seconds_passed >= 90 && delta >= 60 && current_id == *last {
571 self.seek(ClockTime::from_seconds(5), SeekDirection::Backwards);
572 }
573
574 // Set the last knows episode to the current one
575 *last = current_id;
576
577 Some(())
578 }
579
580 /// Seek to the `play_position` stored in the episode.
581 /// Returns Some(()) if the restore was successful and None otherwise.
restore_play_position(&self) -> Option<()>582 fn restore_play_position(&self) -> Option<()> {
583 let ep = self.info.ep.as_ref()?;
584 let pos = ep.play_position();
585 let s: u64 = pos.try_into().ok()?;
586 if pos != 0 {
587 self.player.seek(ClockTime::from_seconds(s));
588 Some(())
589 } else {
590 None
591 }
592 }
593 }
594
595 impl PlayerExt for PlayerWidget {
play(&self)596 fn play(&self) {
597 self.dialog.play_pause.set_visible_child(&self.dialog.pause);
598
599 self.reveal();
600
601 self.controls.pause.show();
602 self.controls.play.hide();
603 self.controls
604 .play_pause_small
605 .set_visible_child(&self.controls.pause_small);
606
607 self.smart_rewind();
608 self.player.play();
609 self.info.mpris.set_playback_status(PlaybackStatus::Playing);
610 if let Some(sender) = &self.sender {
611 send!(sender, Action::InhibitSuspend);
612 }
613 }
614
pause(&mut self)615 fn pause(&mut self) {
616 self.dialog.play_pause.set_visible_child(&self.dialog.play);
617
618 self.controls.pause.hide();
619 self.controls.play.show();
620 self.controls
621 .play_pause_small
622 .set_visible_child(&self.controls.play_small);
623
624 self.player.pause();
625 self.info.mpris.set_playback_status(PlaybackStatus::Paused);
626 if let Some(sender) = &self.sender {
627 send!(sender, Action::UninhibitSuspend);
628 }
629
630 self.controls.last_pause.replace(Some(Local::now()));
631 let pos = self.player.position();
632 self.info.ep.as_mut().map(|ep| {
633 ep.set_play_position(pos.and_then(|s| s.seconds().try_into().ok()).unwrap_or(0))
634 });
635 }
636
stop(&mut self)637 fn stop(&mut self) {
638 self.controls.pause.hide();
639 self.controls.play.show();
640
641 self.info.ep = None;
642 self.player.stop();
643 self.info.mpris.set_playback_status(PlaybackStatus::Paused);
644
645 // Reset the slider bar to the start
646
647 self.timer
648 .on_position_updated(Position(ClockTime::from_seconds(0)));
649 if let Some(sender) = &self.sender {
650 send!(sender, Action::UninhibitSuspend);
651 }
652 }
653
654 // Adapted from https://github.com/philn/glide/blob/b52a65d99daeab0b487f79a0e1ccfad0cd433e22/src/player_context.rs#L219-L245
seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()>655 fn seek(&self, offset: ClockTime, direction: SeekDirection) -> Option<()> {
656 // How far into the podcast we are
657 let position = self.player.position()?;
658 if offset.is_zero() {
659 return Some(());
660 }
661
662 // How much podcast we have
663 let duration = self.player.duration()?;
664 let destination = match direction {
665 // If we are more than `offset` into the podcast, jump back that far
666 SeekDirection::Backwards if position >= offset => position.checked_sub(offset),
667 // If we haven't played `offset` yet just restart the podcast
668 SeekDirection::Backwards if position < offset => Some(ClockTime::from_seconds(0)),
669 // If we have more than `offset` remaining jump forward they amount
670 SeekDirection::Forward if !duration.is_zero() && position + offset <= duration => {
671 position.checked_add(offset)
672 }
673 // We don't have `offset` remaining just move to the end (ending playback)
674 SeekDirection::Forward if !duration.is_zero() && position + offset > duration => {
675 Some(duration)
676 }
677 // Who knows what's going on ¯\_(ツ)_/¯
678 _ => None,
679 };
680
681 // If we calucated a new position, jump to it
682 if let Some(destination) = destination {
683 self.player.seek(destination)
684 }
685
686 Some(())
687 }
688
rewind(&self)689 fn rewind(&self) {
690 let r = self.seek(ClockTime::from_seconds(10), SeekDirection::Backwards);
691 if r.is_none() {
692 warn!("Failed to rewind");
693 }
694 }
695
fast_forward(&self)696 fn fast_forward(&self) {
697 let r = self.seek(ClockTime::from_seconds(10), SeekDirection::Forward);
698 if r.is_none() {
699 warn!("Failed to fast-forward");
700 }
701 }
702
set_playback_rate(&self, rate: f64)703 fn set_playback_rate(&self, rate: f64) {
704 self.player.set_rate(rate);
705 }
706 }
707
708 #[derive(Debug, Clone)]
709 pub(crate) struct PlayerWrapper(pub Rc<RefCell<PlayerWidget>>);
710
711 impl Default for PlayerWrapper {
default() -> Self712 fn default() -> Self {
713 PlayerWrapper(Rc::new(RefCell::new(PlayerWidget::default())))
714 }
715 }
716
717 impl Deref for PlayerWrapper {
718 type Target = Rc<RefCell<PlayerWidget>>;
deref(&self) -> &Self::Target719 fn deref(&self) -> &Self::Target {
720 &self.0
721 }
722 }
723
724 impl PlayerWrapper {
borrow_mut(&self) -> RefMut<'_, PlayerWidget>725 pub(crate) fn borrow_mut(&self) -> RefMut<'_, PlayerWidget> {
726 self.0.borrow_mut()
727 }
new(sender: &Sender<Action>) -> Self728 pub(crate) fn new(sender: &Sender<Action>) -> Self {
729 let w = PlayerWrapper::default();
730 w.init(sender);
731 w
732 }
733
init(&self, sender: &Sender<Action>)734 fn init(&self, sender: &Sender<Action>) {
735 self.borrow_mut().sender = Some(sender.clone());
736 self.connect_control_buttons();
737 self.connect_rate_buttons();
738 self.connect_mpris_buttons(sender);
739 self.connect_gst_signals(sender);
740 self.connect_dialog();
741 }
742
connect_dialog(&self)743 fn connect_dialog(&self) {
744 let this = self.deref();
745 let widget = self.borrow();
746 widget
747 .squeezer
748 .connect_visible_child_notify(clone!(@weak this => move |_| {
749 let widget = this.borrow();
750 if let Some(child) = widget.squeezer.visible_child() {
751 let full = child == this.borrow().full;
752 this.borrow().timer.progress_bar.set_visible(!full);
753 if full {
754 this.borrow().action_bar.style_context().remove_class("player-small");
755 } else {
756 this.borrow().action_bar.style_context().add_class("player-small");
757 }
758 }
759 }));
760
761 widget
762 .timer
763 .duration
764 .bind_property("label", &widget.dialog.duration, "label")
765 .flags(glib::BindingFlags::SYNC_CREATE)
766 .build();
767 widget
768 .timer
769 .progressed
770 .bind_property("label", &widget.dialog.progressed, "label")
771 .flags(glib::BindingFlags::SYNC_CREATE)
772 .build();
773 widget
774 .dialog
775 .slider
776 .set_adjustment(&widget.timer.slider.adjustment());
777
778 widget.evbox.connect_button_press_event(
779 clone!(@weak this => @default-return Inhibit(false), move |_, event| {
780 let widget = this.borrow();
781 if event.button() != 1 {
782 return Inhibit(false);
783 }
784 // only open the dialog when the small toolbar is visible
785 if let Some(child) = widget.squeezer.visible_child() {
786 if child == widget.full {
787 return Inhibit(false);
788 }
789 }
790
791 let parent = widget.container.toplevel().and_then(|toplevel| {
792 toplevel
793 .downcast::<gtk::Window>()
794 .ok()
795 }).unwrap();
796
797 info!("showing dialog");
798 widget.dialog.dialog.set_transient_for(Some(&parent));
799 widget.dialog.dialog.show();
800
801 Inhibit(false)
802 }),
803 );
804
805 widget
806 .dialog
807 .close
808 .connect_clicked(clone!(@weak this => move |_| {
809 this.borrow().dialog.dialog.hide();
810 }));
811 }
812
813 /// Connect the `PlayerControls` buttons to the `PlayerExt` methods.
connect_control_buttons(&self)814 fn connect_control_buttons(&self) {
815 let this = self.deref();
816 let widget = self.borrow();
817 // Connect the play button to the gst Player.
818 widget
819 .controls
820 .play
821 .connect_clicked(clone!(@weak this => move |_| {
822 this.borrow().play();
823 }));
824
825 // Connect the pause button to the gst Player.
826 widget
827 .controls
828 .pause
829 .connect_clicked(clone!(@weak this => move |_| {
830 this.borrow_mut().pause();
831 }));
832
833 // Connect the play button to the gst Player.
834 widget
835 .controls
836 .play_small
837 .connect_clicked(clone!(@weak this => move |_| {
838 this.borrow().play();
839 }));
840
841 // Connect the pause button to the gst Player.
842 widget
843 .controls
844 .pause_small
845 .connect_clicked(clone!(@weak this => move |_| {
846 this.borrow_mut().pause();
847 }));
848
849 // Connect the rewind button to the gst Player.
850 widget
851 .controls
852 .rewind
853 .connect_clicked(clone!(@weak this => move |_| {
854 this.borrow().rewind();
855 }));
856
857 // Connect the fast-forward button to the gst Player.
858 widget
859 .controls
860 .forward
861 .connect_clicked(clone!(@weak this => move |_| {
862 this.borrow().fast_forward();
863 }));
864
865 // Connect the play button to the gst Player.
866 widget
867 .dialog
868 .play
869 .connect_clicked(clone!(@weak this => move |_| {
870 this.borrow().play();
871 }));
872
873 // Connect the pause button to the gst Player.
874 widget
875 .dialog
876 .pause
877 .connect_clicked(clone!(@weak this => move |_| {
878 this.borrow_mut().pause();
879 }));
880
881 // Connect the rewind button to the gst Player.
882 widget
883 .dialog
884 .rewind
885 .connect_clicked(clone!(@weak this => move |_| {
886 this.borrow().rewind();
887 }));
888
889 // Connect the fast-forward button to the gst Player.
890 widget
891 .dialog
892 .forward
893 .connect_clicked(clone!(@weak this => move |_| {
894 this.borrow().fast_forward();
895 }));
896 }
897
connect_gst_signals(&self, sender: &Sender<Action>)898 fn connect_gst_signals(&self, sender: &Sender<Action>) {
899 let widget = self.borrow();
900 // Log gst warnings.
901 widget
902 .player
903 .connect_warning(move |_, warn| warn!("gst warning: {}", warn));
904
905 // Log gst errors.
906 widget
907 .player
908 .connect_error(clone!(@strong sender => move |_, _error| {
909 send!(sender, Action::ErrorNotification(format!("Player Error: {}", _error)));
910 let s = i18n("The media player was unable to execute an action.");
911 send!(sender, Action::ErrorNotification(s));
912 }));
913
914 // The following callbacks require `Send` but are handled by the gtk main loop
915 let weak = Fragile::new(Rc::downgrade(self));
916
917 widget
918 .player
919 .connect_uri_loaded(clone!(@strong weak => move |_, _| {
920 if let Some(player_widget) = weak.get().upgrade() {
921 player_widget.borrow().restore_play_position();
922 player_widget.borrow_mut().info.finished_restore = true;
923 }
924 }));
925
926 // Update the duration label and the slider
927 widget
928 .player
929 .connect_duration_changed(clone!(@strong weak => move |_, clock| {
930 if let Some(player_widget) = weak.get().upgrade() {
931 if let Some(c) = clock {
932 player_widget.borrow().timer.on_duration_changed(Duration(c));
933 }
934 }
935 }));
936
937 // Update the position label and the slider
938 widget
939 .player
940 .connect_position_updated(clone!(@strong weak => move |_, clock| {
941 if let Some(player_widget) = weak.get().upgrade() {
942 // write to db
943 if let Some(c) = clock {
944 let pos = Position(c);
945 let finished_restore = player_widget.borrow().info.finished_restore;
946 player_widget.borrow_mut().info.ep.as_mut().map(|ep| {
947 if finished_restore {
948 ep.set_play_position_if_divergent(pos.seconds() as i32)
949 } else {
950 Ok(())
951 }
952 });
953 player_widget.borrow().timer.on_position_updated(pos)
954 }
955 }
956 }));
957
958 // Reset the slider to 0 and show a play button
959 widget
960 .player
961 .connect_end_of_stream(clone!(@strong sender, @strong weak => move |_| {
962 if let Some(player_widget) = weak.get().upgrade() {
963 // write postion to db
964 player_widget.borrow_mut().info.ep.as_mut().map(|ep| {
965 ep.set_play_position(0)?;
966 ep.set_played_now()?;
967 send!(sender, Action::RefreshEpisodesViewBGR);
968 send!(sender, Action::RefreshWidgetIfSame(ep.show_id()));
969 let ok : Result<(), podcasts_data::errors::DataError> = Ok(());
970 ok
971 });
972
973 player_widget.borrow_mut().stop()
974 }
975 }));
976 }
977
connect_rate_buttons(&self)978 fn connect_rate_buttons(&self) {
979 self.deref().borrow().rate.connect_signals(self.deref());
980 self.deref()
981 .borrow()
982 .dialog
983 .rate
984 .connect_signals(self.deref());
985 }
986
connect_mpris_buttons(&self, sender: &Sender<Action>)987 fn connect_mpris_buttons(&self, sender: &Sender<Action>) {
988 let weak = Rc::downgrade(self);
989 let widget = self.borrow();
990
991 // FIXME: Reference cycle with mpris
992 let mpris = widget.info.mpris.clone();
993 widget
994 .info
995 .mpris
996 .connect_play_pause(clone!(@strong weak => move || {
997 let player = match weak.upgrade() {
998 Some(s) => s,
999 None => return
1000 };
1001
1002 if let Ok(status) = mpris.get_playback_status() {
1003 match status.as_ref() {
1004 "Paused" => player.borrow().play(),
1005 "Stopped" => player.borrow().play(),
1006 _ => player.borrow_mut().pause(),
1007 };
1008 }
1009 }));
1010 widget
1011 .info
1012 .mpris
1013 .connect_play(clone!(@strong weak => move || {
1014 let player = match weak.upgrade() {
1015 Some(s) => s,
1016 None => return
1017 };
1018
1019 player.borrow().play();
1020 }));
1021
1022 widget
1023 .info
1024 .mpris
1025 .connect_pause(clone!(@strong weak => move || {
1026 let player = match weak.upgrade() {
1027 Some(s) => s,
1028 None => return
1029 };
1030
1031 player.borrow_mut().pause();
1032 }));
1033
1034 widget
1035 .info
1036 .mpris
1037 .connect_next(clone!(@strong weak => move || {
1038 if let Some(p) = weak.upgrade() {
1039 p.borrow().fast_forward()
1040 }
1041 }));
1042
1043 widget
1044 .info
1045 .mpris
1046 .connect_previous(clone!(@strong weak => move || {
1047 if let Some(p) = weak.upgrade() {
1048 p.borrow().rewind()
1049 }
1050 }));
1051
1052 widget
1053 .info
1054 .mpris
1055 .connect_raise(clone!(@strong sender => move || {
1056 send!(sender, Action::RaiseWindow);
1057 }));
1058 }
1059 }
1060