fractal/components/media/
video_player.rsuse adw::{prelude::*, subclass::prelude::*};
use gtk::{gio, glib, glib::clone, CompositeTemplate};
use tracing::{error, warn};
use super::video_player_renderer::VideoPlayerRenderer;
use crate::utils::LoadingState;
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 {
#[template_child]
video: TemplateChild<gtk::Picture>,
#[template_child]
timestamp: TemplateChild<gtk::Label>,
#[template_child]
player: TemplateChild<gst_play::Play>,
file: RefCell<Option<gio::File>>,
#[property(get, set = Self::set_compact, explicit_notify)]
compact: Cell<bool>,
#[property(get, builder(LoadingState::default()))]
state: Cell<LoadingState>,
pub(super) error: RefCell<Option<glib::Error>>,
duration: Cell<Option<gst::ClockTime>>,
bus_guard: OnceCell<gst::bus::BusWatchGuard>,
}
#[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 bus_guard = self
.player
.message_bus()
.add_watch_local(clone!(
#[weak(rename_to = imp)]
self,
#[upgrade_or]
glib::ControlFlow::Break,
move |_, message| {
if let Ok(message) = gst_play::PlayMessage::parse(message) {
imp.handle_message(message);
}
glib::ControlFlow::Continue
}
))
.expect("adding message bus watch succeeds");
self.bus_guard
.set(bus_guard)
.expect("bus guard is uninitialized");
}
fn dispose(&self) {
self.player.message_bus().set_flushing(true);
}
}
impl WidgetImpl for VideoPlayer {
fn map(&self) {
self.parent_map();
if self.state.get() != LoadingState::Error {
self.player.play();
}
}
fn unmap(&self) {
self.player.stop();
self.parent_unmap();
}
}
impl BinImpl for VideoPlayer {}
impl VideoPlayer {
fn set_compact(&self, compact: bool) {
if self.compact.get() == compact {
return;
}
self.compact.set(compact);
self.update_timestamp();
self.obj().notify_compact();
}
fn set_state(&self, state: LoadingState) {
if self.state.get() == state {
return;
}
self.state.set(state);
self.obj().notify_state();
}
pub(super) fn play_video_file(&self, file: gio::File) {
let uri = file.uri();
self.file.replace(Some(file));
self.set_duration(None);
self.set_state(LoadingState::Loading);
self.player.set_uri(Some(uri.as_ref()));
self.player.set_audio_track_enabled(false);
if self.obj().is_mapped() {
self.player.play();
} else {
self.player.pause();
}
}
fn handle_message(&self, message: gst_play::PlayMessage) {
match message {
gst_play::PlayMessage::StateChanged { state } => {
if matches!(
state,
gst_play::PlayState::Playing | gst_play::PlayState::Paused
) {
self.set_state(LoadingState::Ready);
}
}
gst_play::PlayMessage::DurationChanged { duration } => {
self.set_duration(duration);
}
gst_play::PlayMessage::Warning { error, .. } => {
warn!("Warning playing video: {error}");
}
gst_play::PlayMessage::Error { error, .. } => {
error!("Error playing video: {error}");
self.error.replace(Some(error));
self.set_state(LoadingState::Error);
}
_ => {}
}
}
fn set_duration(&self, duration: Option<gst::ClockTime>) {
if self.duration.get() == duration {
return;
}
self.duration.set(duration);
self.update_timestamp();
}
fn update_timestamp(&self) {
let visible_duration = self.duration.get().filter(|_| !self.compact.get());
let is_visible = visible_duration.is_some();
if let Some(duration) = visible_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);
let label = if hour > 0 {
format!("{hour}:{min:02}:{sec:02}")
} else {
format!("{min:02}:{sec:02}")
};
self.timestamp.set_label(&label);
}
self.timestamp.set_visible(is_visible);
}
}
}
glib::wrapper! {
pub struct VideoPlayer(ObjectSubclass<imp::VideoPlayer>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl VideoPlayer {
pub fn new() -> Self {
glib::Object::new()
}
pub(crate) fn play_video_file(&self, file: gio::File) {
self.imp().play_video_file(file);
}
pub(crate) fn error(&self) -> Option<glib::Error> {
self.imp().error.borrow().clone()
}
}