Skip to main content

mcraw_tui/
decoder.rs

1use anyhow::{anyhow, Result};
2use serde_json::Value;
3use std::io::Write;
4use std::path::Path;
5
6pub struct Decoder {
7    inner: motioncam_decoder::Decoder,
8}
9
10#[derive(Debug, Clone)]
11pub struct ContainerMetadata {
12    pub color_matrix1: [f32; 9],
13    pub color_matrix2: [f32; 9],
14    pub forward_matrix1: [f32; 9],
15    pub forward_matrix2: [f32; 9],
16    pub calibration_matrix1: [f32; 9],
17    pub calibration_matrix2: [f32; 9],
18    pub calibration_illuminant1: i32,
19    pub calibration_illuminant2: i32,
20    pub has_calibration_illuminants: bool,
21    pub white_level: f64,
22    pub black_level: [f64; 4],
23    pub black_level_count: i32,
24    pub audio_sample_rate_hz: i32,
25    pub num_audio_channels: i32,
26}
27
28#[derive(Debug, Clone)]
29pub struct FrameMetadata {
30    pub width: u32,
31    pub height: u32,
32    pub timestamp_ns: i64,
33    pub as_shot_neutral: [f32; 3],
34    pub exposure_time: f64,
35    pub iso: f32,
36    pub focal_length: f32,
37    pub aperture: f32,
38}
39
40fn json_to_matrix9(val: &Value, key: &str) -> [f32; 9] {
41    let mut result = [0.0f32; 9];
42    if let Some(arr) = val.get(key).and_then(|v| v.as_array()) {
43        for (i, item) in arr.iter().enumerate().take(9) {
44            if let Some(n) = item.as_f64() {
45                result[i] = n as f32;
46            }
47        }
48    }
49    result
50}
51
52fn json_to_black_level(val: &Value) -> ([f64; 4], i32) {
53    let mut result = [0.0f64; 4];
54    let mut count = 0i32;
55    if let Some(arr) = val.get("blackLevel").and_then(|v| v.as_array()) {
56        for (i, item) in arr.iter().enumerate().take(4) {
57            if let Some(n) = item.as_f64() {
58                result[i] = n;
59                count = (i + 1) as i32;
60            }
61        }
62    }
63    (result, count)
64}
65
66fn json_to_as_shot_neutral(val: &Value) -> [f32; 3] {
67    let mut result = [1.0f32; 3];
68    if let Some(arr) = val.get("asShotNeutral").and_then(|v| v.as_array()) {
69        for (i, item) in arr.iter().enumerate().take(3) {
70            if let Some(n) = item.as_f64() {
71                result[i] = n as f32;
72            }
73        }
74    }
75    result
76}
77
78impl Decoder {
79    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
80        let path_str = path.as_ref().to_string_lossy().to_string();
81        tracing::debug!("decoder::new: {}", path_str);
82        let inner = motioncam_decoder::Decoder::from_path(path)
83            .map_err(|e| {
84                tracing::error!("decoder failed to open {}: {}", path_str, e);
85                anyhow!("Failed to open decoder: {}", e)
86            })?;
87        tracing::debug!("decoder opened successfully: {}", path_str);
88        Ok(Self { inner })
89    }
90
91    pub fn container_metadata(&self) -> Result<ContainerMetadata> {
92        tracing::debug!("decoder::container_metadata");
93        let meta = self.inner.container_metadata();
94
95        let color_matrix1 = json_to_matrix9(meta, "colorMatrix1");
96        let color_matrix2 = json_to_matrix9(meta, "colorMatrix2");
97        let forward_matrix1 = json_to_matrix9(meta, "forwardMatrix1");
98        let forward_matrix2 = json_to_matrix9(meta, "forwardMatrix2");
99        let calibration_matrix1 = json_to_matrix9(meta, "calibrationMatrix1");
100        let calibration_matrix2 = json_to_matrix9(meta, "calibrationMatrix2");
101
102        let illuminant1 = meta.get("calibrationIlluminant1").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
103        let illuminant2 = meta.get("calibrationIlluminant2").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
104
105        let white_level = meta.get("whiteLevel").and_then(|v| v.as_f64()).unwrap_or(16383.0);
106
107        let (black_level, black_level_count) = json_to_black_level(meta);
108
109        let audio_sample_rate = meta
110            .get("extraData")
111            .and_then(|e| e.get("audioSampleRate"))
112            .and_then(|v| v.as_i64())
113            .unwrap_or(0) as i32;
114        let audio_channels = meta
115            .get("extraData")
116            .and_then(|e| e.get("audioChannels"))
117            .and_then(|v| v.as_i64())
118            .unwrap_or(0) as i32;
119
120        Ok(ContainerMetadata {
121            color_matrix1,
122            color_matrix2,
123            forward_matrix1,
124            forward_matrix2,
125            calibration_matrix1,
126            calibration_matrix2,
127            calibration_illuminant1: illuminant1,
128            calibration_illuminant2: illuminant2,
129            has_calibration_illuminants: illuminant1 != 0 || illuminant2 != 0,
130            white_level,
131            black_level,
132            black_level_count,
133            audio_sample_rate_hz: audio_sample_rate,
134            num_audio_channels: audio_channels,
135        })
136    }
137
138    pub fn timestamps(&self) -> Result<Vec<i64>> {
139        let ts = self.inner.frame_timestamps().collect::<Vec<_>>();
140        tracing::debug!("decoder::timestamps: {} frames", ts.len());
141        Ok(ts)
142    }
143
144    /// Hint the OS to prefetch a frame's range into the page cache (B4).
145    /// See `motioncam_decoder::Decoder::prefetch`. No-op on Windows.
146    pub fn prefetch(&self, timestamp_ns: i64) {
147        self.inner.prefetch(timestamp_ns);
148    }
149
150    pub fn load_frame(&self, timestamp_ns: i64) -> Result<(Vec<u16>, FrameMetadata)> {
151        let (pixels, meta) = self.inner.load_frame(timestamp_ns)
152            .map_err(|e| {
153                tracing::error!("failed to decode frame at ns {}: {}", timestamp_ns, e);
154                anyhow!("Failed to decode frame at ns {}: {}", timestamp_ns, e)
155            })?;
156
157        let width = meta.get("width").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
158        let height = meta.get("height").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
159        let as_shot_neutral = json_to_as_shot_neutral(&meta);
160        let exposure_time = meta.get("exposureTime").and_then(|v| v.as_f64()).unwrap_or(0.0);
161        let iso = meta.get("iso").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
162        let focal_length = meta.get("focalLength").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
163        let aperture = meta.get("aperture").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
164
165        Ok((pixels, FrameMetadata {
166            width,
167            height,
168            timestamp_ns,
169            as_shot_neutral,
170            exposure_time,
171            iso,
172            focal_length,
173            aperture,
174        }))
175    }
176
177    /// Decode a frame into a caller-owned buffer (B1). Returns only the
178    /// `asShotNeutral` triplet (B2) — the only metadata the export hot
179    /// path uses. Use `load_frame` if you need the other fields.
180    pub fn load_frame_into(&self, timestamp_ns: i64, out: &mut [u16]) -> Result<[f32; 3]> {
181        self.inner.load_frame_into(timestamp_ns, out)
182            .map_err(|e| {
183                tracing::error!("failed to decode frame at ns {}: {}", timestamp_ns, e);
184                anyhow!("Failed to decode frame at ns {}: {}", timestamp_ns, e)
185            })
186    }
187
188    pub fn load_frame_metadata(&self, timestamp_ns: i64) -> Result<FrameMetadata> {
189        let meta = self.inner.load_frame_metadata(timestamp_ns)
190            .map_err(|e| anyhow!("Failed to get metadata for frame at ns {}: {}", timestamp_ns, e))?;
191
192        let width = meta.get("width").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
193        let height = meta.get("height").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
194        let as_shot_neutral = json_to_as_shot_neutral(&meta);
195        let exposure_time = meta.get("exposureTime").and_then(|v| v.as_f64()).unwrap_or(0.0);
196        let iso = meta.get("iso").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
197        let focal_length = meta.get("focalLength").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
198        let aperture = meta.get("aperture").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
199
200        Ok(FrameMetadata {
201            width,
202            height,
203            timestamp_ns,
204            as_shot_neutral,
205            exposure_time,
206            iso,
207            focal_length,
208            aperture,
209        })
210    }
211
212    /// Write all audio chunks to `writer` as raw s16le PCM, one chunk at a
213    /// time.  Never holds more than one chunk in memory — safe for long
214    /// recordings.  The caller should wrap the file in a `BufWriter` for
215    /// decent I/O coalescing.
216    pub fn write_audio_to<W: Write>(&self, writer: &mut W) -> Result<()> {
217        let chunks = self.inner.load_audio()
218            .map_err(|e| anyhow!("Failed to load audio: {}", e))?;
219        for chunk in chunks {
220            for &sample in &chunk.samples {
221                writer.write_all(&sample.to_le_bytes())?;
222            }
223        }
224        Ok(())
225    }
226}