1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
use adw::subclass::prelude::*;
use gst::{bus::BusWatchGuard, ClockTime};
use gst_play::{Play as GstPlay, PlayMessage};
use gtk::{gio, glib, glib::clone, prelude::*, CompositeTemplate};
use tracing::{error, warn};

use super::video_player_renderer::VideoPlayerRenderer;

mod imp {
    use std::cell::{Cell, OnceCell, RefCell};

    use glib::subclass::InitializingObject;

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
    #[template(resource = "/org/gnome/Fractal/ui/components/media/video_player.ui")]
    #[properties(wrapper_type = super::VideoPlayer)]
    pub struct VideoPlayer {
        /// Whether this player should be displayed in a compact format.
        #[property(get, set = Self::set_compact, explicit_notify)]
        pub compact: Cell<bool>,
        pub duration_handler: RefCell<Option<glib::SignalHandlerId>>,
        #[template_child]
        pub video: TemplateChild<gtk::Picture>,
        #[template_child]
        pub timestamp: TemplateChild<gtk::Label>,
        /// The [`GstPlay`] for the video.
        #[template_child]
        #[property(get = Self::player, type = GstPlay)]
        pub player: TemplateChild<GstPlay>,
        pub bus_guard: OnceCell<BusWatchGuard>,
        /// The file that is currently played.
        pub file: RefCell<Option<gio::File>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for VideoPlayer {
        const NAME: &'static str = "VideoPlayer";
        type Type = super::VideoPlayer;
        type ParentType = adw::Bin;

        fn class_init(klass: &mut Self::Class) {
            VideoPlayerRenderer::ensure_type();

            Self::bind_template(klass);
        }

        fn instance_init(obj: &InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for VideoPlayer {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            let bus_guard = self
                .player
                .message_bus()
                .add_watch_local(clone!(
                    #[weak]
                    obj,
                    #[upgrade_or]
                    glib::ControlFlow::Break,
                    move |_, message| {
                        match PlayMessage::parse(message) {
                            Ok(PlayMessage::DurationChanged { duration }) => {
                                obj.duration_changed(duration)
                            }
                            Ok(PlayMessage::Warning { error, .. }) => {
                                warn!("Warning playing video: {error}");
                            }
                            Ok(PlayMessage::Error { error, .. }) => {
                                error!("Error playing video: {error}");
                            }
                            _ => {}
                        }

                        glib::ControlFlow::Continue
                    }
                ))
                .unwrap();
            self.bus_guard.set(bus_guard).unwrap();
        }

        fn dispose(&self) {
            self.player.message_bus().set_flushing(true);
        }
    }

    impl WidgetImpl for VideoPlayer {
        fn map(&self) {
            self.parent_map();
            self.player.play();
        }

        fn unmap(&self) {
            self.player.stop();
            self.parent_unmap();
        }
    }

    impl BinImpl for VideoPlayer {}

    impl VideoPlayer {
        fn player(&self) -> GstPlay {
            self.player.clone()
        }
        /// Set whether this player should be displayed in a compact format.
        fn set_compact(&self, compact: bool) {
            if self.compact.get() == compact {
                return;
            }

            self.compact.set(compact);
            self.obj().notify_compact();
        }
    }
}

glib::wrapper! {
    /// A widget to preview a video media file without controls or sound.
    pub struct VideoPlayer(ObjectSubclass<imp::VideoPlayer>)
        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}

impl VideoPlayer {
    /// Create a new video player.
    pub fn new() -> Self {
        glib::Object::new()
    }

    /// Set the file to display.
    pub fn play_media_file(&self, file: gio::File) {
        self.imp().file.replace(Some(file.clone()));
        self.duration_changed(None);
        let player = self.player();
        player.set_uri(Some(file.uri().as_ref()));
        player.set_audio_track_enabled(false);
    }

    fn duration_changed(&self, duration: Option<ClockTime>) {
        let label = if let Some(duration) = duration {
            let mut time = duration.seconds();

            let sec = time % 60;
            time -= sec;
            let min = (time % (60 * 60)) / 60;
            time -= min * 60;
            let hour = time / (60 * 60);

            if hour > 0 {
                // FIXME: Find how to localize this.
                // hour:minutes:seconds
                format!("{hour}:{min:02}:{sec:02}")
            } else {
                // FIXME: Find how to localize this.
                // minutes:seconds
                format!("{min:02}:{sec:02}")
            }
        } else {
            "--:--".to_owned()
        };
        self.imp().timestamp.set_label(&label);
    }
}