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