Skip to main content

rff_targets/
json.rs

1//! JSON rendering of a [`Plan`] and the format matrix.
2//!
3//! Hand-written rather than serde-derived so the crate stays dependency-free:
4//! this is the payload a web UI or an HTTP handler consumes, and it should not
5//! oblige every consumer of `rff-targets` to compile serde.
6
7use std::fmt::Write;
8
9use crate::{Action, FormatSupport, Plan, Target};
10
11/// Escape a string into a JSON string literal, quotes included.
12fn quote(s: &str) -> String {
13    let mut out = String::with_capacity(s.len() + 2);
14    out.push('"');
15    for c in s.chars() {
16        match c {
17            '"' => out.push_str("\\\""),
18            '\\' => out.push_str("\\\\"),
19            '\n' => out.push_str("\\n"),
20            '\r' => out.push_str("\\r"),
21            '\t' => out.push_str("\\t"),
22            // Control characters have no literal form in JSON.
23            c if (c as u32) < 0x20 => {
24                let _ = write!(out, "\\u{:04x}", c as u32);
25            }
26            c => out.push(c),
27        }
28    }
29    out.push('"');
30    out
31}
32
33fn array(items: impl IntoIterator<Item = String>) -> String {
34    format!("[{}]", items.into_iter().collect::<Vec<_>>().join(","))
35}
36
37impl Plan {
38    /// Render the whole plan as JSON: the source streams, then every target.
39    ///
40    /// Shape (stable; new fields may be added):
41    /// ```json
42    /// {"source_format":"mp4",
43    ///  "source":[{"index":0,"type":"video","codec":"h264"}],
44    ///  "targets":[{"format":"mp4","extension":"mp4","extensions":["mp4"],
45    ///              "long_name":"...","kind":"video","fidelity":"copy",
46    ///              "summary":"h264 copy (copy)","args":["-c:v","copy"],
47    ///              "notes":[],
48    ///              "streams":[{"input_index":0,"type":"video","from":"h264",
49    ///                          "action":"copy"}]}]}
50    /// ```
51    pub fn to_json(&self) -> String {
52        let source = array(self.source.iter().map(|s| {
53            format!(
54                r#"{{"index":{},"type":{},"codec":{}}}"#,
55                s.index,
56                quote(&s.media_type.to_string()),
57                quote(s.codec_id.name())
58            )
59        }));
60        let targets = array(self.targets.iter().map(Target::to_json));
61        let format = match &self.source_format {
62            Some(f) => quote(f),
63            None => "null".to_string(),
64        };
65        format!(r#"{{"source_format":{format},"source":{source},"targets":{targets}}}"#)
66    }
67}
68
69impl Target {
70    /// Render one target as a JSON object.
71    pub fn to_json(&self) -> String {
72        let streams = array(self.streams.iter().map(|s| {
73            let action = match s.action {
74                Action::Copy => r#""action":"copy""#.to_string(),
75                Action::Transcode { to, lossy } => format!(
76                    r#""action":"transcode","to":{},"lossy":{lossy}"#,
77                    quote(to.name())
78                ),
79                Action::Drop(reason) => format!(
80                    r#""action":"drop","reason":{},"detail":{}"#,
81                    quote(reason.name()),
82                    quote(&reason.to_string())
83                ),
84            };
85            format!(
86                r#"{{"input_index":{},"type":{},"from":{},{action}}}"#,
87                s.input_index,
88                quote(&s.media_type.to_string()),
89                quote(s.from.name())
90            )
91        }));
92        format!(
93            concat!(
94                r#"{{"format":{},"long_name":{},"extension":{},"extensions":{},"#,
95                r#""kind":{},"fidelity":{},"summary":{},"args":{},"notes":{},"streams":{}}}"#
96            ),
97            quote(self.format),
98            quote(self.long_name),
99            quote(self.extension),
100            array(self.extensions.iter().map(|e| quote(e))),
101            quote(self.kind.name()),
102            quote(self.fidelity.name()),
103            quote(&self.summary()),
104            array(self.args.iter().map(|a| quote(a))),
105            array(self.notes.iter().map(|n| quote(n))),
106            streams,
107        )
108    }
109}
110
111impl FormatSupport {
112    /// Render one matrix row as a JSON object.
113    pub fn to_json(&self) -> String {
114        let codecs = |ids: &[rff_core::CodecId]| array(ids.iter().map(|c| quote(c.name())));
115        let max = match self.max_streams {
116            Some(n) => n.to_string(),
117            None => "null".to_string(),
118        };
119        format!(
120            concat!(
121                r#"{{"format":{},"long_name":{},"extensions":{},"read":{},"write":{},"#,
122                r#""still_image":{},"max_streams":{},"video":{},"audio":{},"subtitle":{}}}"#
123            ),
124            quote(self.format),
125            quote(self.long_name),
126            array(self.extensions.iter().map(|e| quote(e))),
127            self.can_read,
128            self.can_write,
129            self.still_image,
130            max,
131            codecs(&self.video),
132            codecs(&self.audio),
133            codecs(&self.subtitle),
134        )
135    }
136}
137
138/// Render a whole [`format_matrix`](crate::format_matrix) as a JSON array.
139pub fn matrix_to_json(rows: &[FormatSupport]) -> String {
140    array(rows.iter().map(FormatSupport::to_json))
141}