1pub mod avc;
17pub mod codec_id;
18pub mod demux;
19pub mod ebml;
20pub mod ids;
21pub mod mux;
22
23use oxideav_core::ContainerRegistry;
24
25pub fn register_containers(reg: &mut ContainerRegistry) {
32 reg.register_demuxer("matroska", demux::open);
34 reg.register_muxer("matroska", mux::open);
35 reg.register_probe("matroska", probe_matroska);
36
37 reg.register_demuxer("webm", demux::open);
39 reg.register_muxer("webm", mux::open_webm);
40 reg.register_probe("webm", probe_webm);
41
42 reg.register_extension("mkv", "matroska");
44 reg.register_extension("mka", "matroska");
45 reg.register_extension("mks", "matroska");
46 reg.register_extension("webm", "webm");
47}
48
49pub fn register(ctx: &mut oxideav_core::RuntimeContext) {
59 register_containers(&mut ctx.containers);
60}
61
62oxideav_core::register!("mkv", register);
63
64const EBML_MAGIC: [u8; 4] = [0x1A, 0x45, 0xDF, 0xA3];
66
67const SCORE_DOCTYPE_MATCH: u8 = 100;
71
72const SCORE_SIGNATURE_ONLY: u8 = 60;
76
77fn probe_matroska(p: &oxideav_core::ProbeData) -> u8 {
80 match probe_doctype(p.buf) {
81 DocTypeProbe::Matroska => SCORE_DOCTYPE_MATCH,
82 DocTypeProbe::Webm => SCORE_SIGNATURE_ONLY,
83 DocTypeProbe::EbmlOnly => SCORE_SIGNATURE_ONLY,
84 DocTypeProbe::NotEbml => 0,
85 }
86}
87
88fn probe_webm(p: &oxideav_core::ProbeData) -> u8 {
93 match probe_doctype(p.buf) {
94 DocTypeProbe::Webm => SCORE_DOCTYPE_MATCH,
95 DocTypeProbe::Matroska => 0,
96 DocTypeProbe::EbmlOnly => 0,
97 DocTypeProbe::NotEbml => 0,
98 }
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102enum DocTypeProbe {
103 Matroska,
105 Webm,
107 EbmlOnly,
110 NotEbml,
112}
113
114fn probe_doctype(buf: &[u8]) -> DocTypeProbe {
119 if buf.len() < 4 || buf[0..4] != EBML_MAGIC {
120 return DocTypeProbe::NotEbml;
121 }
122 let mut cur = std::io::Cursor::new(&buf[4..]);
127 let (hdr_size, _) = match ebml::read_vint(&mut cur, false) {
128 Ok(v) => v,
129 Err(_) => return DocTypeProbe::EbmlOnly,
130 };
131 let hdr_start = 4 + cur.position() as usize;
132 let hdr_end = hdr_start.saturating_add(hdr_size as usize);
133 let scan_end = hdr_end.min(buf.len());
134 let mut pos = hdr_start;
135 while pos < scan_end {
136 let mut sub = std::io::Cursor::new(&buf[pos..scan_end]);
137 let (id, id_len) = match ebml::read_vint(&mut sub, true) {
138 Ok(v) => v,
139 Err(_) => return DocTypeProbe::EbmlOnly,
140 };
141 let (size, size_len) = match ebml::read_vint(&mut sub, false) {
142 Ok(v) => v,
143 Err(_) => return DocTypeProbe::EbmlOnly,
144 };
145 let data_start = pos + id_len + size_len;
146 let data_end = data_start.saturating_add(size as usize);
147 if data_end > scan_end {
148 return DocTypeProbe::EbmlOnly;
149 }
150 if id == ids::EBML_DOC_TYPE as u64 {
151 let slice = &buf[data_start..data_end];
152 let trimmed = slice.split(|&b| b == 0).next().unwrap_or(&[]);
154 return match trimmed {
155 b"matroska" => DocTypeProbe::Matroska,
156 b"webm" => DocTypeProbe::Webm,
157 _ => DocTypeProbe::EbmlOnly,
158 };
159 }
160 pos = data_end;
161 }
162 DocTypeProbe::EbmlOnly
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn synth_ebml_head(doc_type: &str) -> Vec<u8> {
172 use crate::ebml::{write_element_id, write_vint};
173 let mut body = Vec::new();
176 body.extend_from_slice(&write_element_id(ids::EBML_VERSION));
178 body.extend_from_slice(&write_vint(1, 0));
179 body.push(0x01);
180 body.extend_from_slice(&write_element_id(ids::EBML_DOC_TYPE));
182 body.extend_from_slice(&write_vint(doc_type.len() as u64, 0));
183 body.extend_from_slice(doc_type.as_bytes());
184 body.extend_from_slice(&write_element_id(ids::EBML_DOC_TYPE_VERSION));
186 body.extend_from_slice(&write_vint(1, 0));
187 body.push(0x02);
188 body.extend_from_slice(&write_element_id(ids::EBML_DOC_TYPE_READ_VERSION));
190 body.extend_from_slice(&write_vint(1, 0));
191 body.push(0x02);
192 let mut out = Vec::new();
194 out.extend_from_slice(&write_element_id(ids::EBML_HEADER));
195 out.extend_from_slice(&write_vint(body.len() as u64, 0));
196 out.extend_from_slice(&body);
197 out
198 }
199
200 #[test]
201 fn probe_doctype_webm() {
202 let head = synth_ebml_head("webm");
203 assert_eq!(probe_doctype(&head), DocTypeProbe::Webm);
204 }
205
206 #[test]
207 fn probe_doctype_matroska() {
208 let head = synth_ebml_head("matroska");
209 assert_eq!(probe_doctype(&head), DocTypeProbe::Matroska);
210 }
211
212 #[test]
213 fn probe_doctype_non_ebml() {
214 let buf = b"RIFF....WAVEfmt ";
215 assert_eq!(probe_doctype(buf), DocTypeProbe::NotEbml);
216 }
217
218 #[test]
219 fn webm_probe_prefers_webm() {
220 let head = synth_ebml_head("webm");
221 let p = oxideav_core::ProbeData {
222 buf: &head,
223 ext: None,
224 };
225 let webm_score = probe_webm(&p);
226 let mkv_score = probe_matroska(&p);
227 assert!(
228 webm_score > mkv_score,
229 "webm probe ({}) should outrank matroska probe ({}) on DocType=webm",
230 webm_score,
231 mkv_score
232 );
233 assert_eq!(webm_score, SCORE_DOCTYPE_MATCH);
234 }
235
236 #[test]
237 fn matroska_probe_prefers_matroska() {
238 let head = synth_ebml_head("matroska");
239 let p = oxideav_core::ProbeData {
240 buf: &head,
241 ext: None,
242 };
243 let webm_score = probe_webm(&p);
244 let mkv_score = probe_matroska(&p);
245 assert!(
246 mkv_score > webm_score,
247 "matroska probe ({}) should outrank webm probe ({}) on DocType=matroska",
248 mkv_score,
249 webm_score
250 );
251 assert_eq!(mkv_score, SCORE_DOCTYPE_MATCH);
252 assert_eq!(webm_score, 0);
253 }
254
255 #[test]
256 fn registry_extension_mapping() {
257 let mut reg = ContainerRegistry::new();
258 register_containers(&mut reg);
259 assert_eq!(reg.container_for_extension("webm"), Some("webm"));
260 assert_eq!(reg.container_for_extension("mkv"), Some("matroska"));
261 assert_eq!(reg.container_for_extension("mka"), Some("matroska"));
262 assert_eq!(reg.container_for_extension("mks"), Some("matroska"));
263 }
264
265 #[test]
266 fn register_via_runtime_context_installs_container() {
267 let mut ctx = oxideav_core::RuntimeContext::new();
268 register(&mut ctx);
269 assert_eq!(
270 ctx.containers.container_for_extension("mkv"),
271 Some("matroska")
272 );
273 assert_eq!(ctx.containers.container_for_extension("webm"), Some("webm"));
274 }
275}