Coverage Report

Created: 2024-02-20 21:15

/builds/xfbs/cindy/src/media.rs
Line
Count
Source (jump to first uncovered line)
1
use crate::Tag;
2
use anyhow::{anyhow, Result};
3
use chrono::NaiveTime;
4
use ffmpeg_next::{
5
    self as ffmpeg,
6
    codec::context::Context,
7
    format::{context::Input, input},
8
    util::log::{set_level, Level},
9
};
10
use serde::{Deserialize, Serialize};
11
use std::{collections::BTreeSet, path::Path};
12
use strum::Display;
13
14
pub fn ffmpeg_init() -> Result<()> {
15
0
    ffmpeg::init()?;
16
0
    set_level(Level::Quiet);
17
0
    Ok(())
18
0
}
19
20
52
#[derive(
C26
lon
e26
,
D26
ebu
g26
,
P26
artialE
q26
,
S0
erializ
e0
, Deserialize)]
21
#[serde(rename_all = "snake_case", tag = "media")]
22
pub enum MediaInfo {
23
    Image(ImageInfo),
24
    Video(VideoInfo),
25
    Audio(AudioInfo),
26
}
27
28
impl From<ImageInfo> for MediaInfo {
29
1
    fn from(info: ImageInfo) -> Self {
30
1
        MediaInfo::Image(info)
31
1
    }
32
}
33
34
impl From<VideoInfo> for MediaInfo {
35
1
    fn from(info: VideoInfo) -> Self {
36
1
        MediaInfo::Video(info)
37
1
    }
38
}
39
40
impl From<AudioInfo> for MediaInfo {
41
1
    fn from(info: AudioInfo) -> Self {
42
1
        MediaInfo::Audio(info)
43
1
    }
44
}
45
46
19
fn resolution(width: u64, height: u64) -> &'static str {
47
19
    let min: u64 = width.min(height);
48
19
    match min {
49
19
        
min2
if min >= 4320 =>
"8k"2
,
50
17
        
min2
if min >= 2160 =>
"4k"2
,
51
15
        
min2
if min >= 1440 =>
"2k"2
,
52
13
        
min4
if min >= 1080 =>
"fullhd"4
,
53
9
        
min2
if min >= 720 =>
"hd"2
,
54
7
        
min2
if min >= 480 =>
"sd"2
,
55
5
        _ => "low",
56
    }
57
19
}
58
59
3
fn durationgroup(duration: u64) -> &'static str {
60
3
    match duration {
61
3
        duration if duration <= 60 => "short",
62
0
        duration if duration <= (3 * 60) => "mediumshort",
63
0
        duration if duration <= (10 * 60) => "medium",
64
0
        duration if duration <= (30 * 60) => "mediumlong",
65
0
        duration if duration <= (60 * 60) => "long",
66
0
        _ => "extended",
67
    }
