Skip to main content

ffcine_assembly/
recipe.rs

1//! Content keys: a canonical JSON form of what a render unit is made of, hashed.
2//!
3//! A key covers everything that changes the pixels (media identity, source range, the resolved
4//! grade and effects, the render profile, the ffcine version) and nothing about where the unit sits
5//! on the timeline, so inserting or removing a clip leaves every other clip's key alone.
6
7use std::fmt::{self, Write as _};
8use std::path::{Path, PathBuf};
9use std::time::UNIX_EPOCH;
10
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value};
13use sha2::{Digest, Sha256};
14
15/// Bump when the render path changes its output for the same recipe (new effect applied, encoder
16/// tweak, ...): every cached unit re-renders.
17pub const RENDER_REVISION: u32 = 8;
18
19/// Bump when the audio mix changes its output for the same recipe: the mix re-renders, the
20/// picture doesn't.
21pub const AUDIO_REVISION: u32 = 2;
22
23/// A content key: 128 bits of SHA-256, hex.
24#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
25pub struct Key(pub String);
26
27impl Key {
28    /// The key of a JSON value, in canonical form (object keys sorted).
29    pub fn of(v: &Value) -> Self {
30        let digest = Sha256::digest(canonical(v).as_bytes());
31        Self(digest[..16].iter().fold(String::new(), |mut s, b| {
32            let _ = write!(s, "{b:02x}");
33            s
34        }))
35    }
36
37    /// The first 10 hex digits, for display.
38    pub fn short(&self) -> &str {
39        &self.0[..self.0.len().min(10)]
40    }
41}
42
43impl fmt::Display for Key {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.write_str(&self.0)
46    }
47}
48
49/// `v` as compact JSON with every object's keys sorted, so key order in the timeline file doesn't
50/// matter.
51pub fn canonical(v: &Value) -> String {
52    fn sorted(v: &Value) -> Value {
53        match v {
54            Value::Object(m) => {
55                let mut keys: Vec<&String> = m.keys().collect();
56                keys.sort();
57                Value::Object(
58                    keys.into_iter()
59                        .map(|k| (k.clone(), sorted(&m[k])))
60                        .collect::<Map<_, _>>(),
61                )
62            }
63            Value::Array(a) => Value::Array(a.iter().map(sorted).collect()),
64            v => v.clone(),
65        }
66    }
67    sorted(v).to_string()
68}
69
70/// Identity of a media file: canonical path, size and modification time (no content read, so
71/// fingerprinting a 15-minute 4K timeline is instant). A missing file fingerprints as missing,
72/// so plans and diffs still work without the media.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct Fingerprint {
75    pub path: PathBuf,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub size: Option<u64>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub mtime_ns: Option<u128>,
80}
81
82/// Looks up file fingerprints; tests swap in a fake.
83pub trait Fingerprinter {
84    fn fingerprint(&self, path: &Path) -> Fingerprint;
85    /// SHA-256 of a small file's bytes (LUTs, shaders), `None` if unreadable.
86    fn content_hash(&self, path: &Path) -> Option<String>;
87    /// The ffmpeg build that runs filter nodes (`ffmpeg -version`'s version line and its
88    /// libavfilter version), in every filter node's key.
89    fn ffmpeg_build(&self) -> String {
90        String::from("unknown")
91    }
92}
93
94/// Fingerprints from the filesystem.
95#[derive(Debug, Clone, Copy, Default)]
96pub struct FsFingerprinter;
97
98impl Fingerprinter for FsFingerprinter {
99    fn fingerprint(&self, path: &Path) -> Fingerprint {
100        let canon = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_owned());
101        let meta = std::fs::metadata(&canon).ok();
102        Fingerprint {
103            path: canon,
104            size: meta.as_ref().map(std::fs::Metadata::len),
105            mtime_ns: meta
106                .and_then(|m| m.modified().ok())
107                .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
108                .map(|d| d.as_nanos()),
109        }
110    }
111
112    fn content_hash(&self, path: &Path) -> Option<String> {
113        let bytes = std::fs::read(path).ok()?;
114        let hash = Sha256::digest(&bytes)
115            .iter()
116            .fold(String::new(), |mut s, b| {
117                let _ = write!(s, "{b:02x}");
118                s
119            });
120        Some(Key::of(&Value::String(hash)).0)
121    }
122
123    /// Of `$FFMPEG` (default `ffmpeg` on the `PATH`, as the renderer runs it), asked once.
124    fn ffmpeg_build(&self) -> String {
125        static BUILD: std::sync::OnceLock<String> = std::sync::OnceLock::new();
126        BUILD
127            .get_or_init(|| {
128                let ffmpeg = std::env::var_os("FFMPEG").unwrap_or_else(|| "ffmpeg".into());
129                std::process::Command::new(ffmpeg)
130                    .args(["-hide_banner", "-version"])
131                    .output()
132                    .ok()
133                    .filter(|o| o.status.success())
134                    .map_or_else(
135                        || String::from("unavailable"),
136                        |o| {
137                            let out = String::from_utf8_lossy(&o.stdout);
138                            let mut lines = out.lines();
139                            let first = lines.next().unwrap_or("").trim().to_owned();
140                            let lavfi = out
141                                .lines()
142                                .find(|l| l.trim_start().starts_with("libavfilter"))
143                                .unwrap_or("")
144                                .split_whitespace()
145                                .collect::<Vec<_>>()
146                                .join(" ");
147                            format!("{first}; {lavfi}")
148                        },
149                    )
150            })
151            .clone()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use serde_json::json;
159
160    #[test]
161    fn key_ignores_object_key_order() {
162        let a = json!({"a": 1, "b": {"x": [1, 2], "y": null}});
163        let b = json!({"b": {"y": null, "x": [1, 2]}, "a": 1});
164        assert_eq!(Key::of(&a), Key::of(&b));
165        assert_ne!(
166            Key::of(&a),
167            Key::of(&json!({"a": 2, "b": {"x": [1, 2], "y": null}}))
168        );
169        assert_eq!(Key::of(&a).0.len(), 32);
170    }
171}