1mod colour;
5mod ebml;
6
7use anyhow::{Context, Result, bail};
8use codec::frame::{ColorMetadata, ColorSpace, ContentLightLevel, PixelFormat, StreamInfo};
9use matroska_demuxer::{Frame as MkvFrame, MatroskaFile, TrackType as MkvTrackType};
10use std::io::Cursor;
11
12use crate::annexb::{
13 NaluCodec, ParamSetTracker, length_prefixed_to_annexb_tracked, parse_avcc, parse_hvcc,
14};
15use crate::streaming::{DemuxHeader, Sample, StreamingDemuxer};
16use crate::MkvColorInfo;
17
18use super::{AudioTrack, DemuxResult};
19
20use colour::{bitrate_from_tags, colour_to_pipeline};
21use ebml::scan_mkv_colour_raw;
22
23#[allow(unused_imports)] pub(crate) use ebml::{read_id_vint, read_size_vint};
27
28pub fn demux_mkv(data: &[u8]) -> Result<DemuxResult> {
33 let cursor = Cursor::new(data);
34 let mut mkv =
35 MatroskaFile::open(cursor).map_err(|e| anyhow::anyhow!("reading MKV header: {e}"))?;
36
37 let (
46 track_number,
47 track_uid,
48 codec_id,
49 width,
50 height,
51 annexb_prepend,
52 length_size,
53 color_space,
54 mut color_metadata,
55 mut color_info,
56 track_default_duration_ns,
57 ) = {
58 let track_info = mkv
59 .tracks()
60 .iter()
61 .find(|t| t.track_type() == MkvTrackType::Video)
62 .context("no video track in MKV")?;
63
64 let track_number = track_info.track_number().get();
65 let track_uid = track_info.track_uid().get();
66 let codec_id = track_info.codec_id().to_string();
67 let default_duration_ns = track_info.default_duration().map(|d| d.get());
74
75 let (annexb_prepend, length_size): (Vec<Vec<u8>>, u8) = if codec_id == "V_MPEG4/ISO/AVC" {
79 let priv_bytes = track_info
80 .codec_private()
81 .context("V_MPEG4/ISO/AVC CodecPrivate missing")?;
82 let cfg = parse_avcc(priv_bytes).context("V_MPEG4/ISO/AVC CodecPrivate malformed")?;
83 (cfg.parameter_sets, cfg.length_size)
84 } else if codec_id == "V_MPEGH/ISO/HEVC" {
85 let priv_bytes = track_info
86 .codec_private()
87 .context("V_MPEGH/ISO/HEVC CodecPrivate missing")?;
88 let cfg = parse_hvcc(priv_bytes).context("V_MPEGH/ISO/HEVC CodecPrivate malformed")?;
89 (cfg.parameter_sets, cfg.length_size)
90 } else {
91 (Vec::new(), 4)
92 };
93
94 if mkv_codec_needs_annexb(&codec_id) && annexb_prepend.is_empty() {
95 bail!("AVC/HEVC MKV CodecPrivate missing or empty — no parameter sets to prepend");
96 }
97
98 let video = track_info
99 .video()
100 .context("video track missing Video element")?;
101 let w = video.pixel_width().get() as u32;
102 let h = video.pixel_height().get() as u32;
103
104 let (color_space, color_metadata, color_info) = match video.colour() {
108 Some(colour) => colour_to_pipeline(colour),
109 None => (
110 ColorSpace::Bt709,
111 ColorMetadata::default(),
112 MkvColorInfo::default(),
113 ),
114 };
115
116 (
117 track_number,
118 track_uid,
119 codec_id,
120 w,
121 h,
122 annexb_prepend,
123 length_size,
124 color_space,
125 color_metadata,
126 color_info,
127 default_duration_ns,
128 )
129 };
130
131 color_info.max_cll = None;
142 color_info.max_fall = None;
143 color_metadata.content_light_level = None;
144 if let Some(md) = color_metadata.mastering_display.as_mut() {
145 md.primaries_r_y = 0;
148 md.primaries_g_y = 0;
149 md.primaries_b_y = 0;
150 }
151 if let Some(local) = color_info.mastering.as_mut() {
152 local.primary_r_chromaticity_y = None;
153 local.primary_g_chromaticity_y = None;
154 local.primary_b_chromaticity_y = None;
155 }
156 if let Some(fix) = scan_mkv_colour_raw(data) {
157 color_info.max_cll = fix.max_cll;
158 color_info.max_fall = fix.max_fall;
159 if fix.max_cll.is_some() || fix.max_fall.is_some() {
160 color_metadata.content_light_level = Some(ContentLightLevel {
161 max_cll: fix.max_cll.unwrap_or(0).min(u16::MAX as u32) as u16,
162 max_fall: fix.max_fall.unwrap_or(0).min(u16::MAX as u32) as u16,
163 });
164 }
165 let chrom = |v: f64| (v * 50_000.0).round().clamp(0.0, u16::MAX as f64) as u16;
168 if let Some(md) = color_metadata.mastering_display.as_mut() {
169 if let Some(y) = fix.primary_r_chromaticity_y {
170 md.primaries_r_y = chrom(y);
171 }
172 if let Some(y) = fix.primary_g_chromaticity_y {
173 md.primaries_g_y = chrom(y);
174 }
175 if let Some(y) = fix.primary_b_chromaticity_y {
176 md.primaries_b_y = chrom(y);
177 }
178 }
179 if let Some(local) = color_info.mastering.as_mut() {
180 if fix.primary_r_chromaticity_y.is_some() {
181 local.primary_r_chromaticity_y = fix.primary_r_chromaticity_y;
182 }
183 if fix.primary_g_chromaticity_y.is_some() {
184 local.primary_g_chromaticity_y = fix.primary_g_chromaticity_y;
185 }
186 if fix.primary_b_chromaticity_y.is_some() {
187 local.primary_b_chromaticity_y = fix.primary_b_chromaticity_y;
188 }
189 }
190 }
191
192 let needs_annexb = mkv_codec_needs_annexb(&codec_id);
193 let codec = match codec_id.as_str() {
194 "V_VP9" => "vp9".to_string(),
195 "V_VP8" => "vp8".to_string(),
196 "V_AV1" => "av1".to_string(),
197 "V_MPEG4/ISO/AVC" => "h264".to_string(),
198 "V_MPEGH/ISO/HEVC" => "h265".to_string(),
199 other => other.to_lowercase(),
200 };
201
202 let timestamp_scale = mkv.info().timestamp_scale().get();
203 let duration_ticks = mkv.info().duration().unwrap_or(0.0);
204 let duration = duration_ticks * (timestamp_scale as f64) / 1_000_000_000.0;
206
207 let tag_bitrate = mkv
211 .tags()
212 .and_then(|tags| bitrate_from_tags(tags, track_uid));
213 if color_info != MkvColorInfo::default() {
217 tracing::info!(
218 bits_per_channel = ?color_info.bits_per_channel,
219 max_cll = ?color_info.max_cll,
220 max_fall = ?color_info.max_fall,
221 mastering = ?color_info.mastering,
222 "MKV Colour: parsed HDR-adjacent metadata"
223 );
224 }
225
226 let mut samples: Vec<Vec<u8>> = Vec::new();
227 let mut frame = MkvFrame::default();
228 let mut total_video_bytes: u64 = 0;
229 let mut mkv_tracker = if needs_annexb {
234 Some(ParamSetTracker::new(if codec_id == "V_MPEG4/ISO/AVC" {
235 NaluCodec::Avc
236 } else {
237 NaluCodec::Hevc
238 }))
239 } else {
240 None
241 };
242 loop {
243 match mkv.next_frame(&mut frame) {
244 Ok(true) => {
245 if frame.track == track_number {
246 let raw = std::mem::take(&mut frame.data);
247 total_video_bytes += raw.len() as u64;
248 if let Some(tracker) = mkv_tracker.as_mut() {
249 let annexb = length_prefixed_to_annexb_tracked(
250 &raw,
251 length_size,
252 tracker,
253 &annexb_prepend,
254 );
255 samples.push(annexb);
256 } else {
257 samples.push(raw);
258 }
259 }
260 }
261 Ok(false) => break,
262 Err(e) => bail!("MKV frame read error: {e}"),
263 }
264 }
265
266 let total_frames = samples.len() as u64;
267 let frame_rate = if duration > 0.0 {
272 total_frames as f64 / duration
273 } else if let Some(dd_ns) = track_default_duration_ns.filter(|n| *n > 0) {
274 1_000_000_000.0 / dd_ns as f64
275 } else {
276 30.0
277 };
278
279 let detected_pf = codec::pixel_format::detect(&codec, &samples);
280
281 let bitrate = match tag_bitrate {
286 Some(b) if b > 0 => b,
287 _ => {
288 if duration > 0.0 && total_video_bytes > 0 {
289 ((total_video_bytes as f64 * 8.0) / duration) as u64
290 } else {
291 0
292 }
293 }
294 };
295
296 let info = StreamInfo {
297 codec: codec.clone(),
298 width,
299 height,
300 frame_rate,
301 duration,
302 pixel_format: detected_pf,
303 color_space,
304 total_frames,
305 bitrate,
306 color_metadata,
307 };
308
309 let audio = super::audio::extract_mkv_audio(data);
312
313 Ok(DemuxResult {
314 codec,
315 info,
316 samples,
317 audio,
318 })
319}
320
321pub struct MkvStreamingDemuxer {
330 mkv: MatroskaFile<Cursor<Vec<u8>>>,
331 header: DemuxHeader,
332 audio: Option<AudioTrack>,
333 track_number: u64,
334 timestamp_scale: u64,
335 annexb_prepend: Vec<Vec<u8>>,
336 length_size: u8,
337 tracker: Option<ParamSetTracker>,
338 default_duration_ns: Option<u64>,
341 pixel_format_detected: bool,
347}
348
349pub(crate) fn demux_mkv_streaming_init(data: &[u8]) -> Result<MkvStreamingDemuxer> {
350 let owned = data.to_vec();
351 let cursor = Cursor::new(owned.as_slice());
354 let probe =
355 MatroskaFile::open(cursor).map_err(|e| anyhow::anyhow!("reading MKV header: {e}"))?;
356
357 let (
358 track_number,
359 track_uid,
360 codec_id,
361 width,
362 height,
363 annexb_prepend,
364 length_size,
365 color_space,
366 mut color_metadata,
367 mut color_info,
368 track_default_duration_ns,
369 ) = {
370 let track_info = probe
371 .tracks()
372 .iter()
373 .find(|t| t.track_type() == MkvTrackType::Video)
374 .context("no video track in MKV")?;
375
376 let track_number = track_info.track_number().get();
377 let track_uid = track_info.track_uid().get();
378 let codec_id = track_info.codec_id().to_string();
379 let default_duration_ns = track_info.default_duration().map(|d| d.get());
380
381 let (annexb_prepend, length_size): (Vec<Vec<u8>>, u8) = if codec_id == "V_MPEG4/ISO/AVC" {
382 let priv_bytes = track_info
383 .codec_private()
384 .context("V_MPEG4/ISO/AVC CodecPrivate missing")?;
385 let cfg = parse_avcc(priv_bytes).context("V_MPEG4/ISO/AVC CodecPrivate malformed")?;
386 (cfg.parameter_sets, cfg.length_size)
387 } else if codec_id == "V_MPEGH/ISO/HEVC" {
388 let priv_bytes = track_info
389 .codec_private()
390 .context("V_MPEGH/ISO/HEVC CodecPrivate missing")?;
391 let cfg = parse_hvcc(priv_bytes).context("V_MPEGH/ISO/HEVC CodecPrivate malformed")?;
392 (cfg.parameter_sets, cfg.length_size)
393 } else {
394 (Vec::new(), 4)
395 };
396
397 if mkv_codec_needs_annexb(&codec_id) && annexb_prepend.is_empty() {
398 bail!("AVC/HEVC MKV CodecPrivate missing or empty — no parameter sets to prepend");
399 }
400
401 let video = track_info
402 .video()
403 .context("video track missing Video element")?;
404 let w = video.pixel_width().get() as u32;
405 let h = video.pixel_height().get() as u32;
406
407 let (color_space, color_metadata, color_info) = match video.colour() {
408 Some(colour) => colour_to_pipeline(colour),
409 None => (
410 ColorSpace::Bt709,
411 ColorMetadata::default(),
412 MkvColorInfo::default(),
413 ),
414 };
415
416 (
417 track_number,
418 track_uid,
419 codec_id,
420 w,
421 h,
422 annexb_prepend,
423 length_size,
424 color_space,
425 color_metadata,
426 color_info,
427 default_duration_ns,
428 )
429 };
430
431 color_info.max_cll = None;
434 color_info.max_fall = None;
435 color_metadata.content_light_level = None;
436 if let Some(md) = color_metadata.mastering_display.as_mut() {
437 md.primaries_r_y = 0;
438 md.primaries_g_y = 0;
439 md.primaries_b_y = 0;
440 }
441 if let Some(local) = color_info.mastering.as_mut() {
442 local.primary_r_chromaticity_y = None;
443 local.primary_g_chromaticity_y = None;
444 local.primary_b_chromaticity_y = None;
445 }
446 if let Some(fix) = scan_mkv_colour_raw(&owned) {
447 color_info.max_cll = fix.max_cll;
448 color_info.max_fall = fix.max_fall;
449 if fix.max_cll.is_some() || fix.max_fall.is_some() {
450 color_metadata.content_light_level = Some(ContentLightLevel {
451 max_cll: fix.max_cll.unwrap_or(0).min(u16::MAX as u32) as u16,
452 max_fall: fix.max_fall.unwrap_or(0).min(u16::MAX as u32) as u16,
453 });
454 }
455 let chrom = |v: f64| (v * 50_000.0).round().clamp(0.0, u16::MAX as f64) as u16;
456 if let Some(md) = color_metadata.mastering_display.as_mut() {
457 if let Some(y) = fix.primary_r_chromaticity_y {
458 md.primaries_r_y = chrom(y);
459 }
460 if let Some(y) = fix.primary_g_chromaticity_y {
461 md.primaries_g_y = chrom(y);
462 }
463 if let Some(y) = fix.primary_b_chromaticity_y {
464 md.primaries_b_y = chrom(y);
465 }
466 }
467 if let Some(local) = color_info.mastering.as_mut() {
468 if fix.primary_r_chromaticity_y.is_some() {
469 local.primary_r_chromaticity_y = fix.primary_r_chromaticity_y;
470 }
471 if fix.primary_g_chromaticity_y.is_some() {
472 local.primary_g_chromaticity_y = fix.primary_g_chromaticity_y;
473 }
474 if fix.primary_b_chromaticity_y.is_some() {
475 local.primary_b_chromaticity_y = fix.primary_b_chromaticity_y;
476 }
477 }
478 }
479
480 let needs_annexb = mkv_codec_needs_annexb(&codec_id);
481 let codec = match codec_id.as_str() {
482 "V_VP9" => "vp9".to_string(),
483 "V_VP8" => "vp8".to_string(),
484 "V_AV1" => "av1".to_string(),
485 "V_MPEG4/ISO/AVC" => "h264".to_string(),
486 "V_MPEGH/ISO/HEVC" => "h265".to_string(),
487 other => other.to_lowercase(),
488 };
489
490 let timestamp_scale = probe.info().timestamp_scale().get();
491 let duration_ticks = probe.info().duration().unwrap_or(0.0);
492 let duration = duration_ticks * (timestamp_scale as f64) / 1_000_000_000.0;
493 let tag_bitrate = probe
494 .tags()
495 .and_then(|tags| bitrate_from_tags(tags, track_uid));
496 if color_info != MkvColorInfo::default() {
497 tracing::info!(
498 bits_per_channel = ?color_info.bits_per_channel,
499 max_cll = ?color_info.max_cll,
500 max_fall = ?color_info.max_fall,
501 mastering = ?color_info.mastering,
502 "MKV Colour: parsed HDR-adjacent metadata"
503 );
504 }
505
506 drop(probe);
507
508 let audio = super::audio::extract_mkv_audio(&owned);
512
513 let mkv = MatroskaFile::open(Cursor::new(owned.clone()))
515 .map_err(|e| anyhow::anyhow!("opening MKV streaming reader: {e}"))?;
516
517 let bitrate = tag_bitrate.unwrap_or(0);
524
525 let frame_rate = if let Some(dd_ns) = track_default_duration_ns.filter(|n| *n > 0) {
529 1_000_000_000.0 / dd_ns as f64
530 } else if duration > 0.0 {
531 30.0
536 } else {
537 30.0
538 };
539
540 let pixel_format = PixelFormat::Yuv420p;
546
547 let info = StreamInfo {
548 codec: codec.clone(),
549 width,
550 height,
551 frame_rate,
552 duration,
553 pixel_format,
554 color_space,
555 total_frames: 0, bitrate,
557 color_metadata,
558 };
559
560 let tracker = if needs_annexb {
561 Some(ParamSetTracker::new(if codec_id == "V_MPEG4/ISO/AVC" {
562 NaluCodec::Avc
563 } else {
564 NaluCodec::Hevc
565 }))
566 } else {
567 None
568 };
569
570 let _ = needs_annexb; Ok(MkvStreamingDemuxer {
572 mkv,
573 header: DemuxHeader { codec, info },
574 audio,
575 track_number,
576 timestamp_scale,
577 annexb_prepend,
578 length_size,
579 tracker,
580 default_duration_ns: track_default_duration_ns,
581 pixel_format_detected: false,
582 })
583}
584
585impl StreamingDemuxer for MkvStreamingDemuxer {
586 fn header(&self) -> &DemuxHeader {
587 &self.header
588 }
589
590 fn next_video_sample(&mut self) -> Result<Option<Sample>> {
591 let mut frame = MkvFrame::default();
592 loop {
593 match self.mkv.next_frame(&mut frame) {
594 Ok(true) => {
595 if frame.track != self.track_number {
596 continue;
597 }
598 let raw = std::mem::take(&mut frame.data);
599 let data = if let Some(tracker) = self.tracker.as_mut() {
600 length_prefixed_to_annexb_tracked(
601 &raw,
602 self.length_size,
603 tracker,
604 &self.annexb_prepend,
605 )
606 } else {
607 raw
608 };
609 if !self.pixel_format_detected {
615 let detected = codec::pixel_format::detect(
616 &self.header.codec,
617 std::slice::from_ref(&data),
618 );
619 self.header.info.pixel_format = detected;
620 self.pixel_format_detected = true;
621 }
622 let pts_ticks = frame.timestamp.saturating_mul(self.timestamp_scale) as i64;
623 let duration_ticks = frame
624 .duration
625 .or(self.default_duration_ns)
626 .map(|ns| ns.min(u32::MAX as u64) as u32)
627 .unwrap_or(0);
628 return Ok(Some(Sample {
629 data,
630 pts_ticks,
631 duration_ticks,
632 }));
633 }
634 Ok(false) => return Ok(None),
635 Err(e) => bail!("MKV frame read error: {e}"),
636 }
637 }
638 }
639
640 fn audio(&self) -> Option<&AudioTrack> {
641 self.audio.as_ref()
642 }
643}
644
645pub fn probe_mkv_color_info(data: &[u8]) -> Option<MkvColorInfo> {
656 let cursor = Cursor::new(data);
657 let mkv = MatroskaFile::open(cursor).ok()?;
658 let track = mkv
659 .tracks()
660 .iter()
661 .find(|t| t.track_type() == MkvTrackType::Video)?;
662 let colour = track.video()?.colour()?;
663 let (_, _, mut info) = colour_to_pipeline(colour);
664
665 info.max_cll = None;
675 info.max_fall = None;
676 if let Some(local) = info.mastering.as_mut() {
677 local.primary_r_chromaticity_y = None;
678 local.primary_g_chromaticity_y = None;
679 local.primary_b_chromaticity_y = None;
680 }
681 if let Some(fix) = scan_mkv_colour_raw(data) {
682 info.max_cll = fix.max_cll;
683 info.max_fall = fix.max_fall;
684 if let Some(local) = info.mastering.as_mut() {
685 if fix.primary_r_chromaticity_y.is_some() {
686 local.primary_r_chromaticity_y = fix.primary_r_chromaticity_y;
687 }
688 if fix.primary_g_chromaticity_y.is_some() {
689 local.primary_g_chromaticity_y = fix.primary_g_chromaticity_y;
690 }
691 if fix.primary_b_chromaticity_y.is_some() {
692 local.primary_b_chromaticity_y = fix.primary_b_chromaticity_y;
693 }
694 }
695 }
696 Some(info)
697}
698
699pub(super) fn mkv_codec_needs_annexb(codec_id: &str) -> bool {
708 matches!(codec_id, "V_MPEG4/ISO/AVC" | "V_MPEGH/ISO/HEVC")
709}
710