68
3
}
69
70
impl MediaInfo {
71
6
    pub fn tags(&self) -> BTreeSet<Tag> {
72
6
        match self {
73
3
            MediaInfo::Image(media) => media.tags(),
74
2
            MediaInfo::Video(media) => media.tags(),
75
1
            MediaInfo::Audio(media) => media.tags(),
76
        }
77
6
    }
78
}
79
80
63
#[derive(
Clone9
,
Debug9
,
P9
artialE
q9
,
S0
erializ
e0
,
D36
eserialize)]
81
pub struct ImageInfo {
82
    format: ImageFormat,
83
    width: u64,
84
    height: u64,
85
}
86
87
impl ImageInfo {
88
3
    fn tags(&self) -> BTreeSet<Tag> {
89
3
        [
90
3
            Tag::new("media".into(), "image".into()),
91
3
            Tag::new("width".into(), self.width.to_string()),
92
3
            Tag::new("height".into(), self.height.to_string()),
93
3
            Tag::new(
94
3
                "resolution".into(),
95
3
                resolution(self.width, self.height).into(),
96
3
            ),
97
3
            self.format.tag(),
98
3
        ]
99
3
        .into()
100
3
    }
101
}
102
103
153
#[derive(
Clone17
,
Debug17
,
P17
artialE
q17
,
S0
erializ
e0
,
D85
eserialize)]
104
pub struct VideoInfo {
105
    format: VideoFormat,
106
    width: u64,
107
    height: u64,
108
    duration: u64,
109
}
110
111
impl VideoInfo {
112
2
    fn tags(&self) -> BTreeSet<Tag> {
113
2
        [
114
2
            Tag::new("media".into(), "video".into()),
115
2
            Tag::new("width".into(), self.width.to_string()),
116
2
            Tag::new("height".into(), self.height.to_string()),
117
2
            Tag::new("duration".into(), self.duration.to_string()),
118
2
            Tag::new("durationgroup".into(), durationgroup(self.duration).into()),
119
2
            Tag::new(
120
2
                "resolution".into(),
121
2
                resolution(self.width, self.height).into(),
122
2
            ),
123
2
            self.format.tag(),
124
2
        ]
125
2
        .into()
126
2
    }
127
}
128
129
0
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
130
pub struct AudioInfo {
131
    format: AudioFormat,
132
    duration: u64,
133
}
134
135
impl AudioInfo {
136
1
    fn tags(&self) -> BTreeSet<Tag> {
137
1
        [
138
1
            Tag::new("media".into(), "audio".into()),
139
1
            Tag::new("duration".into(), self.duration.to_string()),
140
1
            Tag::new("durationgroup".into(), durationgroup(self.duration).into()),
141
1
            self.format.tag(),
142
1
        ]
143
1
        .into()
144
1
    }
145
}
146
147
18
#[derive(
D3
ispla
y3
,
C9
lon
e9
,
D9
ebu
g9
,
PartialEq9
, Eq,
S0
erializ
e0
, Deserialize)]
148
#[strum(serialize_all = "snake_case")]
149
#[serde(rename_all = "snake_case")]
150
pub enum ImageFormat {
151
    Jpg,
152
    Png,
153
    Webp,
154
}
155
156
34
#[derive(
D2
ispla
y2
,
C17
lon
e17
,
D17
ebu
g17
,
PartialEq17
, Eq,
S0
erializ
e0
, Deserialize)]
157
#[strum(serialize_all = "snake_case")]
158
#[serde(rename_all = "snake_case")]
159
pub enum VideoFormat {
160
    Gif,
161
    Mp4,
162
    Avi,
163
    Mpeg,
164
    Ts,
165
    Mkv,
166
    Mov,
167
    Ogg,
168
    Wmv,
169
}
170
171
1
#[derive(Display, 
C0
lon
e0
,
D0
ebu
g0
,
PartialEq0
, Eq,
S0
erializ
e0
,
D0
eserializ
e0
)]
172
#[strum(serialize_all = "snake_case")]
173
#[serde(rename_all = "snake_case")]
174
pub enum AudioFormat {
175
    Mp3,
176
    M4a,
177
}
178
179
trait FormatTag: std::fmt::Display {
180
6
    fn tag(&self) -> Tag {
181
6
        Tag::new("format".into(), self.to_string())
182
6
    }
183
}
184
185
impl FormatTag for ImageFormat {}
186
impl FormatTag for AudioFormat {}
187
impl FormatTag for VideoFormat {}
188
189
43
pub fn media_info(path: &Path) -> Result<MediaInfo> {
190
43
    let 
file29
= input(&path)
?14
;
191
29
    match file.format().name() {
192
29
        "image2" | 
"jpeg_pipe"27
|
"webp_pipe"25
|
"png_pipe"22
=> {
193
11
            image_info(&file).map(MediaInfo::Image)
194
        }
195
18
        "asf"
196
17
        | "ogg"
197
16
        | "mpeg"
198
15
        | "matroska,webm"
199
13
        | "avi"
200
10
        | "mov,mp4,m4a,3gp,3g2,mj2"
201
6
        | "gif"
202
18
        | 
"mpegts"3
=> video_info(&file).map(MediaInfo::Video),
203
0
        format => Err(anyhow!("Unknown format {format}")),
204
    }
205
43
}
206
207
18
fn video_info(input: &Input) -> Result<VideoInfo> {
208
18
    let format = match input.format().name() {
209
18
        "gif" => 
VideoFormat::Gif3
,
210
15
        "mpegts" => 
VideoFormat::Ts3
,
211
12
        "mpeg" => 
VideoFormat::Mpeg1
,
212
11
        "mov,mp4,m4a,3gp,3g2,mj2" => 
VideoFormat::Mov4
,
213
7
        "avi" => 
VideoFormat::Avi3
,
214
4
        "matroska,webm" => 
VideoFormat::Mkv2
,
215
2
        "asf" => 
VideoFormat::Wmv1
,
216
1
        "ogg" => VideoFormat::Ogg,
217
0
        format => return Err(anyhow!("Unknown format {format}")),
218
    };
219
220
18
    let mut info = VideoInfo {
221
18
        format,
222
18
        width: 0,
223
18
        height: 0,
224
18
        duration: 0,
225
18
    };
226
227
23
    for stream in input.
streams()18
{
228
23
        let codec = Context::from_parameters(stream.parameters())
?0
;
229
23
        if codec.medium() == ffmpeg::media::Type::Video {
230
18
            if stream.duration() < 0 {
231
9
                for (name, value) in &
stream.metadata()2
{
232
9
                    if name == "DURATION" {
233
2
                        if let Ok(time) = NaiveTime::parse_from_str(value, "%H:%M:%S%.f") {
234
2
                            info.duration =
235
2
                                time.signed_duration_since(NaiveTime::MIN).num_seconds() as u64;
236
2
                        }
0
237
7
                    }
238
                }
239
16
            } else {
240
16
                info.duration =
241
16
                    (stream.duration() as f64 * f64::from(stream.time_base())).ceil() as u64;
242
16
            }
243
18
            if let Ok(video) = codec.decoder().video() {
244
18
                info.width = video.width() as u64;
245
18
                info.height = video.height() as u64;
246
18
            }
0
247
5
        }
248
    }
249
250
18
    Ok(info)
251
18
}
252
253
11
fn image_info(input: &Input) -> Result<ImageInfo> {
254
11
    let format = match input.format().name() {
255
11
        "image2" => 
ImageFormat::Jpg2
,
256
9
        "jpeg_pipe" => 
ImageFormat::Jpg2
,
257
7
        "webp_pipe" => 
ImageFormat::Webp3
,
258
4
        "png_pipe" => ImageFormat::Png,
259
0
        format => return Err(anyhow!("Unknown format {format}")),
260
    };
261
262
11
    let mut info = ImageInfo {
263
11
        format,
264
11
        width: 0,
265
11
        height: 0,
266
11
    };
267
268
11
    for stream in input.streams() {
269
11
        let codec = ffmpeg::codec::context::Context::from_parameters(stream.parameters())
?0
;
270
11
        if codec.medium() == ffmpeg::media::Type::Video {
271
11
            if let Ok(video) = codec.decoder().video() {
272
11
                info.width = video.width() as u64;
273
11
                info.height = video.height() as u64;
274
11
            }
0
275
0
        }
276
    }
277
278
11
    Ok(info)
279
11
}
280
281
#[cfg(test)]
282
mod tests {
283
    use super::*;
284
    use serde::Deserialize;
285
    use std::fs::read_to_string;
286
287
3
    #[derive(
D2
eserialize)]
288
    struct Samples {
289
        sample: Vec<Sample>,
290
    }
