fractal/utils/media/
mod.rs1use std::{str::FromStr, time::Duration};
4
5use gettextrs::gettext;
6use gtk::{gio, glib, prelude::*};
7use mime::Mime;
8use ruma::UInt;
9
10use crate::utils::OneshotNotifier;
11
12pub(crate) mod audio;
13pub(crate) mod image;
14pub(crate) mod video;
15
16pub(crate) fn filename_for_mime(mime_type: Option<&str>, fallback: Option<mime::Name>) -> String {
24 let (type_, extension) =
25 if let Some(mime) = mime_type.and_then(|m| m.parse::<mime::Mime>().ok()) {
26 let extension =
27 mime_guess::get_mime_extensions(&mime).map(|extensions| extensions[0].to_owned());
28
29 (Some(mime.type_().as_str().to_owned()), extension)
30 } else {
31 (fallback.map(|type_| type_.as_str().to_owned()), None)
32 };
33
34 let name = match type_.as_deref() {
35 Some("image") => gettext("image"),
37 Some("video") => gettext("video"),
39 Some("audio") => gettext("audio"),
41 _ => gettext("file"),
43 };
44
45 extension
46 .map(|extension| format!("{name}.{extension}"))
47 .unwrap_or(name)
48}
49
50pub(crate) struct FileInfo {
52 pub(crate) mime: Mime,
54 pub(crate) filename: String,
56 pub(crate) size: Option<u32>,
58}
59
60impl FileInfo {
61 pub(crate) async fn try_from_file(file: &gio::File) -> Result<FileInfo, glib::Error> {
63 let attributes: &[&str] = &[
64 gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
65 gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
66 gio::FILE_ATTRIBUTE_STANDARD_SIZE,
67 ];
68
69 let info = file
71 .query_info_future(
72 &attributes.join(","),
73 gio::FileQueryInfoFlags::NONE,
74 glib::Priority::DEFAULT,
75 )
76 .await?;
77
78 let mime = info
79 .content_type()
80 .and_then(|content_type| Mime::from_str(&content_type).ok())
81 .unwrap_or(mime::APPLICATION_OCTET_STREAM);
82
83 let filename = info.display_name().to_string();
84
85 let raw_size = info.size();
86 let size = if raw_size >= 0 {
87 Some(raw_size.try_into().unwrap_or(u32::MAX))
88 } else {
89 None
90 };
91
92 Ok(FileInfo {
93 mime,
94 filename,
95 size,
96 })
97 }
98}
99
100async fn load_gstreamer_media_info(file: &gio::File) -> Option<gst_pbutils::DiscovererInfo> {
102 let timeout = gst::ClockTime::from_seconds(15);
103 let discoverer = gst_pbutils::Discoverer::new(timeout).ok()?;
104
105 let notifier = OneshotNotifier::new("load_gstreamer_media_info");
106 let receiver = notifier.listen();
107
108 discoverer.connect_discovered(move |_, info, _| {
109 notifier.notify_value(Some(info.clone()));
110 });
111
112 discoverer.start();
113 discoverer.discover_uri_async(&file.uri()).ok()?;
114
115 let media_info = receiver.await;
116 discoverer.stop();
117
118 media_info
119}
120
121#[derive(Debug, thiserror::Error)]
123#[error(transparent)]
124pub(crate) enum MediaFileError {
125 Sdk(#[from] matrix_sdk::Error),
127 File(#[from] std::io::Error),
129 #[error("Could not access session")]
133 NoSession,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub(crate) struct FrameDimensions {
139 pub(crate) width: u32,
141 pub(crate) height: u32,
143}
144
145impl FrameDimensions {
146 pub(crate) fn from_options(width: Option<UInt>, height: Option<UInt>) -> Option<Self> {
148 Some(Self {
149 width: width?.try_into().ok()?,
150 height: height?.try_into().ok()?,
151 })
152 }
153
154 pub(crate) fn dimension_for_orientation(self, orientation: gtk::Orientation) -> u32 {
156 match orientation {
157 gtk::Orientation::Vertical => self.height,
158 _ => self.width,
159 }
160 }
161
162 pub(crate) fn dimension_for_other_orientation(self, orientation: gtk::Orientation) -> u32 {
164 match orientation {
165 gtk::Orientation::Vertical => self.width,
166 _ => self.height,
167 }
168 }
169
170 pub(crate) fn ge(self, other: Self) -> bool {
176 self.width >= other.width || self.height >= other.height
177 }
178
179 pub(crate) const fn increase_by(mut self, value: u32) -> Self {
181 self.width = self.width.saturating_add(value);
182 self.height = self.height.saturating_add(value);
183 self
184 }
185
186 pub(crate) const fn scale(mut self, factor: u32) -> Self {
188 self.width = self.width.saturating_mul(factor);
189 self.height = self.height.saturating_mul(factor);
190 self
191 }
192
193 pub(crate) fn scale_to_fit(self, requested: Self, content_fit: gtk::ContentFit) -> Self {
196 let w_ratio = f64::from(self.width) / f64::from(requested.width);
197 let h_ratio = f64::from(self.height) / f64::from(requested.height);
198
199 let resize_from_width = match content_fit {
200 gtk::ContentFit::Contain | gtk::ContentFit::ScaleDown => w_ratio > h_ratio,
202 gtk::ContentFit::Cover => w_ratio < h_ratio,
204 _ => return requested,
206 };
207 let downscale_only = content_fit == gtk::ContentFit::ScaleDown;
208
209 #[allow(clippy::cast_sign_loss)] let (width, height) = if resize_from_width {
211 if downscale_only && w_ratio <= 1.0 {
212 return self;
214 }
215
216 let new_height = f64::from(self.height) / w_ratio;
217 (requested.width, new_height as u32)
218 } else {
219 if downscale_only && h_ratio <= 1.0 {
220 return self;
222 }
223
224 let new_width = f64::from(self.width) / h_ratio;
225 (new_width as u32, requested.height)
226 };
227
228 Self { width, height }
229 }
230}
231
232pub(crate) fn time_to_label(time: &Duration) -> String {
235 let mut time = time.as_secs();
236
237 let sec = time % 60;
238 time -= sec;
239 let min = (time % (60 * 60)) / 60;
240 time -= min * 60;
241 let hour = time / (60 * 60);
242
243 if hour > 0 {
244 format!("{hour}:{min:02}:{sec:02}")
247 } else {
248 format!("{min:02}:{sec:02}")
251 }
252}