[fractal/fractal-next] message-media: Use custom player



commit cfe2fe0281a3e1dfd95beb2070fa91babcc05a56
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Tue Feb 1 09:06:24 2022 +0100

    message-media: Use custom player
    
    Allows to play media without sound.
    
    Closes #881

 Cargo.lock                                         |  67 +++++++++++++
 Cargo.toml                                         |   2 +
 data/resources/ui/components-video-player.ui       |  14 ++-
 src/components/mod.rs                              |   2 +
 src/components/video_player.rs                     | 110 ++++++++++++---------
 src/components/video_player_renderer.rs            |  79 +++++++++++++++
 .../content/room_history/message_row/media.rs      |   5 +-
 7 files changed, 228 insertions(+), 51 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 6645fc9d..35236a1a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -539,6 +539,18 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919"
 
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
 [[package]]
 name = "cipher"
 version = "0.2.5"
@@ -971,8 +983,10 @@ dependencies = [
  "ashpd",
  "futures",
  "gettext-rs",
+ "gst-plugin-gtk4",
  "gstreamer",
  "gstreamer-base",
+ "gstreamer-player",
  "gstreamer-video",
  "gtk-macros",
  "gtk4",
@@ -1523,6 +1537,30 @@ dependencies = [
  "system-deps 6.0.0",
 ]
 
+[[package]]
+name = "gst-plugin-gtk4"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f70501fa85dfdbeebecea35d747791351d04266f5c2c4aa23014d9fe74132290"
+dependencies = [
+ "fragile",
+ "gst-plugin-version-helper",
+ "gstreamer",
+ "gstreamer-base",
+ "gstreamer-video",
+ "gtk4",
+ "once_cell",
+]
+
+[[package]]
+name = "gst-plugin-version-helper"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "3a6a4dd1cb931cc6b49af354a68f21b3aee46b5b07370215d942f3a71542123f"
+dependencies = [
+ "chrono",
+]
+
 [[package]]
 name = "gstreamer"
 version = "0.18.1"
@@ -1575,6 +1613,35 @@ dependencies = [
  "system-deps 6.0.0",
 ]
 
+[[package]]
+name = "gstreamer-player"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "5f14ee02352ba73cadebe640bfb33f12fe8d03cbcad816a102d55a0251fb99bb"
+dependencies = [
+ "bitflags",
+ "glib",
+ "gstreamer",
+ "gstreamer-player-sys",
+ "gstreamer-video",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gstreamer-player-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "1f9b674b39a4d0e18710f6e3d2b109f1793d8028ee4e39da3909b55b4529d399"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-sys",
+ "gstreamer-video-sys",
+ "libc",
+ "system-deps 6.0.0",
+]
+
 [[package]]
 name = "gstreamer-sys"
 version = "0.18.0"
diff --git a/Cargo.toml b/Cargo.toml
index 65d0d9c4..c6e78d18 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,8 @@ ashpd = { version = "0.2.0-beta-1", features = [
 gst = { version = "0.18", package = "gstreamer" }
 gst_base = { version = "0.18", package = "gstreamer-base" }
 gst_video = { version = "0.18", package = "gstreamer-video" }
+gst_player = { version = "0.18", package = "gstreamer-player" }
+gst_gtk = { version = "0.1.0", package = "gst-plugin-gtk4" }
 image = { version = "0.23", default-features = false, features = ["png"] }
 regex = "1.5.4"
 mime_guess = "2.0.3"
diff --git a/data/resources/ui/components-video-player.ui b/data/resources/ui/components-video-player.ui
index a1d664dc..35dd617d 100644
--- a/data/resources/ui/components-video-player.ui
+++ b/data/resources/ui/components-video-player.ui
@@ -1,10 +1,21 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
+  <object class="GstPlayer" id="player">
+    <property name="video-renderer">
+      <object class="ComponentsVideoPlayerRenderer" id="video_renderer"/>
+    </property>
+    <property name="signal-dispatcher">
+      <object class="GstPlayerGMainContextSignalDispatcher"/>
+    </property>
+    <signal name="duration-changed" handler="duration_changed" swapped="true"/>
+  </object>
   <template class="ComponentsVideoPlayer" parent="AdwBin">
     <child>
       <object class="GtkOverlay">
         <child>
-          <object class="GtkPicture" id="video"/>
+          <object class="GtkPicture" id="video">
+            <property name="paintable" bind-source="video_renderer" bind-property="paintable" 
bind-flags="sync-create"/>
+          </object>
         </child>
         <child type="overlay">
           <object class="GtkLabel" id="timestamp">
@@ -17,7 +28,6 @@
             <property name="valign">GTK_ALIGN_START</property>
             <property name="margin-start">5</property>
             <property name="margin-top">5</property>
-            <property name="label">00:00</property>
             <layout>
               <property name="measure">true</property>
             </layout>
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 098e7bd1..ba7c9f5e 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -11,6 +11,7 @@ mod reaction_chooser;
 mod room_title;
 mod spinner_button;
 mod video_player;
+mod video_player_renderer;
 
 pub use self::{
     auth_dialog::{AuthData, AuthDialog},
@@ -26,4 +27,5 @@ pub use self::{
     room_title::RoomTitle,
     spinner_button::SpinnerButton,
     video_player::VideoPlayer,
+    video_player_renderer::VideoPlayerRenderer,
 };
diff --git a/src/components/video_player.rs b/src/components/video_player.rs
index 541cabe0..76ab33b4 100644
--- a/src/components/video_player.rs
+++ b/src/components/video_player.rs
@@ -1,5 +1,9 @@
 use adw::subclass::prelude::*;
-use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+use gst::ClockTime;
+use gst_player::{Player, PlayerGMainContextSignalDispatcher};
+use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use super::VideoPlayerRenderer;
 
 mod imp {
     use std::cell::{Cell, RefCell};
@@ -19,6 +23,8 @@ mod imp {
         pub video: TemplateChild<gtk::Picture>,
         #[template_child]
         pub timestamp: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub player: TemplateChild<Player>,
     }
 
     #[glib::object_subclass]
@@ -28,7 +34,11 @@ mod imp {
         type ParentType = adw::Bin;
 
         fn class_init(klass: &mut Self::Class) {
+            VideoPlayerRenderer::static_type();
+            Player::static_type();
+            PlayerGMainContextSignalDispatcher::static_type();
             Self::bind_template(klass);
+            Self::Type::bind_template_callbacks(klass);
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -39,13 +49,22 @@ mod imp {
     impl ObjectImpl for VideoPlayer {
         fn properties() -> &'static [glib::ParamSpec] {
             static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
-                vec![glib::ParamSpecBoolean::new(
-                    "compact",
-                    "Compact",
-                    "Whether this player should be displayed in a compact format",
-                    false,
-                    glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
-                )]
+                vec![
+                    glib::ParamSpecBoolean::new(
+                        "compact",
+                        "Compact",
+                        "Whether this player should be displayed in a compact format",
+                        false,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecObject::new(
+                        "player",
+                        "Player",
+                        "The GStreamerPlayer for the video",
+                        Player::static_type(),
+                        glib::ParamFlags::READABLE,
+                    ),
+                ]
             });
 
             PROPERTIES.as_ref()
@@ -67,6 +86,7 @@ mod imp {
         fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
             match pspec.name() {
                 "compact" => obj.compact().to_value(),
+                "player" => obj.player().to_value(),
                 _ => unimplemented!(),
             }
         }
@@ -78,11 +98,12 @@ mod imp {
 }
 
 glib::wrapper! {
-    /// A widget displaying a video media file.
+    /// 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;
 }
 
+#[gtk::template_callbacks]
 impl VideoPlayer {
     /// Create a new video player.
     #[allow(clippy::new_without_default)]
@@ -90,6 +111,10 @@ impl VideoPlayer {
         glib::Object::new(&[]).expect("Failed to create VideoPlayer")
     }
 
+    pub fn player(&self) -> &Player {
+        &*self.imp().player
+    }
+
     pub fn compact(&self) -> bool {
         self.imp().compact.get()
     }
@@ -103,43 +128,38 @@ impl VideoPlayer {
         self.notify("compact");
     }
 
-    /// Set the media_file to display.
-    pub fn set_media_file(&self, media_file: &gtk::MediaFile) {
-        let priv_ = self.imp();
-
-        if let Some(handler_id) = priv_.duration_handler.take() {
-            if let Some(paintable) = priv_.video.paintable() {
-                paintable.disconnect(handler_id);
-            }
-        }
-
-        priv_.video.set_paintable(Some(media_file));
-        let timestamp = &*priv_.timestamp;
-        let handler_id =
-            media_file.connect_duration_notify(clone!(@weak timestamp => move |media_file| {
-                timestamp.set_label(&duration(media_file));
-            }));
-        priv_.duration_handler.replace(Some(handler_id));
+    /// Set the file to display.
+    pub fn play_media_file(&self, file: &gio::File) {
+        self.duration_changed(None);
+        let player = self.player();
+        player.set_uri(Some(file.uri().as_ref()));
+        player.set_audio_track_enabled(false);
+        player.play();
     }
-}
 
-/// Get the duration of `media_file` as a `String`.
-fn duration(media_file: &gtk::MediaFile) -> String {
-    let mut time = media_file.duration() / 1000000;
-
-    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!("{}:{:02}:{:02}", hour, min, sec)
-    } else {
-        // FIXME: Find how to localize this.
-        // minutes:seconds
-        format!("{:02}:{:02}", min, sec)
+    #[template_callback]
+    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!("{}:{:02}:{:02}", hour, min, sec)
+            } else {
+                // FIXME: Find how to localize this.
+                // minutes:seconds
+                format!("{:02}:{:02}", min, sec)
+            }
+        } else {
+            "--:--".to_owned()
+        };
+        self.imp().timestamp.set_label(&label);
     }
 }
diff --git a/src/components/video_player_renderer.rs b/src/components/video_player_renderer.rs
new file mode 100644
index 00000000..e610c802
--- /dev/null
+++ b/src/components/video_player_renderer.rs
@@ -0,0 +1,79 @@
+use adw::subclass::prelude::*;
+use gst_gtk::PaintableSink;
+use gst_player::{subclass::prelude::*, Player, PlayerVideoRenderer};
+use gtk::{gdk, glib, prelude::*};
+
+mod imp {
+    use once_cell::{sync::Lazy, unsync::OnceCell};
+
+    use super::*;
+
+    #[derive(Debug, Default)]
+    pub struct VideoPlayerRenderer {
+        /// The sink to use to display the video.
+        pub sink: OnceCell<PaintableSink>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for VideoPlayerRenderer {
+        const NAME: &'static str = "ComponentsVideoPlayerRenderer";
+        type Type = super::VideoPlayerRenderer;
+        type ParentType = glib::Object;
+        type Interfaces = (PlayerVideoRenderer,);
+    }
+
+    impl ObjectImpl for VideoPlayerRenderer {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![glib::ParamSpecObject::new(
+                    "paintable",
+                    "Paintable",
+                    "Paintable to render the video into",
+                    gdk::Paintable::static_type(),
+                    glib::ParamFlags::READABLE,
+                )]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "paintable" => obj.paintable().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            obj.imp().sink.set(PaintableSink::new(None)).unwrap();
+        }
+    }
+
+    impl PlayerVideoRendererImpl for VideoPlayerRenderer {
+        fn create_video_sink(&self, _obj: &Self::Type, _player: &Player) -> gst::Element {
+            self.sink.get().unwrap().to_owned().upcast()
+        }
+    }
+}
+
+glib::wrapper! {
+    /// A widget displaying a video media file.
+    pub struct VideoPlayerRenderer(ObjectSubclass<imp::VideoPlayerRenderer>)
+        @implements PlayerVideoRenderer;
+}
+
+impl VideoPlayerRenderer {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create VideoPlayerRenderer")
+    }
+
+    pub fn paintable(&self) -> gdk::Paintable {
+        self.imp().sink.get().unwrap().property("paintable")
+    }
+}
+
+impl Default for VideoPlayerRenderer {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/session/content/room_history/message_row/media.rs 
b/src/session/content/room_history/message_row/media.rs
index f515d285..3a24a1f6 100644
--- a/src/session/content/room_history/message_row/media.rs
+++ b/src/session/content/room_history/message_row/media.rs
@@ -461,9 +461,6 @@ impl MessageMedia {
                                     gio::Cancellable::NONE,
                                 )
                                 .unwrap();
-                                let media_file = gtk::MediaFile::for_file(&file);
-                                media_file.set_muted(true);
-                                media_file.connect_prepared_notify(|media_file| media_file.play());
 
                                 let child = if let Some(Ok(child)) =
                                     priv_.media.child().map(|w| w.downcast::<VideoPlayer>())
@@ -475,7 +472,7 @@ impl MessageMedia {
                                     child
                                 };
                                 child.set_compact(obj.compact());
-                                child.set_media_file(&media_file)
+                                child.play_media_file(&file)
                             }
                         };
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]