Skip to main content

motioncam_decoder/
decoder.rs

1use std::collections::HashMap;
2use std::path::Path;
3use memmap2::Mmap;
4use serde_json::Value;
5use rayon::prelude::*;
6use crate::container::*;
7use crate::error::{McRawError, Result, read_le_bytes};
8use crate::raw;
9use crate::raw_legacy;
10
11/// A chunk of decoded audio data.
12pub struct AudioChunk {
13    /// Timestamp of this chunk in nanoseconds, or `-1` if unavailable.
14    pub timestamp_ns: i64,
15    /// 16-bit signed PCM samples.
16    pub samples: Vec<i16>,
17}
18
19/// Decoder for MCRAW container files.
20///
21/// Opens a `.mcraw` file via memory-mapped I/O and provides access to
22/// container metadata, raw frame data, and audio chunks.
23pub struct Decoder {
24    mmap: Mmap,
25    metadata: Value,
26    frame_offsets: Vec<BufferOffset>,
27    frame_map: HashMap<i64, BufferOffset>,
28    audio_offsets: Vec<BufferOffset>,
29}
30
31impl Decoder {
32    /// Open and decode an MCRAW file at the given path.
33    pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
34        let file = std::fs::File::open(path)?;
35        let mmap = unsafe { Mmap::map(&file)? };
36        Self::from_mmap(mmap)
37    }
38
39    fn from_mmap(mmap: Mmap) -> Result<Self> {
40        let data = &mmap[..];
41        if data.len() < 8 {
42            return Err(McRawError::InvalidHeader);
43        }
44        if data[0..7] != CONTAINER_ID {
45            return Err(McRawError::InvalidHeader);
46        }
47        if data[7] != CONTAINER_VERSION {
48            return Err(McRawError::UnsupportedVersion(data[7]));
49        }
50
51        let mut cursor = 8;
52        let meta_item = parse_item(&data[cursor..])?;
53        cursor += 8;
54        if meta_item.item_type != ItemType::Metadata {
55            return Err(McRawError::InvalidItemType(meta_item.item_type as u32));
56        }
57        let meta_json = data
58            .get(cursor..cursor + meta_item.size as usize)
59            .ok_or(McRawError::TruncatedData)?;
60        let metadata: Value = serde_json::from_slice(meta_json)?;
61
62        let idx_size = std::mem::size_of::<BufferIndex>() + std::mem::size_of::<Item>();
63        if data.len() < idx_size {
64            return Err(McRawError::TruncatedData);
65        }
66        let end_cursor = data.len() - idx_size;
67        let idx_item = parse_item(&data[end_cursor..])?;
68        if idx_item.item_type != ItemType::BufferIndex {
69            return Err(McRawError::InvalidItemType(idx_item.item_type as u32));
70        }
71        let buf_idx = parse_buffer_index(&data[end_cursor + 8..])?;
72        if buf_idx.magic_number != INDEX_MAGIC_NUMBER {
73            return Err(McRawError::CorruptedIndex);
74        }
75
76        let mut frame_offsets = vec![BufferOffset { offset: 0, timestamp: 0 }; buf_idx.num_offsets as usize];
77        let mut off_cursor = buf_idx.index_data_offset as usize;
78        for bo in &mut frame_offsets {
79            *bo = parse_buffer_offset(data.get(off_cursor..).ok_or(McRawError::TruncatedData)?)?;
80            off_cursor += 16;
81        }
82
83        frame_offsets.sort_by_key(|b| b.timestamp);
84        let frame_map: HashMap<_, _> = frame_offsets.iter().map(|b| (b.timestamp, *b)).collect();
85
86        let mut audio_offsets = Vec::new();
87        if let Some(last) = frame_offsets.last() {
88            let mut cur = last.offset as usize;
89            while cur + 8 <= data.len() {
90                let Ok(item) = parse_item(&data[cur..]) else { break; };
91                cur += 8;
92                match item.item_type {
93                    ItemType::Buffer | ItemType::Metadata | ItemType::AudioData | ItemType::AudioDataMetadata => {
94                        cur += item.size as usize;
95                    }
96                    ItemType::AudioIndex => {
97                        let aidx = parse_audio_index(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
98                        cur += 16;
99                        audio_offsets.reserve(aidx.num_offsets as usize);
100                        for _ in 0..aidx.num_offsets {
101                            audio_offsets.push(parse_buffer_offset(data.get(cur..).ok_or(McRawError::TruncatedData)?)?);
102                            cur += 16;
103                        }
104                    }
105                    _ => break,
106                }
107            }
108        }
109
110        Ok(Self { mmap, metadata, frame_offsets, frame_map, audio_offsets })
111    }
112
113    /// Returns a reference to the container-level metadata (JSON).
114    pub fn container_metadata(&self) -> &Value {
115        &self.metadata
116    }
117
118    /// Returns an iterator over all frame timestamps in the container.
119    pub fn frame_timestamps(&self) -> impl Iterator<Item = i64> + '_ {
120        self.frame_offsets.iter().map(|b| b.timestamp)
121    }
122
123    /// Audio sample rate in Hz, read from container metadata.
124    pub fn audio_sample_rate_hz(&self) -> i64 {
125        self.metadata["extraData"]["audioSampleRate"].as_i64().unwrap_or(0)
126    }
127
128    /// Number of audio channels, read from container metadata.
129    pub fn num_audio_channels(&self) -> i64 {
130        self.metadata["extraData"]["audioChannels"].as_i64().unwrap_or(0)
131    }
132
133    /// Load metadata for a specific frame without decoding pixel data.
134    pub fn load_frame_metadata(&self, timestamp: i64) -> Result<Value> {
135        let offset = self.frame_map.get(&timestamp).ok_or(McRawError::FrameNotFound(timestamp))?.offset as usize;
136        let data = &self.mmap[..];
137        let mut cur = offset;
138        let buf_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
139        cur += 8 + buf_item.size as usize;
140        let meta_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
141        cur += 8;
142        let json_bytes = data.get(cur..cur + meta_item.size as usize).ok_or(McRawError::TruncatedData)?;
143        Ok(serde_json::from_slice(json_bytes)?)
144    }
145
146    /// Load and decode a single frame by timestamp.
147    ///
148    /// Returns `(pixels, metadata)` where `pixels` is a `Vec<u16>` in
149    /// row-major order (Bayer raw data).
150    pub fn load_frame(&self, timestamp: i64) -> Result<(Vec<u16>, Value)> {
151        let offset = self.frame_map.get(&timestamp).ok_or(McRawError::FrameNotFound(timestamp))?.offset as usize;
152        let data = &self.mmap[..];
153        let mut cur = offset;
154        let buf_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
155        cur += 8;
156        let compressed = data.get(cur..cur + buf_item.size as usize).ok_or(McRawError::TruncatedData)?;
157        cur += buf_item.size as usize;
158        let meta_item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
159        cur += 8;
160        let json_bytes = data.get(cur..cur + meta_item.size as usize).ok_or(McRawError::TruncatedData)?;
161        let metadata: Value = serde_json::from_slice(json_bytes)?;
162        let width = metadata["width"].as_u64().ok_or_else(|| McRawError::DecompressionFailed("missing width".into()))? as usize;
163        let height = metadata["height"].as_u64().ok_or_else(|| McRawError::DecompressionFailed("missing height".into()))? as usize;
164        let comp_type = metadata["compressionType"].as_i64().unwrap_or(7);
165        let mut output = vec![0u16; width * height];
166        match comp_type {
167            7 => { raw::decode(&mut output, width, height, compressed)?; }
168            6 => { raw_legacy::decode_legacy(&mut output, width, height, compressed)?; }
169            other => return Err(McRawError::InvalidCompressionType(other as i32)),
170        }
171        Ok((output, metadata))
172    }
173
174    /// Decode all frames in parallel using rayon.
175    ///
176    /// Only available when the `parallel` feature is enabled (default).
177    pub fn load_all_frames_parallel(&self) -> Result<Vec<(i64, Vec<u16>, Value)>> {
178        self.frame_offsets
179            .par_iter()
180            .map(|bo| {
181                let (pixels, meta) = self.load_frame(bo.timestamp)?;
182                Ok((bo.timestamp, pixels, meta))
183            })
184            .collect()
185    }
186
187    /// Load all audio chunks from the container.
188    pub fn load_audio(&self) -> Result<Vec<AudioChunk>> {
189        let data = &self.mmap[..];
190        let mut chunks = Vec::with_capacity(self.audio_offsets.len());
191        for off in &self.audio_offsets {
192            let mut cur = off.offset as usize;
193            if cur + 8 > data.len() {
194                continue;
195            }
196            let item = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?)?;
197            cur += 8;
198            if item.item_type != ItemType::AudioData {
199                continue;
200            }
201            let samples_bytes = data.get(cur..cur + item.size as usize).ok_or(McRawError::TruncatedData)?;
202            cur += item.size as usize;
203            let samples: Vec<i16> = samples_bytes
204                .chunks_exact(2)
205                .map(|b| i16::from_le_bytes([b[0], b[1]]))
206                .collect();
207            let mut timestamp_ns = -1i64;
208            if cur + 8 <= data.len() {
209                if let Ok(meta_item) = parse_item(data.get(cur..).ok_or(McRawError::TruncatedData)?) {
210                    if meta_item.item_type == ItemType::AudioDataMetadata && cur + 16 <= data.len() {
211                        timestamp_ns = i64::from_le_bytes(read_le_bytes(data, cur + 8)?);
212                    }
213                }
214            }
215            chunks.push(AudioChunk { timestamp_ns, samples });
216        }
217        Ok(chunks)
218    }
219}
220
221#[inline]
222fn parse_item(data: &[u8]) -> Result<Item> {
223    if data.len() < 8 {
224        return Err(McRawError::TruncatedData);
225    }
226    Ok(Item {
227        item_type: ItemType::try_from(u32::from_le_bytes(read_le_bytes(data, 0)?))?,
228        size: u32::from_le_bytes(read_le_bytes(data, 4)?),
229    })
230}
231
232#[inline]
233fn parse_buffer_offset(data: &[u8]) -> Result<BufferOffset> {
234    Ok(BufferOffset {
235        offset: i64::from_le_bytes(read_le_bytes(data, 0)?),
236        timestamp: i64::from_le_bytes(read_le_bytes(data, 8)?),
237    })
238}
239
240#[inline]
241fn parse_buffer_index(data: &[u8]) -> Result<BufferIndex> {
242    Ok(BufferIndex {
243        magic_number: u32::from_le_bytes(read_le_bytes(data, 0)?),
244        num_offsets: i32::from_le_bytes(read_le_bytes(data, 4)?),
245        index_data_offset: i64::from_le_bytes(read_le_bytes(data, 8)?),
246    })
247}
248
249#[inline]
250fn parse_audio_index(data: &[u8]) -> Result<AudioIndex> {
251    Ok(AudioIndex {
252        num_offsets: i64::from_le_bytes(read_le_bytes(data, 0)?),
253        start_timestamp_ms: i64::from_le_bytes(read_le_bytes(data, 8)?),
254    })
255}