Skip to main content

nexus_common/media/processors/
video.rs

1use async_trait::async_trait;
2use tokio::process::Command;
3
4use crate::{media::FileVariant, models::file::FileDetails, types::DynError};
5
6use super::{BaseProcessingOptions, VariantProcessor};
7
8pub struct VideoOptions {
9    width: String,
10    format: String,
11    content_type: String,
12}
13
14impl BaseProcessingOptions for VideoOptions {
15    fn content_type(&self) -> String {
16        self.content_type.clone()
17    }
18}
19
20/// VideoProcessor is just a prototype and not a real implementation
21/// when we decide to actual start video processing we will need to implement this.
22pub struct VideoProcessor;
23
24#[async_trait]
25impl VariantProcessor for VideoProcessor {
26    type ProcessingOptions = VideoOptions;
27
28    fn get_valid_variants_for_content_type(_content_type: &str) -> Vec<FileVariant> {
29        vec![FileVariant::Main]
30    }
31
32    fn get_content_type_for_variant(_file: &FileDetails, _variant: &FileVariant) -> String {
33        String::from("video/mp4")
34    }
35
36    fn get_options_for_variant(
37        _file: &FileDetails,
38        _variant: &FileVariant,
39    ) -> Result<VideoOptions, DynError> {
40        // Return Err until we have a real implementation
41        // TODO: Add real implementation for videos
42        Err("Not implemented".into())
43    }
44
45    async fn process(
46        origin_file_path: &str,
47        output_file_path: &str,
48        options: &VideoOptions,
49    ) -> Result<String, DynError> {
50        let origin_file_format = VideoProcessor::get_format(origin_file_path).await?;
51
52        let output = match origin_file_format == options.format {
53            true => output_file_path.to_string(),
54            false => format!("{}.{}", output_file_path, options.format),
55        };
56
57        let child_output = match Command::new("ffmpeg")
58            .arg("-i")
59            .arg(origin_file_path)
60            .arg("-vf")
61            .arg(format!("scale={}:-1", options.width))
62            .arg("-c:a")
63            .arg("copy")
64            .arg(output)
65            .output() // Automatically pipes stdout and stderr
66            .await
67        {
68            Ok(output) => output,
69            Err(err) => return Err(err.into()),
70        };
71
72        if child_output.status.success() {
73            Ok(String::from_utf8_lossy(&child_output.stdout).to_string())
74        } else {
75            Err(format!(
76                "FFmpeg command failed: {}",
77                String::from_utf8_lossy(&child_output.stderr)
78            )
79            .into())
80        }
81    }
82}
83
84impl VideoProcessor {
85    // function to get the format of the video
86    async fn get_format(input: &str) -> Result<String, DynError> {
87        let child_output = Command::new("ffmpeg")
88            .arg("-i")
89            .arg(input)
90            .arg("-f")
91            .arg("null")
92            .output() // Automatically pipes stdout and stderr
93            .await?;
94
95        if child_output.status.success() {
96            Ok(String::from_utf8_lossy(&child_output.stdout).to_string())
97        } else {
98            Err(format!(
99                "FFmpeg metadata extraction failed: {}",
100                String::from_utf8_lossy(&child_output.stderr)
101            )
102            .into())
103        }
104    }
105}