Browse Source

message-media: Use custom player

Allows to play media without sound.

Closes #881
merge-requests/1327/merge
Kévin Commaille 4 years ago
parent
commit
cfe2fe0281
No known key found for this signature in database
GPG Key ID: DD507DAE96E8245C
  1. 67
      Cargo.lock
  2. 2
      Cargo.toml
  3. 14
      data/resources/ui/components-video-player.ui
  4. 2
      src/components/mod.rs
  5. 110
      src/components/video_player.rs
  6. 79
      src/components/video_player_renderer.rs
  7. 5
      src/session/content/room_history/message_row/media.rs

67
Cargo.lock generated

@ -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"

2
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"

14
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>

2
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,
};

110
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);
}
}

79
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()
}
}

5
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)
}
};

Loading…
Cancel
Save