[fractal] Implementation of video player
- From: Daniel Garcia Moreno <danigm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] Implementation of video player
- Date: Tue, 28 Jan 2020 12:52:22 +0000 (UTC)
commit a42873052efde0ab3ef6f9ed73dccaeb1c52ca9a
Author: sonjita <sonjaleaheinze gmail com>
Date: Sat Dec 14 11:56:21 2019 +0100
Implementation of video player
This commit implements a video player for video messages. For any video
message in the room history, a video widget appears in the room history.
There, the video gets played muted in a loop until one of the following happens:
- the widget gets out of sight due to scrolling;
- the user navigates out of the room history, for example into room settings;
- Fractal gets unfocussed;
- the user leaves the room.
When the user clicks on the video widget, the media viewer is opened.
There, the user can play and pause the video unmuted and seek in it.
fractal-gtk/src/uitypes.rs | 1 +
fractal-gtk/src/widgets/inline_player.rs | 518 +++++++++++++++++++++++++------
fractal-gtk/src/widgets/media_viewer.rs | 181 ++++++++---
fractal-gtk/src/widgets/message.rs | 95 +++---
fractal-gtk/src/widgets/mod.rs | 3 +
fractal-gtk/src/widgets/room_history.rs | 329 ++++++++++++++++++--
fractal-gtk/src/widgets/scroll_widget.rs | 5 +
7 files changed, 917 insertions(+), 215 deletions(-)
---
diff --git a/fractal-gtk/src/uitypes.rs b/fractal-gtk/src/uitypes.rs
index 111986aa..68089a48 100644
--- a/fractal-gtk/src/uitypes.rs
+++ b/fractal-gtk/src/uitypes.rs
@@ -2,6 +2,7 @@ use crate::types::Message;
use crate::widgets;
use chrono::prelude::DateTime;
use chrono::prelude::Local;
+
/* MessageContent contains all data needed to display one row
* therefore it should contain only one Message body with one format
* To-Do: this should be moved to a file collecting all structs used in the UI */
diff --git a/fractal-gtk/src/widgets/inline_player.rs b/fractal-gtk/src/widgets/inline_player.rs
index f9318010..99a058e8 100644
--- a/fractal-gtk/src/widgets/inline_player.rs
+++ b/fractal-gtk/src/widgets/inline_player.rs
@@ -21,7 +21,7 @@ use fractal_api::clone;
use gst::prelude::*;
use gst::ClockTime;
use gst_player;
-use log::{error, warn};
+use log::{error, info, warn};
use gtk;
use gtk::prelude::*;
@@ -36,11 +36,30 @@ use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
-trait PlayerExt {
+use std::sync::mpsc::channel;
+use std::sync::mpsc::TryRecvError;
+use std::sync::mpsc::{Receiver, Sender};
+
+use url::Url;
+
+use crate::app::App;
+use crate::backend::BKCommand;
+use crate::i18n::i18n;
+
+pub trait PlayerExt {
fn play(&self);
fn pause(&self);
fn stop(&self);
- fn set_uri(&self, uri: &str);
+ fn initialize_stream(
+ player: &Rc<Self>,
+ backend: &Sender<BKCommand>,
+ media_url: &String,
+ server_url: &Url,
+ bx: >k::Box,
+ start_playing: bool,
+ );
+ fn get_controls_container(player: &Rc<Self>) -> Option<gtk::Box>;
+ fn get_player(player: &Rc<Self>) -> gst_player::Player;
}
#[derive(Debug, Clone)]
@@ -107,18 +126,36 @@ fn format_duration(seconds: u32) -> String {
}
#[derive(Debug, Clone)]
-struct PlayerControls {
+struct PlayButtons {
container: gtk::Box,
play: gtk::Button,
pause: gtk::Button,
}
+#[derive(Debug, Clone)]
+pub struct PlayerControls {
+ container: gtk::Box,
+ buttons: PlayButtons,
+ timer: PlayerTimes,
+}
+
+pub trait MediaPlayer {
+ fn get_player(&self) -> gst_player::Player;
+ fn get_controls(&self) -> Option<PlayerControls>;
+ fn get_local_path_access(&self) -> Rc<RefCell<Option<String>>>;
+}
+
+trait ControlsConnection {
+ fn init(s: &Rc<Self>);
+ fn connect_control_buttons(s: &Rc<Self>);
+ fn connect_gst_signals(s: &Rc<Self>);
+}
+
#[derive(Debug, Clone)]
pub struct AudioPlayerWidget {
- pub container: gtk::Box,
player: gst_player::Player,
controls: PlayerControls,
- timer: PlayerTimes,
+ local_path: Rc<RefCell<Option<String>>>,
}
impl Default for AudioPlayerWidget {
@@ -130,6 +167,8 @@ impl Default for AudioPlayerWidget {
Some(&dispatcher.upcast::<gst_player::PlayerSignalDispatcher>()),
);
+ player.set_video_track_enabled(false);
+
let mut config = player.get_config();
config.set_position_update_interval(250);
player.set_config(config).unwrap();
@@ -141,38 +180,12 @@ impl Default for AudioPlayerWidget {
// This ideally will never occur.
player.connect_error(move |_, err| error!("gst Error: {}", err));
- let builder = gtk::Builder::new_from_resource("/org/gnome/Fractal/ui/audio_player.ui");
- let container = builder.get_object("container").unwrap();
-
- let buttons = builder.get_object("buttons").unwrap();
- let play = builder.get_object("play_button").unwrap();
- let pause = builder.get_object("pause_button").unwrap();
-
- let controls = PlayerControls {
- container: buttons,
- play,
- pause,
- };
-
- let timer_container = builder.get_object("timer").unwrap();
- let progressed = builder.get_object("progress_time_label").unwrap();
- let duration = builder.get_object("total_duration_label").unwrap();
- let slider: gtk::Scale = builder.get_object("seek").unwrap();
- slider.set_range(0.0, 1.0);
- let slider_update = Rc::new(Self::connect_update_slider(&slider, &player));
- let timer = PlayerTimes {
- container: timer_container,
- progressed,
- duration,
- slider,
- slider_update,
- };
+ let controls = create_controls(&player);
AudioPlayerWidget {
- container,
player,
controls,
- timer,
+ local_path: Rc::new(RefCell::new(None)),
}
}
}
@@ -184,7 +197,7 @@ impl AudioPlayerWidget {
// When the widget is attached to a parent,
// since it's a rust struct and not a widget the
// compiler drops the refference to it at the end of
- // scope. That's cause we only attach the `self.container`
+ // scope. That's cause we only attach the `self.controls.container`
// to the parent.
//
// So this callback keeps a refference to the Rust Struct
@@ -195,95 +208,418 @@ impl AudioPlayerWidget {
// when we drop the room widget, this callback runs freeing
// the last refference we were holding.
let foo = RefCell::new(Some(w.clone()));
- w.container.connect_remove(move |_, _| {
+ w.controls.container.connect_remove(move |_, _| {
foo.borrow_mut().take();
});
w
}
+}
- #[cfg_attr(rustfmt, rustfmt_skip)]
- pub fn init(s: &Rc<Self>) {
- Self::connect_control_buttons(s);
- Self::connect_gst_signals(s);
+impl MediaPlayer for AudioPlayerWidget {
+ fn get_player(&self) -> gst_player::Player {
+ self.player.clone()
}
- pub fn initialize_stream(&self, uri: &str) {
- self.set_uri(uri)
+ fn get_controls(&self) -> Option<PlayerControls> {
+ Some(self.controls.clone())
}
- #[cfg_attr(rustfmt, rustfmt_skip)]
- /// Connect the `PlayerControls` buttons to the `PlayerExt` methods.
- fn connect_control_buttons(s: &Rc<Self>) {
- let weak = Rc::downgrade(s);
+ fn get_local_path_access(&self) -> Rc<RefCell<Option<String>>> {
+ self.local_path.clone()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct VideoPlayerWidget {
+ player: gst_player::Player,
+ controls: Option<PlayerControls>,
+ local_path: Rc<RefCell<Option<String>>>,
+ dimensions: Rc<RefCell<Option<(i32, i32)>>>,
+}
+
+impl Default for VideoPlayerWidget {
+ fn default() -> Self {
+ let dispatcher = gst_player::PlayerGMainContextSignalDispatcher::new(None);
+ let sink = gst::ElementFactory::make("gtksink", None).unwrap();
+ let renderer = gst_player::PlayerVideoOverlayVideoRenderer::new_with_sink(&sink).upcast();
+ let player = gst_player::Player::new(
+ Some(&renderer),
+ // Use the gtk main thread
+ Some(&dispatcher.upcast::<gst_player::PlayerSignalDispatcher>()),
+ );
+
+ let mut config = player.get_config();
+ config.set_position_update_interval(250);
+ player.set_config(config).unwrap();
- // Connect the play button to the gst Player.
- s.controls.play.connect_clicked(clone!(weak => move |_| {
- weak.upgrade().map(|p| p.play());
- }));
+ // Log gst warnings.
+ player.connect_warning(move |_, warn| warn!("gst warning: {}", warn));
- // Connect the pause button to the gst Player.
- s.controls.pause.connect_clicked(clone!(weak => move |_| {
- weak.upgrade().map(|p| p.pause());
- }));
+ // Log gst errors.
+ // This ideally will never occur.
+ player.connect_error(move |_, err| error!("gst Error: {}", err));
+
+ VideoPlayerWidget {
+ player,
+ controls: None,
+ local_path: Rc::new(RefCell::new(None)),
+ dimensions: Rc::new(RefCell::new(None)),
+ }
}
+}
- #[cfg_attr(rustfmt, rustfmt_skip)]
- fn connect_gst_signals(s: &Rc<Self>) {
- // The followign callbacks require `Send` but are handled by the gtk main loop
- let weak = Fragile::new(Rc::downgrade(s));
+impl VideoPlayerWidget {
+ pub fn new(with_controls: bool) -> Rc<Self> {
+ let mut player_widget = Self::default();
- // Update the duration label and the slider
- s.player.connect_duration_changed(clone!(weak => move |_, clock| {
- weak.get().upgrade().map(|p| p.timer.on_duration_changed(Duration(clock)));
- }));
+ if with_controls {
+ let controls = create_controls(&player_widget.player);
+ player_widget.controls = Some(controls);
+ }
- // Update the position label and the slider
- s.player.connect_position_updated(clone!(weak => move |_, clock| {
- weak.get().upgrade().map(|p| p.timer.on_position_updated(Position(clock)));
- }));
+ let w = Rc::new(player_widget);
+ if with_controls {
+ // When the widget is attached to a parent,
+ // since it's a rust struct and not a widget the
+ // compiler drops the refference to it at the end of
+ // scope. That's cause we only attach the `self.controls.container`
+ // to the parent.
+ //
+ // So this callback keeps a refference to the Rust Struct
+ // so the compiler won't drop it which would cause to also drop
+ // the `gst_player`.
+ //
+ // When the widget is detached from it's parent which happens
+ // when we drop the room widget, this callback runs freeing
+ // the last refference we were holding.
+ let container = w.controls.clone().unwrap().container;
+ let foo = RefCell::new(Some(w.clone()));
+ container.connect_remove(move |_, _| {
+ foo.borrow_mut().take();
+ });
+ }
+ w
+ }
- // Reset the slider to 0 and show a play button
- s.player.connect_end_of_stream(clone!(weak => move |_| {
- weak.get().upgrade().map(|p| p.stop());
- }));
+ pub fn get_video_widget(&self) -> gtk::Widget {
+ let pipeline = self.player.get_pipeline();
+ pipeline
+ .get_property("video-sink")
+ .unwrap()
+ .get::<gst::Element>()
+ .expect("The player of a VideoPlayerWidget should not use the default sink.")
+ .get_property("widget")
+ .unwrap()
+ .get::<gtk::Widget>()
+ .unwrap()
}
- fn connect_update_slider(slider: >k::Scale, player: &gst_player::Player) -> SignalHandlerId {
- slider.connect_value_changed(clone!(player => move |slider| {
- let value = slider.get_value() as u64;
- player.seek(ClockTime::from_seconds(value));
- }))
+ pub fn auto_adjust_video_dimensions(player_widget: &Rc<Self>) {
+ /* The followign callback requires `Send` but is handled by the gtk main loop */
+ let player_weak = Fragile::new(Rc::downgrade(&player_widget));
+ let dimensions_weak = Fragile::new(Rc::downgrade(&player_widget.dimensions));
+ player_widget.player.connect_video_dimensions_changed(
+ move |_, video_width, video_height| {
+ if video_width != 0 {
+ player_weak.get().upgrade().map(|player| {
+ let widget = player.get_video_widget();
+ let allocated_width = widget.get_allocated_width();
+ let adjusted_height = allocated_width * video_height / video_width;
+ widget.set_size_request(-1, adjusted_height);
+ });
+ }
+ dimensions_weak.get().upgrade().map(|dimensions| {
+ *dimensions.borrow_mut() = Some((video_width, video_height));
+ });
+ },
+ );
+ let player_weak = Rc::downgrade(&player_widget);
+ player_widget
+ .get_video_widget()
+ .connect_size_allocate(move |_, allocation| {
+ player_weak.upgrade().map(|player| {
+ if let Some((video_width, video_height)) = *player.dimensions.borrow() {
+ if video_width != 0
+ && allocation.height * video_width != allocation.width * video_height
+ {
+ let adjusted_height = allocation.width * video_height / video_width;
+ player
+ .get_video_widget()
+ .set_size_request(-1, adjusted_height);
+ }
+ }
+ });
+ });
+
+ /* Sometimes, set_size_request() doesn't get captured visually. The following timeout takes care of
that. */
+ let player_weak = Rc::downgrade(&player_widget);
+ gtk::timeout_add_seconds(1, move || {
+ player_weak.upgrade().map(|player| {
+ let (_, height) = player.get_video_widget().get_size_request();
+ player.get_video_widget().set_size_request(-1, height - 1);
+ player.get_video_widget().set_size_request(-1, height);
+ });
+ Continue(true)
+ });
}
-}
-impl PlayerExt for AudioPlayerWidget {
- fn play(&self) {
- self.controls.pause.show();
- self.controls.play.hide();
+ pub fn auto_adjust_widget_to_video_dimensions<T: IsA<gtk::Widget>>(
+ bx: >k::Box,
+ widget: &T,
+ player: &Rc<VideoPlayerWidget>,
+ ) {
+ /* When gtk allocates a different size to the video widget than its minimal preferred size
+ (set by set_size_request()), the method auto_adjust_video_dimensions() does not have any effect.
+ When that happens and furthermore, the video widget is embedded in a vertically oriented box,
+ this function here can be called. Here, the widget's height gets adjusted as a consequence of
+ adjusting the top and bottom margin of the box, rather than through the widget's preferred height.*/
+ let top_box = gtk::Box::new(gtk::Orientation::Vertical, 6);
+ bx.pack_start(&top_box, true, true, 0);
+ let bottom_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
+ bx.pack_start(&bottom_box, true, true, 0);
+ bx.reorder_child(widget, 1);
+ /* The followign callbacks requires `Send` but is handled by the gtk main loop */
+ let dimensions_weak = Fragile::new(Rc::downgrade(&player.dimensions));
+ let bx_weak = Fragile::new(bx.downgrade());
+ player
+ .player
+ .connect_video_dimensions_changed(move |_, video_width, video_height| {
+ dimensions_weak.get().upgrade().map(|dimensions| {
+ *dimensions.borrow_mut() = Some((video_width, video_height));
+ });
+ bx_weak.get().upgrade().map(|bx| {
+ adjust_box_margins_to_video_dimensions(&bx, video_width, video_height);
+ });
+ });
+ let player_weak = Rc::downgrade(player);
+ bx.connect_size_allocate(move |bx, _| {
+ player_weak.upgrade().map(|player| {
+ if let Some((video_width, video_height)) = *player.dimensions.borrow() {
+ adjust_box_margins_to_video_dimensions(&bx, video_width, video_height);
+ }
+ });
+ });
+ }
+ /* As soon as there's an implementation for that in gst::Player, we should take that one instead. */
+ pub fn play_in_loop(&self) -> SignalHandlerId {
+ self.player.set_mute(true);
self.player.play();
+ self.player.connect_end_of_stream(|player| {
+ player.play();
+ })
}
- fn pause(&self) {
- self.controls.pause.hide();
- self.controls.play.show();
+ pub fn stop_loop(&self, id: SignalHandlerId) {
+ self.player.set_mute(false);
+ self.player.stop();
+ self.player.disconnect(id);
+ }
+}
+
+impl PartialEq for VideoPlayerWidget {
+ fn eq(&self, other: &Self) -> bool {
+ self.player == other.player
+ }
+}
- self.player.pause();
+impl MediaPlayer for VideoPlayerWidget {
+ fn get_player(&self) -> gst_player::Player {
+ self.player.clone()
+ }
+
+ fn get_controls(&self) -> Option<PlayerControls> {
+ self.controls.clone()
+ }
+
+ fn get_local_path_access(&self) -> Rc<RefCell<Option<String>>> {
+ self.local_path.clone()
+ }
+}
+
+impl<T: MediaPlayer + 'static> PlayerExt for T {
+ fn play(&self) {
+ if let Some(controls) = self.get_controls() {
+ controls.buttons.pause.show();
+ controls.buttons.play.hide();
+ }
+ self.get_player().play();
+ }
+
+ fn pause(&self) {
+ if let Some(controls) = self.get_controls() {
+ controls.buttons.pause.hide();
+ controls.buttons.play.show();
+ }
+ self.get_player().pause();
}
#[cfg_attr(rustfmt, rustfmt_skip)]
fn stop(&self) {
- self.controls.pause.hide();
- self.controls.play.show();
+ if let Some(controls) = self.get_controls() {
+ controls.buttons.pause.hide();
+ controls.buttons.play.show();
+ // Reset the slider position to 0
+ controls.timer.on_position_updated(Position(ClockTime::from_seconds(0)));
+ }
- self.player.stop();
+ self.get_player().stop();
+ }
- // Reset the slider position to 0
- self.timer.on_position_updated(Position(ClockTime::from_seconds(0)));
+ fn initialize_stream(
+ player: &Rc<Self>,
+ backend: &Sender<BKCommand>,
+ media_url: &String,
+ server_url: &Url,
+ bx: >k::Box,
+ start_playing: bool,
+ ) {
+ bx.set_opacity(0.3);
+ let (tx, rx): (Sender<String>, Receiver<String>) = channel();
+ backend
+ .send(BKCommand::GetMediaAsync(
+ server_url.clone(),
+ media_url.clone(),
+ tx,
+ ))
+ .unwrap();
+ let local_path = player.get_local_path_access();
+ gtk::timeout_add(
+ 50,
+ clone!(player, bx => move || {
+ match rx.try_recv() {
+ Err(TryRecvError::Empty) => gtk::Continue(true),
+ Err(TryRecvError::Disconnected) => {
+ let msg = i18n("Could not retrieve file URI");
+ /* FIXME: don't use APPOP! */
+ APPOP!(show_error, (msg));
+ gtk::Continue(true)
+ },
+ Ok(path) => {
+ info!("MEDIA PATH: {}", &path);
+ *local_path.borrow_mut() = Some(path.clone());
+ let uri = format!("file://{}", path);
+ player.get_player().set_uri(&uri);
+ if player.get_controls().is_some() {
+ ControlsConnection::init(&player);
+ }
+ bx.set_opacity(1.0);
+ if start_playing {
+ player.play();
+ }
+ gtk::Continue(false)
+ }
+ }
+ }),
+ );
+ }
+
+ fn get_controls_container(player: &Rc<Self>) -> Option<gtk::Box> {
+ player.get_controls().map(|controls| controls.container)
+ }
+
+ fn get_player(player: &Rc<Self>) -> gst_player::Player {
+ player.get_player()
+ }
+}
+
+impl<T: MediaPlayer + 'static> ControlsConnection for T {
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ fn init(s: &Rc<Self>) {
+ Self::connect_control_buttons(s);
+ Self::connect_gst_signals(s);
+ }
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ /// Connect the `PlayerControls` buttons to the `PlayerEssentials` methods.
+ fn connect_control_buttons(s: &Rc<Self>) {
+ if s.get_controls().is_some() {
+ let weak = Rc::downgrade(s);
+
+ // Connect the play button to the gst Player.
+ s.get_controls().unwrap().buttons.play.connect_clicked(clone!(weak => move |_| {
+ weak.upgrade().map(|p| p.play());
+ }));
+
+ // Connect the pause button to the gst Player.
+ s.get_controls().unwrap().buttons.pause.connect_clicked(clone!(weak => move |_| {
+ weak.upgrade().map(|p| p.pause());
+ }));
+ }
}
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ fn connect_gst_signals(s: &Rc<Self>) {
+ if s.get_controls().is_some() {
+ // The followign callbacks require `Send` but are handled by the gtk main loop
+ let weak = Fragile::new(Rc::downgrade(s));
+
+ // Update the duration label and the slider
+ s.get_player().connect_duration_changed(clone!(weak => move |_, clock| {
+ weak.get().upgrade().map(|p|
p.get_controls().unwrap().timer.on_duration_changed(Duration(clock)));
+ }));
+
+ // Update the position label and the slider
+ s.get_player().connect_position_updated(clone!(weak => move |_, clock| {
+ weak.get().upgrade().map(|p|
p.get_controls().unwrap().timer.on_position_updated(Position(clock)));
+ }));
+
+ // Reset the slider to 0 and show a play button
+ s.get_player().connect_end_of_stream(clone!(weak => move |_| {
+ weak.get().upgrade().map(|p| p.stop());
+ }));
+ }
+ }
+}
- fn set_uri(&self, uri: &str) {
- self.player.set_uri(uri)
+fn create_controls(player: &gst_player::Player) -> PlayerControls {
+ let builder = gtk::Builder::new_from_resource("/org/gnome/Fractal/ui/audio_player.ui");
+ let container = builder.get_object("container").unwrap();
+
+ let buttons_container = builder.get_object("buttons").unwrap();
+ let play = builder.get_object("play_button").unwrap();
+ let pause = builder.get_object("pause_button").unwrap();
+
+ let buttons = PlayButtons {
+ container: buttons_container,
+ play,
+ pause,
+ };
+
+ let timer_container = builder.get_object("timer").unwrap();
+ let progressed = builder.get_object("progress_time_label").unwrap();
+ let duration = builder.get_object("total_duration_label").unwrap();
+ let slider: gtk::Scale = builder.get_object("seek").unwrap();
+ slider.set_range(0.0, 1.0);
+ let slider_update = Rc::new(connect_update_slider(&slider, player));
+ let timer = PlayerTimes {
+ container: timer_container,
+ progressed,
+ duration,
+ slider,
+ slider_update,
+ };
+ PlayerControls {
+ container,
+ buttons,
+ timer,
+ }
+}
+
+fn connect_update_slider(slider: >k::Scale, player: &gst_player::Player) -> SignalHandlerId {
+ slider.connect_value_changed(clone!(player => move |slider| {
+ let value = slider.get_value() as u64;
+ player.seek(ClockTime::from_seconds(value));
+ }))
+}
+
+fn adjust_box_margins_to_video_dimensions(bx: >k::Box, video_width: i32, video_height: i32) {
+ if video_width != 0 {
+ let current_width = bx.get_allocated_width();
+ let adjusted_height = current_width * video_height / video_width;
+ if let Some(scrolled_window) = bx.get_parent().and_then(|viewport| viewport.get_parent()) {
+ let height_visible_area = scrolled_window.get_allocated_height();
+ let margin = (height_visible_area - adjusted_height) / 2;
+ bx.set_spacing(margin);
+ }
}
}
diff --git a/fractal-gtk/src/widgets/media_viewer.rs b/fractal-gtk/src/widgets/media_viewer.rs
index 60147234..ee47bff1 100644
--- a/fractal-gtk/src/widgets/media_viewer.rs
+++ b/fractal-gtk/src/widgets/media_viewer.rs
@@ -12,6 +12,7 @@ use glib;
use glib::signal;
use gtk;
use gtk::prelude::*;
+use gtk::Overlay;
use gtk::ResponseType;
use url::Url;
@@ -23,6 +24,8 @@ use std::fs;
use crate::backend::BKCommand;
use crate::widgets::image;
use crate::widgets::ErrorDialog;
+use crate::widgets::PlayerExt;
+use crate::widgets::{MediaPlayer, VideoPlayerWidget};
use std::sync::mpsc::channel;
use std::sync::mpsc::TryRecvError;
use std::sync::mpsc::{Receiver, Sender};
@@ -37,6 +40,13 @@ pub struct MediaViewer {
backend: Sender<BKCommand>,
}
+#[derive(Debug)]
+enum Widget {
+ Image(image::Image),
+ Video(Rc<VideoPlayerWidget>),
+ None,
+}
+
#[derive(Debug)]
struct Data {
builder: gtk::Builder,
@@ -45,7 +55,7 @@ struct Data {
server_url: Url,
access_token: AccessToken,
- pub image: Option<image::Image>,
+ widget: Widget,
media_list: Vec<Message>,
current_media_index: usize,
@@ -73,7 +83,7 @@ impl Data {
loading_more_media: false,
loading_error: false,
no_more_media: false,
- image: None,
+ widget: Widget::None,
builder,
backend,
server_url,
@@ -83,14 +93,21 @@ impl Data {
}
}
- pub fn save_media(&self) -> Option<()> {
- let image = self.image.clone()?;
- save_file_as(
- &self.main_window,
- image.local_path.lock().unwrap().clone().unwrap_or_default(),
- self.media_list[self.current_media_index].body.clone(),
- );
- None
+ pub fn save_media(&self) {
+ let local_path = match &self.widget {
+ Widget::Image(image) => image.local_path.lock().unwrap().clone(),
+ Widget::Video(player) => player.get_local_path_access().borrow().clone(),
+ Widget::None => None,
+ };
+ if let Some(local_path) = local_path {
+ save_file_as(
+ &self.main_window,
+ local_path,
+ self.media_list[self.current_media_index].body.clone(),
+ );
+ } else {
+ ErrorDialog::new(false, &i18n("Media is not loaded, yet."));
+ }
}
pub fn enter_full_screen(&mut self) {
@@ -129,7 +146,7 @@ impl Data {
headerbar_revealer.add(&media_viewer_headerbar);
- self.redraw_image_in_viewport();
+ self.redraw_media_in_viewport();
}
pub fn leave_full_screen(&mut self) {
@@ -169,7 +186,7 @@ impl Data {
media_viewer_headerbar.set_hexpand(true);
media_viewer_headerbar_box.add(&media_viewer_headerbar);
- self.redraw_image_in_viewport();
+ self.redraw_media_in_viewport();
}
pub fn set_nav_btn_visibility(&self) {
@@ -210,7 +227,7 @@ impl Data {
set_header_title(&self.builder, name);
}
- self.redraw_image_in_viewport();
+ self.redraw_media_in_viewport();
return true;
}
}
@@ -225,10 +242,10 @@ impl Data {
set_header_title(&self.builder, name);
}
- self.redraw_image_in_viewport();
+ self.redraw_media_in_viewport();
}
- pub fn redraw_image_in_viewport(&mut self) {
+ pub fn redraw_media_in_viewport(&mut self) {
let media_viewport = self
.builder
.get_object::<gtk::Viewport>("media_viewport")
@@ -238,22 +255,74 @@ impl Data {
media_viewport.remove(&child);
}
- let url = self.media_list[self.current_media_index]
- .url
- .clone()
- .unwrap_or_default();
+ let msg = &self.media_list[self.current_media_index];
+ let url = msg.url.clone().unwrap_or_default();
+ match msg.mtype.as_ref() {
+ "m.image" => {
+ let image = image::Image::new(&self.backend, self.server_url.clone(), &url)
+ .fit_to_width(true)
+ .center(true)
+ .build();
+ media_viewport.add(&image.widget);
+ image.widget.show();
+ self.widget = Widget::Image(image);
+ }
+ "m.video" => {
+ let bx = self.create_video_widget(&url);
+ media_viewport.add(&bx);
+ media_viewport.show_all();
+ }
+ _ => {}
+ }
+ self.set_nav_btn_visibility();
+ }
+
+ fn create_video_widget(&mut self, url: &String) -> gtk::Box {
+ let with_controls = true;
+ let player = VideoPlayerWidget::new(with_controls);
+ let bx = gtk::Box::new(gtk::Orientation::Vertical, 0);
+ let start_playing = true;
+ PlayerExt::initialize_stream(
+ &player,
+ &self.backend,
+ url,
+ &self.server_url.clone(),
+ &bx,
+ start_playing,
+ );
- let image = image::Image::new(&self.backend, self.server_url.clone(), &url)
- .fit_to_width(true)
- .center(true)
- .build();
+ let overlay = Overlay::new();
+ overlay.add(&player.get_video_widget());
+ let control_box = PlayerExt::get_controls_container(&player).unwrap();
+ control_box.set_valign(gtk::Align::End);
+ control_box.get_style_context().add_class("osd");
+ overlay.add_overlay(&control_box);
- media_viewport.add(&image.widget);
- image.widget.show();
+ bx.pack_start(&overlay, false, false, 0);
- self.set_nav_btn_visibility();
+ if let Some(win) = self.main_window.clone().get_window() {
+ if win.get_state().contains(gdk::WindowState::FULLSCREEN) {
+ bx.set_child_packing(&overlay, true, true, 0, gtk::PackType::Start);
+ } else {
+ bx.set_margin_start(70);
+ bx.set_margin_end(70);
+ overlay.set_valign(gtk::Align::Center);
+ overlay.set_halign(gtk::Align::Center);
+ VideoPlayerWidget::auto_adjust_widget_to_video_dimensions(&bx, &overlay, &player);
+ }
+ }
- self.image = Some(image);
+ let player_weak = Rc::downgrade(&player);
+ bx.connect_state_flags_changed(move |_, flag| {
+ let focussed = gtk::StateFlags::BACKDROP;
+ player_weak.upgrade().map(|player| {
+ if !flag.contains(focussed) {
+ player.pause();
+ }
+ });
+ });
+ self.widget = Widget::Video(player);
+ bx
}
}
@@ -278,7 +347,7 @@ impl MediaViewer {
.messages
.clone()
.into_iter()
- .filter(|msg| msg.mtype == "m.image")
+ .filter(|msg| msg.mtype == "m.image" || msg.mtype == "m.video")
.collect();
let current_media_index = media_list
@@ -312,6 +381,7 @@ impl MediaViewer {
.expect("Can't find media_viewer_headerbar in ui file.");
self.connect_media_viewer_headerbar();
self.connect_media_viewer_box();
+ self.connect_stop_video_when_leaving();
Some((body, header))
}
@@ -336,20 +406,28 @@ impl MediaViewer {
.get_object::<gtk::Viewport>("media_viewport")
.expect("Cant find media_viewport in ui file.");
- let image = image::Image::new(
- &self.backend,
- self.data.borrow().server_url.clone(),
- &media_msg.url.clone().unwrap_or_default(),
- )
- .fit_to_width(true)
- .center(true)
- .build();
-
- media_viewport.add(&image.widget);
- media_viewport.show_all();
-
- self.data.borrow_mut().image = Some(image);
- self.data.borrow_mut().set_nav_btn_visibility();
+ let url = media_msg.url.clone().unwrap_or_default();
+ match media_msg.mtype.as_ref() {
+ "m.image" => {
+ let image =
+ image::Image::new(&self.backend, self.data.borrow().server_url.clone(), &url)
+ .fit_to_width(true)
+ .center(true)
+ .build();
+
+ media_viewport.add(&image.widget);
+ media_viewport.show_all();
+
+ self.data.borrow_mut().widget = Widget::Image(image);
+ self.data.borrow_mut().set_nav_btn_visibility();
+ }
+ "m.video" => {
+ let bx = self.data.borrow_mut().create_video_widget(&url);
+ media_viewport.add(&bx);
+ media_viewport.show_all();
+ }
+ _ => {}
+ }
}
/* connect media viewer headerbar */
@@ -564,6 +642,23 @@ impl MediaViewer {
}
});
}
+
+ fn connect_stop_video_when_leaving(&self) {
+ let media_viewer_box = self
+ .builder
+ .clone()
+ .get_object::<gtk::Box>("media_viewer_box")
+ .expect("Cant find media_viewer_box in ui file.");
+ let data_weak = Rc::downgrade(&self.data);
+ media_viewer_box.connect_unmap(move |_| {
+ data_weak.upgrade().map(|data| match &data.borrow().widget {
+ Widget::Video(player_widget) => {
+ PlayerExt::get_player(&player_widget).stop();
+ }
+ _ => {}
+ });
+ });
+ }
}
fn set_header_title(ui: >k::Builder, title: &str) {
@@ -637,7 +732,7 @@ fn load_more_media(data: Rc<RefCell<Data>>, builder: gtk::Builder, backend: Send
let media_list = data.borrow().media_list.clone();
let img_msgs: Vec<Message> = msgs
.into_iter()
- .filter(|msg| msg.mtype == "m.image")
+ .filter(|msg| msg.mtype == "m.image" || msg.mtype == "m.video")
.collect();
let img_msgs_count = img_msgs.len();
let new_media_list: Vec<Message> =
diff --git a/fractal-gtk/src/widgets/message.rs b/fractal-gtk/src/widgets/message.rs
index becb2909..ece491d2 100644
--- a/fractal-gtk/src/widgets/message.rs
+++ b/fractal-gtk/src/widgets/message.rs
@@ -1,8 +1,5 @@
-use crate::app::App;
use crate::i18n::i18n;
-use fractal_api::clone;
use itertools::Itertools;
-use log::info;
use chrono::prelude::*;
use glib;
@@ -10,16 +7,14 @@ use gtk;
use gtk::prelude::*;
use gtk::WidgetExt;
use pango;
+use std::rc::Rc;
+use std::sync::mpsc::Sender;
use url::Url;
use crate::backend::BKCommand;
use crate::util::markup_text;
-use std::sync::mpsc::channel;
-use std::sync::mpsc::TryRecvError;
-use std::sync::mpsc::{Receiver, Sender};
-
use crate::cache::download_to_cache;
use crate::cache::download_to_cache_username;
use crate::cache::download_to_cache_username_emote;
@@ -29,8 +24,8 @@ use crate::uitypes::MessageContent as Message;
use crate::uitypes::RowType;
use crate::widgets;
use crate::widgets::message_menu::MessageMenu;
-use crate::widgets::AudioPlayerWidget;
use crate::widgets::AvatarExt;
+use crate::widgets::{AudioPlayerWidget, PlayerExt, VideoPlayerWidget};
/* A message row in the room history */
#[derive(Clone, Debug)]
@@ -43,6 +38,7 @@ pub struct MessageBox {
gesture: gtk::GestureLongPress,
row: gtk::ListBoxRow,
image: Option<gtk::DrawingArea>,
+ video_player: Option<Rc<VideoPlayerWidget>>,
pub header: bool,
}
@@ -67,6 +63,7 @@ impl MessageBox {
gesture,
row,
image: None,
+ video_player: None,
header: true,
}
}
@@ -179,7 +176,8 @@ impl MessageBox {
RowType::Image => self.build_room_msg_image(msg),
RowType::Emote => self.build_room_msg_emote(msg),
RowType::Audio => self.build_room_audio_player(msg),
- RowType::Video | RowType::File => self.build_room_msg_file(msg),
+ RowType::Video => self.build_room_video_player(msg),
+ RowType::File => self.build_room_msg_file(msg),
_ => self.build_room_msg_body(msg),
};
@@ -353,7 +351,7 @@ impl MessageBox {
bx.pack_start(&image.widget, true, true, 0);
bx.show_all();
self.image = Some(image.widget);
- self.connect_image(msg);
+ self.connect_media_viewer(msg);
bx
}
@@ -375,42 +373,15 @@ impl MessageBox {
fn build_room_audio_player(&self, msg: &Message) -> gtk::Box {
let bx = gtk::Box::new(gtk::Orientation::Horizontal, 6);
- let player = widgets::AudioPlayerWidget::new();
- bx.set_opacity(0.3);
-
- let url = msg.url.clone().unwrap_or_default();
- let backend = self.backend.clone();
-
- let (tx, rx): (Sender<String>, Receiver<String>) = channel();
- backend
- .send(BKCommand::GetMediaAsync(
- self.server_url.clone(),
- url.clone(),
- tx,
- ))
- .unwrap();
-
- gtk::timeout_add(
- 50,
- clone!(player, bx => move || {
- match rx.try_recv() {
- Err(TryRecvError::Empty) => gtk::Continue(true),
- Err(TryRecvError::Disconnected) => {
- let msg = i18n("Could not retrieve file URI");
- /* FIXME: don't use APPOP! */
- APPOP!(show_error, (msg));
- gtk::Continue(true)
- },
- Ok(directory) => {
- info!("AUDIO DIRECTORY: {}", &directory);
- let uri = format!("file://{}", directory);
- player.initialize_stream(&uri);
- AudioPlayerWidget::init(&player);
- bx.set_opacity(1.0);
- gtk::Continue(false)
- }
- }
- }),
+ let player = AudioPlayerWidget::new();
+ let start_playing = false;
+ PlayerExt::initialize_stream(
+ &player,
+ &self.backend,
+ &msg.url.clone().unwrap_or_default(),
+ &self.server_url,
+ &bx,
+ start_playing,
);
let download_btn = gtk::Button::new_from_icon_name(
@@ -423,11 +394,39 @@ impl MessageBox {
download_btn.set_action_target_value(Some(&data));
download_btn.set_action_name(Some("room_history.save_as"));
- bx.pack_start(&player.container, false, true, 0);
+ let control_box = PlayerExt::get_controls_container(&player)
+ .expect("Every AudioPlayer must have controls.");
+ bx.pack_start(&control_box, false, true, 0);
bx.pack_start(&download_btn, false, false, 3);
bx
}
+ fn build_room_video_player(&mut self, msg: &Message) -> gtk::Box {
+ let with_controls = false;
+ let player = VideoPlayerWidget::new(with_controls);
+ let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
+ let start_playing = false;
+ PlayerExt::initialize_stream(
+ &player,
+ &self.backend,
+ &msg.url.clone().unwrap_or_default(),
+ &self.server_url,
+ &bx,
+ start_playing,
+ );
+ let video_widget = player.get_video_widget();
+ video_widget.set_size_request(-1, 390);
+ VideoPlayerWidget::auto_adjust_video_dimensions(&player);
+ bx.pack_start(&video_widget, true, true, 0);
+ self.connect_media_viewer(msg);
+ self.video_player = Some(player.clone());
+ bx
+ }
+
+ pub fn get_video_widget(&self) -> Option<Rc<VideoPlayerWidget>> {
+ self.video_player.clone()
+ }
+
fn build_room_msg_file(&self, msg: &Message) -> gtk::Box {
let bx = gtk::Box::new(gtk::Orientation::Horizontal, 12);
let btn_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
@@ -571,7 +570,7 @@ impl MessageBox {
None
}
- fn connect_image(&self, msg: &Message) -> Option<()> {
+ fn connect_media_viewer(&self, msg: &Message) -> Option<()> {
let data = glib::Variant::from(msg.id.as_str());
self.row.set_action_name(Some("app.open-media-viewer"));
self.row.set_action_target_value(Some(&data));
diff --git a/fractal-gtk/src/widgets/mod.rs b/fractal-gtk/src/widgets/mod.rs
index 6572c7ce..385431ba 100644
--- a/fractal-gtk/src/widgets/mod.rs
+++ b/fractal-gtk/src/widgets/mod.rs
@@ -33,6 +33,9 @@ pub use self::divider::NewMessageDivider;
pub use self::error_dialog as ErrorDialog;
pub use self::file_dialog as FileDialog;
pub use self::inline_player::AudioPlayerWidget;
+pub use self::inline_player::MediaPlayer;
+pub use self::inline_player::PlayerExt;
+pub use self::inline_player::VideoPlayerWidget;
pub use self::kicked_dialog::KickedDialog;
pub use self::login::LoginWidget;
pub use self::media_viewer::MediaViewer;
diff --git a/fractal-gtk/src/widgets/room_history.rs b/fractal-gtk/src/widgets/room_history.rs
index 83c02f3e..ae6e74e7 100644
--- a/fractal-gtk/src/widgets/room_history.rs
+++ b/fractal-gtk/src/widgets/room_history.rs
@@ -2,6 +2,8 @@ use chrono::DateTime;
use chrono::Datelike;
use chrono::Local;
use chrono::Timelike;
+use fragile::Fragile;
+use log::warn;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
@@ -15,9 +17,12 @@ use crate::uitypes::RowType;
use crate::globals;
use crate::widgets;
+use crate::widgets::{PlayerExt, VideoPlayerWidget};
use gio::ActionMapExt;
use gio::SimpleActionGroup;
use glib::source;
+use glib::SignalHandlerId;
+use glib::Source;
use gtk;
use gtk::prelude::*;
use url::Url;
@@ -25,7 +30,9 @@ use url::Url;
struct List {
list: VecDeque<Element>,
new_divider_index: Option<usize>,
+ playing_videos: Vec<(Rc<VideoPlayerWidget>, SignalHandlerId)>,
listbox: gtk::ListBox,
+ video_scroll_debounce: Option<source::SourceId>,
view: widgets::ScrollWidget,
}
@@ -34,51 +41,29 @@ impl List {
List {
list: VecDeque::new(),
new_divider_index: None,
+ playing_videos: Vec::new(),
listbox,
+ video_scroll_debounce: None,
view,
}
}
- pub fn add_top(&mut self, element: Element) -> Option<()> {
+ pub fn add_top(&mut self, element: Element) {
self.view.set_balance_top();
/* insert position is 1 because at position 0 is the spinner */
- match element {
- Element::Message(ref message) => {
- self.listbox
- .insert(message.widget.as_ref()?.get_listbox_row(), 1);
- }
- Element::NewDivider(ref divider) => {
- self.listbox.insert(divider.get_widget(), 1);
- }
- Element::DayDivider(ref divider) => {
- self.listbox.insert(divider, 1);
- }
- }
+ self.listbox.insert(element.get_listbox_row(), 1);
self.list.push_back(element);
self.view.set_kinetic_scrolling(true);
/* TODO: update the previous message:
* we need to update the previous row because it could be that we have to remove the header */
- None
}
- pub fn add_bottom(&mut self, element: Element) -> Option<()> {
- match element {
- Element::Message(ref message) => {
- self.listbox
- .insert(message.widget.as_ref()?.get_listbox_row(), -1);
- }
- Element::NewDivider(ref divider) => {
- self.listbox.insert(divider.get_widget(), -1);
- }
- Element::DayDivider(ref divider) => {
- self.listbox.insert(divider, -1);
- }
- }
+ pub fn add_bottom(&mut self, element: Element) {
+ self.listbox.insert(element.get_listbox_row(), -1);
if let Some(index) = self.new_divider_index {
self.new_divider_index = Some(index + 1);
}
self.list.push_front(element);
- None
}
fn create_new_message_divider(rows: Rc<RefCell<Self>>) -> widgets::NewMessageDivider {
@@ -95,6 +80,131 @@ impl List {
};
widgets::NewMessageDivider::new(i18n("New Messages").as_str(), remove_divider)
}
+ fn update_videos(&mut self) {
+ let visible = self.find_visible_videos();
+ let mut new_looped: Vec<(Rc<VideoPlayerWidget>, SignalHandlerId)> =
+ Vec::with_capacity(visible.len());
+
+ /* Once drain_filter is not nightly-only anymore, we can use drain_filter. */
+ for (player, handler_id) in self.playing_videos.drain(..) {
+ if visible.contains(&player) {
+ new_looped.push((player, handler_id));
+ } else {
+ player.stop_loop(handler_id);
+ }
+ }
+ for player in visible {
+ if !new_looped.iter().any(|(widget, _)| widget == &player) {
+ let handler_id = player.play_in_loop();
+ new_looped.push((player, handler_id));
+ }
+ }
+ self.playing_videos = new_looped;
+ }
+
+ fn find_visible_videos(&self) -> Vec<Rc<VideoPlayerWidget>> {
+ self.find_all_visible_indices()
+ .iter()
+ .filter_map(|&index| match self.list.get(index)? {
+ Element::Message(content) => match content.mtype {
+ RowType::Video => {
+ Some(content
+ .widget
+ .as_ref()?
+ .get_video_widget()
+ .expect("The widget of every MessageContent, whose mtype is RowType::Video, must
have a video_player."))
+ }
+ _ => None,
+ },
+ _ => None,
+ })
+ .collect()
+ }
+
+ fn find_all_visible_indices(&self) -> Vec<usize> {
+ let len = self.list.len();
+ let mut indices = Vec::new();
+ if len == 0 {
+ return indices;
+ }
+ if let Some(visible_index) = self.find_visible_index((0, len - 1)) {
+ indices.push(visible_index);
+ let upper = self.list.iter().enumerate().skip(visible_index + 1);
+ self.add_while_visible(&mut indices, upper);
+ let lower = self
+ .list
+ .iter()
+ .enumerate()
+ .rev()
+ .skip(self.list.len() - visible_index);
+ self.add_while_visible(&mut indices, lower);
+ }
+ indices
+ }
+
+ fn find_visible_index(&self, range: (usize, usize)) -> Option<usize> {
+ let middle_index = (range.0 + range.1) / 2;
+ let element = &self.list[middle_index];
+ let scrolled_window = self.view.get_scrolled_window();
+ let index = match get_rel_position(&scrolled_window, element) {
+ RelativePosition::AboveSight => {
+ if range.0 == range.1 {
+ return None;
+ } else {
+ self.find_visible_index((range.0, middle_index))
+ }
+ }
+ RelativePosition::InSight => Some(middle_index),
+ RelativePosition::BelowSight => {
+ if range.0 == range.1 {
+ return None;
+ } else {
+ self.find_visible_index((middle_index + 1, range.1))
+ }
+ }
+ };
+ index
+ }
+
+ fn add_while_visible<'a, T>(&self, indices: &mut Vec<usize>, iterator: T)
+ where
+ T: Iterator<Item = (usize, &'a Element)>,
+ {
+ let scrolled_window = self.view.get_scrolled_window();
+ for (index, element) in iterator {
+ match get_rel_position(&scrolled_window, element) {
+ RelativePosition::InSight => {
+ indices.push(index);
+ }
+ _ => {
+ break;
+ }
+ }
+ }
+ }
+}
+
+fn get_rel_position(scrolled_window: >k::ScrolledWindow, element: &Element) -> RelativePosition {
+ let widget = element.get_listbox_row();
+ let height_visible_area = gtk::WidgetExt::get_allocated_height(scrolled_window);
+ let height_widget = gtk::WidgetExt::get_allocated_height(widget);
+ let rel_y = gtk::WidgetExt::translate_coordinates(widget, scrolled_window, 0, 0)
+ .expect("Both scrolled_window and widget should be realized and share a common toplevel.")
+ .1;
+ if rel_y <= -height_widget {
+ RelativePosition::AboveSight
+ } else if rel_y < height_visible_area {
+ RelativePosition::InSight
+ } else {
+ RelativePosition::BelowSight
+ }
+}
+
+#[derive(Clone, Debug)]
+enum RelativePosition {
+ InSight,
+ AboveSight,
+ BelowSight,
}
/* These Enum contains all differnet types of rows the room history can have, e.g room message, new
@@ -106,6 +216,20 @@ enum Element {
DayDivider(gtk::ListBoxRow),
}
+impl Element {
+ fn get_listbox_row(&self) -> >k::ListBoxRow {
+ match self {
+ Element::Message(content) => content
+ .widget
+ .as_ref()
+ .expect("The content of every message element must have widget.")
+ .get_listbox_row(),
+ Element::NewDivider(widgets) => widgets.get_widget(),
+ Element::DayDivider(widget) => widget,
+ }
+ }
+}
+
pub struct RoomHistory {
/* Contains a list of msg ids to keep track of the displayed messages */
rows: Rc<RefCell<List>>,
@@ -134,14 +258,18 @@ impl RoomHistory {
/* Add the action groupe to the room_history */
listbox.insert_action_group("room_history", Some(&actions));
-
- Some(RoomHistory {
+ let mut rh = RoomHistory {
rows: Rc::new(RefCell::new(List::new(scroll, listbox))),
backend: op.backend.clone(),
server_url: op.login_data.clone()?.server_url.clone(),
source_id: Rc::new(RefCell::new(None)),
queue: Rc::new(RefCell::new(VecDeque::new())),
- })
+ };
+
+ rh.connect_video_auto_play();
+ rh.connect_video_focus();
+
+ Some(rh)
}
pub fn create(&mut self, mut messages: Vec<MessageContent>) -> Option<()> {
@@ -158,9 +286,124 @@ impl RoomHistory {
/* Add the rest of the messages after the new message divider */
self.add_new_messages_in_batch(bottom);
+ let weak_rows = Rc::downgrade(&self.rows);
+ let id = timeout_add(250, move || {
+ weak_rows.upgrade().map(|rows| {
+ rows.borrow_mut().update_videos();
+ });
+ Continue(false)
+ });
+ self.rows.borrow_mut().video_scroll_debounce = Some(id);
+
None
}
+ fn connect_video_auto_play(&self) {
+ let scrollbar = self
+ .rows
+ .borrow()
+ .view
+ .get_scrolled_window()
+ .get_vscrollbar()
+ .expect("The scrolled window must have a vertical scrollbar.")
+ .downcast::<gtk::Scrollbar>()
+ .unwrap();
+ let weak_rows = Rc::downgrade(&self.rows);
+ scrollbar.connect_value_changed(move |_| {
+ weak_rows.upgrade().map(|rows| {
+ let weak_rows_inner = weak_rows.clone();
+ let new_id = timeout_add(250, move || {
+ weak_rows_inner.upgrade().map(|rows| {
+ rows.borrow_mut().update_videos();
+ rows.borrow_mut().video_scroll_debounce = None;
+ });
+ Continue(false)
+ });
+ if let Some(old_id) = rows.borrow_mut().video_scroll_debounce.replace(new_id) {
+ let _ = Source::remove(old_id);
+ }
+ });
+ });
+ }
+
+ fn connect_video_focus(&mut self) {
+ let scrolled_window = self.rows.borrow().view.get_scrolled_window();
+ let weak_rows = Rc::downgrade(&self.rows);
+ scrolled_window.connect_map(move |_| {
+ /* The user has navigated back into the room history */
+ weak_rows.upgrade().map(|rows| {
+ let len = rows.borrow().playing_videos.len();
+ if len != 0 {
+ warn!(
+ "{:?} videos were playing while the room history was not displayed.",
+ len
+ );
+ for (player, hander_id) in rows.borrow_mut().playing_videos.drain(..) {
+ player.stop_loop(hander_id);
+ }
+ }
+ let visible_videos = rows.borrow().find_visible_videos();
+ let mut videos = Vec::with_capacity(visible_videos.len());
+ for player in visible_videos {
+ let handler_id = player.play_in_loop();
+ videos.push((player, handler_id));
+ }
+ rows.borrow_mut().playing_videos = videos;
+ });
+ });
+
+ let weak_rows = Rc::downgrade(&self.rows);
+ scrolled_window.connect_unmap(move |_| {
+ /* The user has navigated out of the room history */
+ weak_rows.upgrade().map(|rows| {
+ if let Some(id) = rows.borrow_mut().video_scroll_debounce.take() {
+ let _ = Source::remove(id);
+ }
+ for (player, handler_id) in rows.borrow_mut().playing_videos.drain(..) {
+ player.stop_loop(handler_id);
+ }
+ });
+ });
+
+ let weak_rows = Rc::downgrade(&self.rows);
+ scrolled_window.connect_state_flags_changed(move |window, flag| {
+ if window.get_mapped() {
+ /* The room history is being displayed */
+ weak_rows.upgrade().map(|rows| {
+ let focussed = gtk::StateFlags::BACKDROP;
+ if flag.contains(focussed) {
+ /* Fractal has been focussed */
+ let len = rows.borrow().playing_videos.len();
+ if len != 0 {
+ warn!(
+ "{:?} videos were playing while Fractal was focussed out.",
+ len
+ );
+ for (player, handler_id) in rows.borrow_mut().playing_videos.drain(..) {
+ player.stop_loop(handler_id);
+ }
+ }
+ let visible_videos = rows.borrow().find_visible_videos();
+ let mut videos = Vec::with_capacity(visible_videos.len());
+ for player in visible_videos {
+ let handler_id = player.play_in_loop();
+ videos.push((player, handler_id));
+ }
+ rows.borrow_mut().playing_videos = videos;
+ } else {
+ /* Fractal has been unfocussed */
+ if let Some(id) = rows.borrow_mut().video_scroll_debounce.take() {
+ let _ = Source::remove(id);
+ }
+ for (player, handler_id) in rows.borrow_mut().playing_videos.drain(..) {
+ player.stop_loop(handler_id);
+ }
+ }
+ });
+ }
+ });
+ }
+
fn run_queue(&mut self) -> Option<()> {
let backend = self.backend.clone();
let queue = self.queue.clone();
@@ -221,6 +464,7 @@ impl RoomHistory {
has_header,
backend.clone(),
server_url.clone(),
+ &rows,
));
rows.borrow_mut().add_top(Element::Message(item));
if let Some(day_divider) = day_divider {
@@ -286,6 +530,7 @@ impl RoomHistory {
has_header,
self.backend.clone(),
self.server_url.clone(),
+ &self.rows,
);
item.widget = Some(b);
rows.add_bottom(Element::Message(item));
@@ -308,7 +553,7 @@ impl RoomHistory {
let msg_widget = msg.widget.clone()?;
let msg_sender = msg.sender.clone();
msg.msg.redacted = true;
- rows.listbox.remove(msg_widget.get_listbox_row()?);
+ rows.listbox.remove(msg_widget.get_listbox_row());
// If the redacted message was a header message let's set
// the header on the next non-redacted message instead.
@@ -346,7 +591,6 @@ impl RoomHistory {
for item in messages {
self.add_new_message(item);
}
-
None
}
@@ -380,12 +624,31 @@ fn create_row(
has_header: bool,
backend: Sender<BKCommand>,
server_url: Url,
+ rows: &Rc<RefCell<List>>,
) -> widgets::MessageBox {
/* we need to create a message with the username, so that we don't have to pass
* all information to the widget creating each row */
let mut mb = widgets::MessageBox::new(backend, server_url);
mb.create(&row, has_header && row.mtype != RowType::Emote);
+ match row.mtype {
+ RowType::Video => {
+ /* The followign callback requires `Send` but is handled by the gtk main loop */
+ let fragile_rows = Fragile::new(Rc::downgrade(rows));
+ PlayerExt::get_player(&mb.get_video_widget()
+ .expect("The widget of every MessageContent, whose mtype is RowType::Video, must have a
video_player."))
+ .connect_uri_loaded(move |player, _| {
+ fragile_rows.get().upgrade().map(|rows| {
+ if rows.borrow().playing_videos.iter().any(|(player_widget, _)| {
+ &PlayerExt::get_player(&player_widget) == player
+ }) {
+ player.play();
+ }
+ });
+ });
+ }
+ _ => {}
+ }
mb
}
diff --git a/fractal-gtk/src/widgets/scroll_widget.rs b/fractal-gtk/src/widgets/scroll_widget.rs
index e55a2cf9..b1a1130d 100644
--- a/fractal-gtk/src/widgets/scroll_widget.rs
+++ b/fractal-gtk/src/widgets/scroll_widget.rs
@@ -266,6 +266,11 @@ impl ScrollWidget {
pub fn get_container(&self) -> gtk::Widget {
self.widgets.container.clone()
}
+
+ pub fn get_scrolled_window(&self) -> gtk::ScrolledWindow {
+ self.widgets.view.clone()
+ }
+
pub fn reset_request_sent(&self) {
self.request_sent.set(false);
self.widgets.spinner.stop();
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]