291
292
199
    #[derive(
D173
eserialize)]
293
    struct Sample {
294
        file: String,
295
        #[serde(flatten)]
296
        info: MediaInfo,
297
    }
298
299
1
    #[test]
300
1
    fn test_resolution() {
301
1
        assert_eq!(resolution(480, 240), "low");
302
1
        assert_eq!(resolution(240, 480), "low");
303
304
1
        assert_eq!(resolution(650, 480), "sd");
305
1
        assert_eq!(resolution(480, 640), "sd");
306
307
1
        assert_eq!(resolution(1024, 720), "hd");
308
1
        assert_eq!(resolution(720, 1024), "hd");
309
310
1
        assert_eq!(resolution(1920, 1080), "fullhd");
311
1
        assert_eq!(resolution(1080, 1920), "fullhd");
312
313
1
        assert_eq!(resolution(2560, 1440), "2k");
314
1
        assert_eq!(resolution(1440, 2560), "2k");
315
316
1
        assert_eq!(resolution(3840, 2160), "4k");
317
1
        assert_eq!(resolution(2160, 3840), "4k");
318
319
1
        assert_eq!(resolution(7680, 4320), "8k");
320
1
        assert_eq!(resolution(4320, 7680), "8k");
321
1
    }
322
323
1
    #[test]
