Skip to main content

wedeo_codec/
options.rs

1/// Codec-private options, matching FFmpeg's AVOption system.
2///
3/// Options are stored as string key-value pairs, parsed by each codec.
4/// Uses `Vec<(String, String)>` for deterministic ordering (required for
5/// FATE test parity — see CLAUDE.md).
6#[derive(Debug, Clone, Default)]
7pub struct CodecOptions {
8    options: Vec<(String, String)>,
9}
10
11impl CodecOptions {
12    pub fn new() -> Self {
13        Self {
14            options: Vec::new(),
15        }
16    }
17
18    /// Set an option. If the key already exists, its value is replaced in-place
19    /// to preserve insertion order.
20    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
21        let key = key.into();
22        let value = value.into();
23        if let Some(entry) = self.options.iter_mut().find(|(k, _)| k == &key) {
24            entry.1 = value;
25        } else {
26            self.options.push((key, value));
27        }
28    }
29
30    /// Get the string value for a key.
31    pub fn get(&self, key: &str) -> Option<&str> {
32        self.options
33            .iter()
34            .find(|(k, _)| k == key)
35            .map(|(_, v)| v.as_str())
36    }
37
38    /// Get an option parsed as `i64`.
39    pub fn get_i64(&self, key: &str) -> Option<i64> {
40        self.get(key).and_then(|v| v.parse().ok())
41    }
42
43    /// Get an option parsed as `f64`.
44    pub fn get_f64(&self, key: &str) -> Option<f64> {
45        self.get(key).and_then(|v| v.parse().ok())
46    }
47
48    /// Get an option parsed as `bool`.
49    ///
50    /// Recognizes "1", "true", "yes" as true and "0", "false", "no" as false,
51    /// matching FFmpeg's `av_opt_set` boolean parsing.
52    pub fn get_bool(&self, key: &str) -> Option<bool> {
53        self.get(key).and_then(|v| match v {
54            "1" | "true" | "yes" => Some(true),
55            "0" | "false" | "no" => Some(false),
56            _ => None,
57        })
58    }
59
60    /// Iterate over all options as `(&str, &str)` pairs.
61    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
62        self.options.iter().map(|(k, v)| (k.as_str(), v.as_str()))
63    }
64
65    /// Returns true if no options have been set.
66    pub fn is_empty(&self) -> bool {
67        self.options.is_empty()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_set_and_get() {
77        let mut opts = CodecOptions::new();
78        opts.set("bitrate", "128000");
79        assert_eq!(opts.get("bitrate"), Some("128000"));
80        assert_eq!(opts.get("missing"), None);
81    }
82
83    #[test]
84    fn test_overwrite_preserves_order() {
85        let mut opts = CodecOptions::new();
86        opts.set("a", "1");
87        opts.set("b", "2");
88        opts.set("a", "3");
89
90        let keys: Vec<&str> = opts.iter().map(|(k, _)| k).collect();
91        assert_eq!(keys, vec!["a", "b"]);
92        assert_eq!(opts.get("a"), Some("3"));
93    }
94
95    #[test]
96    fn test_typed_getters() {
97        let mut opts = CodecOptions::new();
98        opts.set("threads", "4");
99        opts.set("quality", "0.95");
100        opts.set("strict", "true");
101        opts.set("experimental", "1");
102        opts.set("disabled", "no");
103        opts.set("bad_int", "abc");
104
105        assert_eq!(opts.get_i64("threads"), Some(4));
106        assert_eq!(opts.get_f64("quality"), Some(0.95));
107        assert_eq!(opts.get_bool("strict"), Some(true));
108        assert_eq!(opts.get_bool("experimental"), Some(true));
109        assert_eq!(opts.get_bool("disabled"), Some(false));
110        assert_eq!(opts.get_i64("bad_int"), None);
111        assert_eq!(opts.get_i64("missing"), None);
112    }
113
114    #[test]
115    fn test_is_empty() {
116        let mut opts = CodecOptions::new();
117        assert!(opts.is_empty());
118        opts.set("key", "value");
119        assert!(!opts.is_empty());
120    }
121
122    #[test]
123    fn test_default() {
124        let opts = CodecOptions::default();
125        assert!(opts.is_empty());
126    }
127}