Skip to main content

motioncam_decoder/
decoder.rs

1use std::collections::HashMap;
2use std::path::Path;
3use memmap2::Mmap;
4use serde_json::Value;
5#[cfg(feature = "parallel")]
6use rayon::prelude::*;
7use crate::container::*;
8use crate::error::{McRawError, Result, read_le_bytes};
9use crate::raw;
10use crate::raw_legacy;
11
12/// A chunk of decoded audio data.
13pub struct AudioChunk {
14    /// Timestamp of this chunk in nanoseconds, or `-1` if unavailable.
15    pub timestamp_ns: i64,
16    /// 16-bit signed PCM samples.
17    pub samples: Vec<i16>,
18}
19
20/// Decoder for MCRAW container files.
21///
22/// Opens a `.mcraw` file via memory-mapped I/O and provides access to
23/// container metadata, raw frame data, and audio chunks.
24pub struct Decoder {
25    mmap: Mmap,
26    metadata: Value,
27    frame_offsets: Vec<BufferOffset>,
28    frame_map: HashMap<i64, BufferOffset>,
29    audio_offsets: Vec<BufferOffset>,
30}
31
32impl Decoder {
33    /// Open and decode an MCRAW file at the given path.
34    pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
35        let file = std::fs::File::open(path)?;
36        let mmap = unsafe { Mmap::map(&file)? };
37        Self::from_mmap(mmap)
38    }
39
40    fn from_mmap(mmap: Mmap) -> Result<Self> {
41        let data = &mmap[..];
42        if data.len() < 8 {
43            return Err(McRawError::InvalidHeader);
44        }
45        if data[0..7] != CONTAINER_ID {
46            return Err(McRawError::InvalidHeader);
47        }
48        if data[7] != CONTAINER_VERSION {
49            return Err(McRawError::UnsupportedVersion(data[7]));
50        }
51
52        let mut cursor = 8;
53        let meta_item = parse_item(&data[cursor..])?;
54        cursor += 8;
55        if meta_item.item_type != ItemType::Metadata {
56            return Err(McRawError::InvalidItemType(meta_item.item_type as u32));
57        }
58        let meta_json = data
59            .get(cursor..cursor + meta_item.size as usize)
60            .ok_or(McRawError::TruncatedData)?;
61        let metadata: Value = serde_json::from_slice(meta_json)?;
62
63        let idx_size = std::mem::size_of::<BufferIndex>() + std::mem::size_of::<Item>();
64        if data.len() < idx_size {
65            return Err(McRawError::TruncatedData);
66        }
67        let end_cursor = data.len() - idx_size;
68        let idx_item = parse_item(&data[end_cursor..])?;
69        if idx_item.item_type != ItemType::BufferIndex {
70            return Err(McRawError::InvalidItemType(idx_item.item_type as u32));
71        }
72        let buf_idx = parse_buffer_index(&data[end_cursor + 8..])?;
73        if buf_idx.magic_number != INDEX_MAGIC_NUMBER {
74            return Err(McRawError::CorruptedIndex);
75        }
76
77        let mut frame_offsets = vec![BufferOffset { offset: 0, timestamp: 0 }; buf_idx.num_offsets as usize];
78        let mut off_cursor = buf_idx.index_data_offset as usize;
79        for bo in &mut frame_offsets {
80            *bo = parse_buffer_offset(data.get(off_cursor..).ok_or(McRawError::TruncatedData)?)?;
81            off_cursor += 16;
82        }
83
84        frame_offsets.sort_by_key(|b| b.timestamp);
85        let frame_map: HashMap<_, _> = frame_offsets.iter().map(|b| (b.timestamp, *b)).collect();
86
87        let mut audio_offsets = Vec::new();
88        if let Some(last) = frame_offsets.last() {
89            let mut cur = last.offset as usize;
90            while cur + 8 <= data.len() {
91                let Ok(item) = parse_item(&data[cur..]) else { break; };
92                cur += 8;
93                match item.item_type {
94                    ItemType::Buffer | ItemType::Metadata | ItemType::AudioData | ItemType::AudioDataMetadata => {
95                        cur += item.size as usize;
96                    }
97                    ItemType::AudioIndex => {
98                        let aidx = parse_audio_index(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
99                        cur += 16;
100                        audio_offsets.reserve(aidx.num_offsets as usize);
101                        for _ in 0..aidx.num_offsets {
102                            audio_offsets.push(parse_buffer_offset(data.get(cur..).ok_or(McRawError::TruncatedData)?)?);
103                            cur += 16;
104                        }
105                    }
106                    _ => break,
107                }
108            }
109        }
110
111        Ok(Self { mmap, metadata, frame_offsets, frame_map, audio_offsets })
112    }
113
114    /// Returns a reference to the container-level metadata (JSON).
115    pub fn container_metadata(&self) -> &Value {
116        &self.metadata
117    }
118
119    /// Hint the OS to prefetch a frame's compressed bytes into the page cache.
120    ///
121    /// Uses `madvise(MADV_WILLNEED)` on Unix to bring the frame's mmap range
122    /// into the page cache ahead of the actual read. Platform-specific
123    /// implementation — this is a no-op on non-Unix targets.
124    #[cfg(unix)]
125    pub fn prefetch(&self, timestamp: i64) {
126        if let Some(bo) = self.frame_map.get(&timestamp) {
127            let start = bo.offset.max(0) as usize;
128            let end = (start + 4 * 1024 * 1024).min(self.mmap.len());
129            if end > start {
130                unsafe {
131                    libc::madvise(
132                        self.mmap.as_ptr().add(start) as *mut _,
133                        end - start,
134                        libc::MADV_WILLNEED,
135                    );
136                }
137            }
138        }
139    }
140
141    /// No-op on non-Unix platforms.
142    #[cfg(not(unix))]
143    pub fn prefetch(&self, _timestamp: i64) {}
144
145    /// Returns an iterator over all frame timestamps in the container.
146    pub fn frame_timestamps(&self) -> impl Iterator<Item = i64> + '_ {
147        self.frame_offsets.iter().map(|b| b.timestamp)
148    }
149
150    /// Audio sample rate in Hz, read from container metadata.
151    pub fn audio_sample_rate_hz(&self) -> i64 {
152        self.metadata["extraData"]["audioSampleRate"].as_i64().unwrap_or(0)
153    }
154
155    /// Number of audio channels, read from container metadata.
156    pub fn num_audio_channels(&self) -> i64 {
157        self.metadata["extraData"]["audioChannels"].as_i64().unwrap_or(0)
158    }
159
160    /// Load metadata for a specific frame without decoding pixel data.
161    pub fn load_frame_metadata(&self, timestamp: i64) -> Result<Value> {
162        let offset = self.frame_map.get(&timestamp).ok_or(McRawError::FrameNotFound(timestamp))?.offset as usize;
163        let data = &self.mmap[..];
164        let mut cur = offset;
165        let buf_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
166        cur += 8 + buf_item.size as usize;
167        let meta_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
168        cur += 8;
169        let json_bytes = data.get(cur..cur + meta_item.size as usize).ok_or(McRawError::TruncatedData)?;
170        Ok(serde_json::from_slice(json_bytes)?)
171    }
172
173    /// Load and decode a single frame by timestamp.
174    ///
175    /// Returns `(pixels, metadata)` where `pixels` is a `Vec<u16>` in
176    /// row-major order (Bayer raw data).
177    pub fn load_frame(&self, timestamp: i64) -> Result<(Vec<u16>, Value)> {
178        let offset = self.frame_map.get(&timestamp).ok_or(McRawError::FrameNotFound(timestamp))?.offset as usize;
179        let data = &self.mmap[..];
180        let mut cur = offset;
181        let buf_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
182        cur += 8;
183        let compressed = data.get(cur..cur + buf_item.size as usize).ok_or(McRawError::TruncatedData)?;
184        cur += buf_item.size as usize;
185        let meta_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
186        cur += 8;
187        let json_bytes = data.get(cur..cur + meta_item.size as usize).ok_or(McRawError::TruncatedData)?;
188        let metadata: Value = serde_json::from_slice(json_bytes)?;
189        let width = metadata["width"].as_u64().ok_or_else(|| McRawError::DecompressionFailed("missing width".into()))? as usize;
190        let height = metadata["height"].as_u64().ok_or_else(|| McRawError::DecompressionFailed("missing height".into()))? as usize;
191        let comp_type = metadata["compressionType"].as_i64().unwrap_or(7);
192        let mut output = vec![0u16; width * height];
193        match comp_type {
194            7 => { raw::decode(&mut output, width, height, compressed)?; }
195            6 => { raw_legacy::decode_legacy(&mut output, width, height, compressed)?; }
196            other => return Err(McRawError::InvalidCompressionType(other as i32)),
197        }
198        Ok((output, metadata))
199    }
200
201    /// Decode a single frame by timestamp into a caller-owned buffer.
202    ///
203    /// Avoids the per-frame allocation that `load_frame` does. Returns
204    /// only `asShotNeutral` as `[f32; 3]` — if you need other metadata
205    /// fields, use `load_frame` instead.
206    pub fn load_frame_into(&self, timestamp: i64, out: &mut [u16]) -> Result<[f32; 3]> {
207        let offset = self.frame_map.get(&timestamp).ok_or(McRawError::FrameNotFound(timestamp))?.offset as usize;
208        let data = &self.mmap[..];
209        let mut cur = offset;
210        let buf_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
211        cur += 8;
212        let compressed = data.get(cur..cur + buf_item.size as usize).ok_or(McRawError::TruncatedData)?;
213        cur += buf_item.size as usize;
214        let meta_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
215        cur += 8;
216        let json_bytes = data.get(cur..cur + meta_item.size as usize).ok_or(McRawError::TruncatedData)?;
217        let light: FrameMetaLight = serde_json::from_slice(json_bytes)
218            .map_err(|e| McRawError::DecompressionFailed(format!("light meta: {}", e)))?;
219        let width = light.width.ok_or_else(|| McRawError::DecompressionFailed("missing width".into()))? as usize;
220        let height = light.height.ok_or_else(|| McRawError::DecompressionFailed("missing height".into()))? as usize;
221        let comp_type = light.compression_type.unwrap_or(7);
222        let pixel_count = width * height;
223        if out.len() < pixel_count {
224            return Err(McRawError::TruncatedData);
225        }
226        match comp_type {
227            7 => { raw::decode(&mut out[..pixel_count], width, height, compressed)?; }
228            6 => { raw_legacy::decode_legacy(&mut out[..pixel_count], width, height, compressed)?; }
229            other => return Err(McRawError::InvalidCompressionType(other as i32)),
230        }
231        let asn = light.as_shot_neutral.unwrap_or([1.0, 1.0, 1.0]);
232        Ok([asn[0] as f32, asn[1] as f32, asn[2] as f32])
233    }
234
235    /// Decode all frames in parallel using rayon.
236    ///
237    /// Only available when the `parallel` feature is enabled (default).
238    pub fn load_all_frames_parallel(&self) -> Result<Vec<(i64, Vec<u16>, Value)>> {
239        self.frame_offsets
240            .par_iter()
241            .map(|bo| {
242                let (pixels, meta) = self.load_frame(bo.timestamp)?;
243                Ok((bo.timestamp, pixels, meta))
244            })
245            .collect()
246    }
247
248    /// Load all audio chunks from the container.
249    pub fn load_audio(&self) -> Result<Vec<AudioChunk>> {
250        let data = &self.mmap[..];
251        let mut chunks = Vec::with_capacity(self.audio_offsets.len());
252        for off in &self.audio_offsets {
253            let mut cur = off.offset as usize;
254            if cur + 8 > data.len() {
255                continue;
256            }
257            let item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
258            cur += 8;
259            if item.item_type != ItemType::AudioData {
260                continue;
261            }
262            let samples_bytes = data.get(cur..cur + item.size as usize).ok_or(McRawError::TruncatedData)?;
263            cur += item.size as usize;
264            let samples: Vec<i16> = samples_bytes
265                .chunks_exact(2)
266                .map(|b| i16::from_le_bytes([b[0], b[1]]))
267                .collect();
268            let mut timestamp_ns = -1i64;
269            if cur + 8 <= data.len() {
270                if let Ok(meta_item) = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?) {
271                    if meta_item.item_type == ItemType::AudioDataMetadata && cur + 16 <= data.len() {
272                        timestamp_ns = i64::from_le_bytes(read_le_bytes(data, cur + 8)?);
273                    }
274                }
275            }
276            chunks.push(AudioChunk { timestamp_ns, samples });
277        }
278        Ok(chunks)
279    }
280}
281
282#[inline]
283fn parse_item(data: &[u8]) -> Result<Item> {
284    if data.len() < 8 {
285        return Err(McRawError::TruncatedData);
286    }
287    Ok(Item {
288        item_type: ItemType::try_from(u32::from_le_bytes(read_le_bytes(data, 0)?))?,
289        size: u32::from_le_bytes(read_le_bytes(data, 4)?),
290    })
291}
292
293#[inline]
294fn parse_buffer_offset(data: &[u8]) -> Result<BufferOffset> {
295    Ok(BufferOffset {
296        offset: i64::from_le_bytes(read_le_bytes(data, 0)?),
297        timestamp: i64::from_le_bytes(read_le_bytes(data, 8)?),
298    })
299}
300
301#[inline]
302fn parse_buffer_index(data: &[u8]) -> Result<BufferIndex> {
303    Ok(BufferIndex {
304        magic_number: u32::from_le_bytes(read_le_bytes(data, 0)?),
305        num_offsets: i32::from_le_bytes(read_le_bytes(data, 4)?),
306        index_data_offset: i64::from_le_bytes(read_le_bytes(data, 8)?),
307    })
308}
309
310#[inline]
311fn parse_audio_index(data: &[u8]) -> Result<AudioIndex> {
312    Ok(AudioIndex {
313        num_offsets: i64::from_le_bytes(read_le_bytes(data, 0)?),
314        start_timestamp_ms: i64::from_le_bytes(read_le_bytes(data, 8)?),
315    })
316}
317
318/// Minimal per-frame metadata for the fast export path.
319///
320/// Used by `load_frame_into` to avoid parsing the full JSON Value tree.
321/// All fields are `Option` so older files that lack a key don't error.
322#[derive(serde::Deserialize)]
323struct FrameMetaLight {
324    width: Option<u64>,
325    height: Option<u64>,
326    #[serde(rename = "compressionType")]
327    compression_type: Option<i64>,
328    #[serde(rename = "asShotNeutral")]
329    as_shot_neutral: Option<[f64; 3]>,
330}