324
1
    fn image_info_tags() {
325
1
        let info: MediaInfo = ImageInfo {
326
1
            format: ImageFormat::Jpg,
327
1
            width: 1920,
328
1
            height: 1080,
329
1
        }
330
1
        .into();
331
1
        let tags = info.tags();
332
1
        assert!(tags.contains(&Tag::new("media".into(), "image".into())));
333
1
        assert!(tags.contains(&Tag::new("format".into(), "jpg".into())));
334
1
        assert!(tags.contains(&Tag::new("width".into(), "1920".into())));
335
1
        assert!(tags.contains(&Tag::new("height".into(), "1080".into())));
336
1
        assert!(tags.contains(&Tag::new("resolution".into(), "fullhd".into())));
337
1
    }
338
339
1
    #[test]
340
1
    fn video_info_tags() {
341
1
        let info: MediaInfo = VideoInfo {
342
1
            format: VideoFormat::Gif,
343
1
            width: 1920,
344
1
            height: 1080,
345
1
            duration: 60,
346
1
        }
347
1
        .into();
348
1
        let tags = info.tags();
349
1
        assert!(tags.contains(&Tag::new("media".into(), "video".into())));
350
1
        assert!(tags.contains(&Tag::new("format".into(), "gif".into())));
351
1
        assert!(tags.contains(&Tag::new("width".into(), "1920".into())));
352
1
        assert!(tags.contains(&Tag::new("height".into(), "1080".into())));
353
1
        assert!(tags.contains(&Tag::new("duration".into(), "60".into())));
354
1
        assert!(tags.contains(&Tag::new("resolution".into(), "fullhd".into())));
355
1
        assert!(tags.contains(&Tag::new("durationgroup".into(), "short".into())));
356
1
    }
357
358
1
    #[test]
359
1
    fn audio_info_tags() {
360
1
        let info: MediaInfo = AudioInfo {
361
1
            format: AudioFormat::Mp3,
362
1
            duration: 60,
363
1
        }
364
1
        .into();
365
1
        let tags = info.tags();
366
1
        assert!(tags.contains(&Tag::new("media".into(), "audio".into())));
367
1
        assert!(tags.contains(&Tag::new("format".into(), "mp3".into())));
368
1
        assert!(tags.contains(&Tag::new("duration".into(), "60".into())));
369
1
    }
370
371
1
    #[test]
372
1
    fn media_info_samples() {
373
1
        let samples: Samples =
374
1
            toml::from_str(&read_to_string("samples/samples.toml").unwrap()).unwrap();
375
27
        for 
sample26
in &samples.sample {
376
26
            let path = Path::new("samples").join(&sample.file);
377
26
            assert_eq!(
378
26
                &media_info(&path).unwrap(),
379
26
                &sample.info,
380
0
                "{path:?} media info"
381
            );
382
26
            let _clone = sample.info.clone();
383
26
            let _debug = format!("{:?}", sample.info);
384
        }
385
1
    }
386
}