fractal/utils/media/
mod.rsuse std::{cell::Cell, str::FromStr, sync::Mutex};
use gettextrs::gettext;
use gtk::{gio, glib, prelude::*};
use matrix_sdk::attachment::BaseAudioInfo;
use mime::Mime;
use ruma::UInt;
pub mod image;
pub mod video;
pub fn filename_for_mime(mime_type: Option<&str>, fallback: Option<mime::Name>) -> String {
let (type_, extension) =
if let Some(mime) = mime_type.and_then(|m| m.parse::<mime::Mime>().ok()) {
let extension =
mime_guess::get_mime_extensions(&mime).map(|extensions| extensions[0].to_owned());
(Some(mime.type_().as_str().to_owned()), extension)
} else {
(fallback.map(|type_| type_.as_str().to_owned()), None)
};
let name = match type_.as_deref() {
Some("image") => gettext("image"),
Some("video") => gettext("video"),
Some("audio") => gettext("audio"),
_ => gettext("file"),
};
extension
.map(|extension| format!("{name}.{extension}"))
.unwrap_or(name)
}
pub struct FileInfo {
pub mime: Mime,
pub filename: String,
pub size: Option<u32>,
}
pub async fn load_file(file: &gio::File) -> Result<(Vec<u8>, FileInfo), glib::Error> {
let attributes: &[&str] = &[
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
gio::FILE_ATTRIBUTE_STANDARD_SIZE,
];
let info = file
.query_info_future(
&attributes.join(","),
gio::FileQueryInfoFlags::NONE,
glib::Priority::DEFAULT,
)
.await?;
let mime = info
.content_type()
.and_then(|content_type| Mime::from_str(&content_type).ok())
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
let filename = info.display_name().to_string();
let raw_size = info.size();
let size = if raw_size >= 0 {
Some(raw_size.try_into().unwrap_or(u32::MAX))
} else {
None
};
let (data, _) = file.load_contents_future().await?;
Ok((
data.into(),
FileInfo {
mime,
filename,
size,
},
))
}
async fn load_gstreamer_media_info(file: &gio::File) -> Option<gst_pbutils::DiscovererInfo> {
let timeout = gst::ClockTime::from_seconds(15);
let discoverer = gst_pbutils::Discoverer::new(timeout).ok()?;
let (sender, receiver) = futures_channel::oneshot::channel();
let sender = Mutex::new(Cell::new(Some(sender)));
discoverer.connect_discovered(move |_, info, _| {
if let Some(sender) = sender.lock().unwrap().take() {
sender.send(info.clone()).unwrap();
}
});
discoverer.start();
discoverer.discover_uri_async(&file.uri()).ok()?;
let media_info = receiver.await.unwrap();
discoverer.stop();
Some(media_info)
}
pub async fn load_audio_info(file: &gio::File) -> BaseAudioInfo {
let mut info = BaseAudioInfo {
duration: None,
size: None,
};
let Some(media_info) = load_gstreamer_media_info(file).await else {
return info;
};
info.duration = media_info.duration().map(Into::into);
info
}
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum MediaFileError {
Sdk(#[from] matrix_sdk::Error),
File(#[from] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameDimensions {
pub width: u32,
pub height: u32,
}
impl FrameDimensions {
pub(crate) fn from_options(width: Option<UInt>, height: Option<UInt>) -> Option<Self> {
Some(Self {
width: width?.try_into().ok()?,
height: height?.try_into().ok()?,
})
}
pub(crate) fn dimension_for_orientation(self, orientation: gtk::Orientation) -> u32 {
match orientation {
gtk::Orientation::Vertical => self.height,
_ => self.width,
}
}
pub(crate) fn dimension_for_other_orientation(self, orientation: gtk::Orientation) -> u32 {
match orientation {
gtk::Orientation::Vertical => self.width,
_ => self.height,
}
}
pub(crate) fn ge(self, other: Self) -> bool {
self.width >= other.width || self.height >= other.height
}
pub(crate) const fn increase_by(mut self, value: u32) -> Self {
self.width = self.width.saturating_add(value);
self.height = self.height.saturating_add(value);
self
}
pub(crate) const fn scale(mut self, factor: u32) -> Self {
self.width = self.width.saturating_mul(factor);
self.height = self.height.saturating_mul(factor);
self
}
pub(crate) fn scale_to_fit(self, requested: Self, content_fit: gtk::ContentFit) -> Self {
let w_ratio = f64::from(self.width) / f64::from(requested.width);
let h_ratio = f64::from(self.height) / f64::from(requested.height);
let resize_from_width = match content_fit {
gtk::ContentFit::Contain | gtk::ContentFit::ScaleDown => w_ratio > h_ratio,
gtk::ContentFit::Cover => w_ratio < h_ratio,
_ => return requested,
};
let downscale_only = content_fit == gtk::ContentFit::ScaleDown;
#[allow(clippy::cast_sign_loss)] let (width, height) = if resize_from_width {
if downscale_only && w_ratio <= 1.0 {
return self;
}
let new_height = f64::from(self.height) / w_ratio;
(requested.width, new_height as u32)
} else {
if downscale_only && h_ratio <= 1.0 {
return self;
}
let new_width = f64::from(self.width) / h_ratio;
(new_width as u32, requested.height)
};
Self { width, height }
}
}