Skip to main content

fractal/utils/media/
mod.rs

1//! Collection of methods for media.
2
3use 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
16/// Get a default filename for a mime type.
17///
18/// Tries to guess the file extension, but it might not find it.
19///
20/// If the mime type is unknown, it uses the name for `fallback`. The fallback
21/// mime types that are recognized are `mime::IMAGE`, `mime::VIDEO` and
22/// `mime::AUDIO`, other values will behave the same as `None`.
23pub(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        // Translators: Default name for image files.
36        Some("image") => gettext("image"),
37        // Translators: Default name for video files.
38        Some("video") => gettext("video"),
39        // Translators: Default name for audio files.
40        Some("audio") => gettext("audio"),
41        // Translators: Default name for files.
42        _ => gettext("file"),
43    };
44
45    extension
46        .map(|extension| format!("{name}.{extension}"))
47        .unwrap_or(name)
48}
49
50/// Information about a file.
51pub(crate) struct FileInfo {
52    /// The mime type of the file.
53    pub(crate) mime: Mime,
54    /// The name of the file.
55    pub(crate) filename: String,
56    /// The size of the file in bytes.
57    pub(crate) size: Option<u32>,
58}
59
60impl FileInfo {
61    /// Try to load information about the given file.
62    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        // Read mime type.
70        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
100/// Load information for the given media file.
101async 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/// All errors that can occur when downloading a media to a file.
122#[derive(Debug, thiserror::Error)]
123#[error(transparent)]
124pub(crate) enum MediaFileError {
125    /// An error occurred when downloading the media.
126    Sdk(#[from] matrix_sdk::Error),
127    /// An error occurred when writing the media to a file.
128    File(#[from] std::io::Error),
129    /// We could not access the Matrix client via the [`Session`].
130    ///
131    /// [`Session`]: crate::session::Session
132    #[error("Could not access session")]
133    NoSession,
134}
135
136/// The dimensions of a frame.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub(crate) struct FrameDimensions {
139    /// The width of the frame.
140    pub(crate) width: u32,
141    /// The height of the frame.
142    pub(crate) height: u32,
143}
144
145impl FrameDimensions {
146    /// Construct a `FrameDimensions` from the given optional dimensions.
147    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    /// Get the dimension for the given orientation.
155    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    /// Get the dimension for the other orientation than the given one.
163    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    /// Whether these dimensions are greater than or equal to the given
171    /// dimensions.
172    ///
173    /// Returns `true` if either `width` or `height` is bigger than or equal to
174    /// the one in the other dimensions.
175    pub(crate) fn ge(self, other: Self) -> bool {
176        self.width >= other.width || self.height >= other.height
177    }
178
179    /// Increase both of these dimensions by the given value.
180    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    /// Scale these dimensions with the given factor.
187    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    /// Scale these dimensions to fit into the requested dimensions while
194    /// preserving the aspect ratio and respecting the given content fit.
195    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            // The largest ratio wins so the frame fits into the requested dimensions.
201            gtk::ContentFit::Contain | gtk::ContentFit::ScaleDown => w_ratio > h_ratio,
202            // The smallest ratio wins so the frame fills the requested dimensions.
203            gtk::ContentFit::Cover => w_ratio < h_ratio,
204            // We just return the requested dimensions since we do not care about the ratio.
205            _ => return requested,
206        };
207        let downscale_only = content_fit == gtk::ContentFit::ScaleDown;
208
209        #[allow(clippy::cast_sign_loss)] // We need to convert the f64 to a u32.
210        let (width, height) = if resize_from_width {
211            if downscale_only && w_ratio <= 1.0 {
212                // We do not want to upscale.
213                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                // We do not want to upscale.
221                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
232/// Get the string representation of the given elapsed time to present it in a
233/// media player.
234pub(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        // FIXME: Find how to localize this.
245        // hour:minutes:seconds
246        format!("{hour}:{min:02}:{sec:02}")
247    } else {
248        // FIXME: Find how to localize this.
249        // minutes:seconds
250        format!("{min:02}:{sec:02}")
251    }
252}