v0.11.5: MKV container fixes — timestamps, frame rate, HDR, chapters, disposition
This commit is contained in:
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.5 (2026-04-18)
|
||||||
|
|
||||||
|
### MKV container fixes — Jellyfin/player compatibility
|
||||||
|
- **Timestamp normalization** — MKV and M2TS output starts at 0.000s instead of raw disc PTS offset. Fixes playback failures in Jellyfin and other players.
|
||||||
|
- **DefaultDuration** — correct frame rate written to MKV track header. Fixes wrong avg_frame_rate (was 293/12, now 24000/1001).
|
||||||
|
- **HDR Colour metadata** — MatrixCoefficients, TransferCharacteristics, Primaries, Range written to MKV video track. Enables HDR tone mapping in players.
|
||||||
|
- **DisplayWidth/DisplayHeight** — aspect ratio fields in MKV video track.
|
||||||
|
- **Chapters (Blu-ray)** — accept mark_type 0 as chapter entry (was filtering to type 1 only, which no disc uses).
|
||||||
|
- **Chapters (DVD)** — extract chapter timestamps from PGC program map + cell durations.
|
||||||
|
- **Default disposition** — only first video and first audio track marked default. Fixes wrong auto-selection in players.
|
||||||
|
|
||||||
## 0.11.3 (2026-04-18)
|
## 0.11.3 (2026-04-18)
|
||||||
|
|
||||||
### Unified versioning
|
### Unified versioning
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "0.11.3"
|
version = "0.11.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
+2
-2
@@ -159,12 +159,12 @@ impl Disc {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Convert marks to chapters (filter mark_type == 1 = chapter entry)
|
// Convert marks to chapters (mark_type 0 or 1 = chapter entry, 2 = link)
|
||||||
let first_in_time = parsed.play_items.first().map(|pi| pi.in_time).unwrap_or(0);
|
let first_in_time = parsed.play_items.first().map(|pi| pi.in_time).unwrap_or(0);
|
||||||
let chapters: Vec<Chapter> = parsed
|
let chapters: Vec<Chapter> = parsed
|
||||||
.marks
|
.marks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|m| m.mark_type == 1)
|
.filter(|m| m.mark_type <= 1)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, m)| {
|
.map(|(i, m)| {
|
||||||
let time_secs = (m.timestamp as f64 - first_in_time as f64) / 45000.0;
|
let time_secs = (m.timestamp as f64 - first_in_time as f64) / 45000.0;
|
||||||
|
|||||||
+11
-1
@@ -101,6 +101,16 @@ impl Disc {
|
|||||||
streams.extend(audio_streams.iter().cloned());
|
streams.extend(audio_streams.iter().cloned());
|
||||||
streams.extend(subtitle_streams);
|
streams.extend(subtitle_streams);
|
||||||
|
|
||||||
|
let chapters: Vec<Chapter> = dvd_title
|
||||||
|
.chapter_times
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, &t)| Chapter {
|
||||||
|
time_secs: t,
|
||||||
|
name: format!("Chapter {}", i + 1),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
titles.push(DiscTitle {
|
titles.push(DiscTitle {
|
||||||
playlist: format!("VTS_{:02}_{}.VOB", ts.vts_number, title_number),
|
playlist: format!("VTS_{:02}_{}.VOB", ts.vts_number, title_number),
|
||||||
playlist_id: title_number,
|
playlist_id: title_number,
|
||||||
@@ -108,7 +118,7 @@ impl Disc {
|
|||||||
size_bytes,
|
size_bytes,
|
||||||
clips: Vec::new(),
|
clips: Vec::new(),
|
||||||
streams,
|
streams,
|
||||||
chapters: Vec::new(),
|
chapters,
|
||||||
extents,
|
extents,
|
||||||
content_format: ContentFormat::MpegPs,
|
content_format: ContentFormat::MpegPs,
|
||||||
codec_privates: Vec::new(),
|
codec_privates: Vec::new(),
|
||||||
|
|||||||
+35
@@ -47,6 +47,8 @@ pub struct DvdTitle {
|
|||||||
pub duration_secs: f64,
|
pub duration_secs: f64,
|
||||||
/// Cell sector ranges
|
/// Cell sector ranges
|
||||||
pub cells: Vec<DvdCell>,
|
pub cells: Vec<DvdCell>,
|
||||||
|
/// Chapter start times in seconds (derived from program map + cell times)
|
||||||
|
pub chapter_times: Vec<f64>,
|
||||||
/// Subtitle palette from PGC: 16 entries of [padding, Y, Cb, Cr].
|
/// Subtitle palette from PGC: 16 entries of [padding, Y, Cb, Cr].
|
||||||
pub palette: Option<Vec<[u8; 4]>>,
|
pub palette: Option<Vec<[u8; 4]>>,
|
||||||
}
|
}
|
||||||
@@ -541,6 +543,37 @@ fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result<DvdTitle>
|
|||||||
duration_secs
|
duration_secs
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extract chapter times from program map + cell durations
|
||||||
|
// PGC program map offset at 0xE6, maps program_number → first cell_number
|
||||||
|
let chapter_times = {
|
||||||
|
let pgm_map_offset = be_u16(data, pgc_offset + 0xE6).unwrap_or(0) as usize;
|
||||||
|
let nr_of_programs = byte_at(data, pgc_offset + 0x02).unwrap_or(0) as usize;
|
||||||
|
let mut times = Vec::new();
|
||||||
|
if pgm_map_offset > 0 && nr_of_programs > 0 && cell_playback_offset > 0 {
|
||||||
|
let pgm_base = pgc_offset + pgm_map_offset;
|
||||||
|
// Collect cell durations
|
||||||
|
let mut cell_durations = Vec::with_capacity(num_cells);
|
||||||
|
let cell_base = pgc_offset + cell_playback_offset;
|
||||||
|
for i in 0..num_cells {
|
||||||
|
let co = cell_base + i * 24;
|
||||||
|
if co + 8 <= data.len() {
|
||||||
|
cell_durations.push(bcd_to_secs(&data[co + 4..co + 8]));
|
||||||
|
} else {
|
||||||
|
cell_durations.push(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Program map: each byte is the first cell number (1-based) for that program
|
||||||
|
for p in 0..nr_of_programs {
|
||||||
|
if pgm_base + p >= data.len() { break; }
|
||||||
|
let first_cell = data[pgm_base + p] as usize;
|
||||||
|
// Chapter time = sum of cell durations before this program's first cell
|
||||||
|
let time: f64 = cell_durations[..first_cell.saturating_sub(1)].iter().sum();
|
||||||
|
times.push(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
times
|
||||||
|
};
|
||||||
|
|
||||||
// Extract subtitle palette at PGC offset 0xA4: 16 colors × 4 bytes [padding, Y, Cb, Cr]
|
// Extract subtitle palette at PGC offset 0xA4: 16 colors × 4 bytes [padding, Y, Cb, Cr]
|
||||||
let palette = if pgc_offset + 0xA4 + 64 <= data.len() {
|
let palette = if pgc_offset + 0xA4 + 64 <= data.len() {
|
||||||
let mut colors = Vec::with_capacity(16);
|
let mut colors = Vec::with_capacity(16);
|
||||||
@@ -562,6 +595,7 @@ fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result<DvdTitle>
|
|||||||
chapters,
|
chapters,
|
||||||
duration_secs,
|
duration_secs,
|
||||||
cells,
|
cells,
|
||||||
|
chapter_times,
|
||||||
palette,
|
palette,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -651,6 +685,7 @@ mod tests {
|
|||||||
chapters: 5,
|
chapters: 5,
|
||||||
duration_secs: 3600.0,
|
duration_secs: 3600.0,
|
||||||
cells: vec![cell.clone()],
|
cells: vec![cell.clone()],
|
||||||
|
chapter_times: Vec::new(),
|
||||||
palette: None,
|
palette: None,
|
||||||
};
|
};
|
||||||
assert_eq!(title.chapters, 5);
|
assert_eq!(title.chapters, 5);
|
||||||
|
|||||||
+1
-1
@@ -256,7 +256,7 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
|
|||||||
|
|
||||||
// Parse PlayListMark section
|
// Parse PlayListMark section
|
||||||
let mut marks = Vec::new();
|
let mut marks = Vec::new();
|
||||||
if mark_start > 0 && mark_start + 4 < data.len() {
|
if mark_start > 0 && mark_start + 4 <= data.len() {
|
||||||
let ms = &data[mark_start..];
|
let ms = &data[mark_start..];
|
||||||
if ms.len() >= 6 {
|
if ms.len() >= 6 {
|
||||||
let num_marks = u16::from_be_bytes([ms[4], ms[5]]) as usize;
|
let num_marks = u16::from_be_bytes([ms[4], ms[5]]) as usize;
|
||||||
|
|||||||
+86
-3
@@ -5,7 +5,7 @@
|
|||||||
//! cues and seek head are finalized at the end.
|
//! cues and seek head are finalized at the end.
|
||||||
|
|
||||||
use super::ebml;
|
use super::ebml;
|
||||||
use crate::disc::{AudioStream, Chapter, Codec, SubtitleStream, VideoStream};
|
use crate::disc::{AudioStream, Chapter, Codec, ColorSpace, HdrFormat, SubtitleStream, VideoStream};
|
||||||
use std::io::{self, Seek, SeekFrom, Write};
|
use std::io::{self, Seek, SeekFrom, Write};
|
||||||
|
|
||||||
/// MKV track definition (built from disc stream metadata).
|
/// MKV track definition (built from disc stream metadata).
|
||||||
@@ -20,6 +20,14 @@ pub struct MkvTrack {
|
|||||||
// Video-specific
|
// Video-specific
|
||||||
pub pixel_width: u32,
|
pub pixel_width: u32,
|
||||||
pub pixel_height: u32,
|
pub pixel_height: u32,
|
||||||
|
pub default_duration_ns: u64, // nanoseconds per frame (0 = unknown)
|
||||||
|
pub display_width: u32, // display aspect ratio width (0 = same as pixel)
|
||||||
|
pub display_height: u32, // display aspect ratio height (0 = same as pixel)
|
||||||
|
// HDR colour metadata
|
||||||
|
pub colour_matrix: u8, // MatrixCoefficients (9=bt2020nc)
|
||||||
|
pub colour_transfer: u8, // TransferCharacteristics (16=smpte2084/PQ)
|
||||||
|
pub colour_primaries: u8, // Primaries (9=bt2020)
|
||||||
|
pub colour_range: u8, // Range (1=tv/limited)
|
||||||
// Audio-specific
|
// Audio-specific
|
||||||
pub sample_rate: f64,
|
pub sample_rate: f64,
|
||||||
pub channels: u8,
|
pub channels: u8,
|
||||||
@@ -36,16 +44,40 @@ impl MkvTrack {
|
|||||||
_ => "V_MPEG2",
|
_ => "V_MPEG2",
|
||||||
};
|
};
|
||||||
let (w, h) = v.resolution.pixels();
|
let (w, h) = v.resolution.pixels();
|
||||||
|
let (num, den) = v.frame_rate.as_fraction();
|
||||||
|
let default_duration_ns = if num > 0 {
|
||||||
|
(1_000_000_000u64 * den as u64) / num as u64
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let (matrix, transfer, primaries, range) = match v.color_space {
|
||||||
|
ColorSpace::Bt2020 => (9, 16, 9, 1), // bt2020nc, PQ, bt2020, limited
|
||||||
|
ColorSpace::Bt709 => (1, 1, 1, 1), // bt709
|
||||||
|
ColorSpace::Unknown => (0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
// Override transfer for non-PQ HDR
|
||||||
|
let transfer = match v.hdr {
|
||||||
|
HdrFormat::Hdr10 | HdrFormat::Hdr10Plus | HdrFormat::DolbyVision => 16, // PQ
|
||||||
|
HdrFormat::Hlg => 18,
|
||||||
|
_ => transfer,
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
track_type: ebml::TRACK_TYPE_VIDEO,
|
track_type: ebml::TRACK_TYPE_VIDEO,
|
||||||
codec_id,
|
codec_id,
|
||||||
language: "und".into(),
|
language: "und".into(),
|
||||||
name: v.label.clone(),
|
name: v.label.clone(),
|
||||||
codec_private: None, // filled later by parser
|
codec_private: None,
|
||||||
is_default: !v.secondary,
|
is_default: !v.secondary,
|
||||||
is_forced: false,
|
is_forced: false,
|
||||||
pixel_width: w,
|
pixel_width: w,
|
||||||
pixel_height: h,
|
pixel_height: h,
|
||||||
|
default_duration_ns,
|
||||||
|
display_width: w,
|
||||||
|
display_height: h,
|
||||||
|
colour_matrix: matrix,
|
||||||
|
colour_transfer: transfer,
|
||||||
|
colour_primaries: primaries,
|
||||||
|
colour_range: range,
|
||||||
sample_rate: 0.0,
|
sample_rate: 0.0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -73,6 +105,13 @@ impl MkvTrack {
|
|||||||
is_forced: false,
|
is_forced: false,
|
||||||
pixel_width: 0,
|
pixel_width: 0,
|
||||||
pixel_height: 0,
|
pixel_height: 0,
|
||||||
|
default_duration_ns: 0,
|
||||||
|
display_width: 0,
|
||||||
|
display_height: 0,
|
||||||
|
colour_matrix: 0,
|
||||||
|
colour_transfer: 0,
|
||||||
|
colour_primaries: 0,
|
||||||
|
colour_range: 0,
|
||||||
sample_rate: sr,
|
sample_rate: sr,
|
||||||
channels: ch,
|
channels: ch,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -94,6 +133,13 @@ impl MkvTrack {
|
|||||||
is_forced: s.forced,
|
is_forced: s.forced,
|
||||||
pixel_width: 0,
|
pixel_width: 0,
|
||||||
pixel_height: 0,
|
pixel_height: 0,
|
||||||
|
default_duration_ns: 0,
|
||||||
|
display_width: 0,
|
||||||
|
display_height: 0,
|
||||||
|
colour_matrix: 0,
|
||||||
|
colour_transfer: 0,
|
||||||
|
colour_primaries: 0,
|
||||||
|
colour_range: 0,
|
||||||
sample_rate: 0.0,
|
sample_rate: 0.0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -116,6 +162,7 @@ pub struct MkvMuxer<W: Write + Seek> {
|
|||||||
cluster_pos: u64,
|
cluster_pos: u64,
|
||||||
cluster_size_pos: u64,
|
cluster_size_pos: u64,
|
||||||
cluster_ts_ms: i64,
|
cluster_ts_ms: i64,
|
||||||
|
base_pts_ms: Option<i64>,
|
||||||
cues: Vec<CuePoint>,
|
cues: Vec<CuePoint>,
|
||||||
frame_count: u64,
|
frame_count: u64,
|
||||||
/// File positions of codecPrivate placeholders (track_idx → offset, max_size).
|
/// File positions of codecPrivate placeholders (track_idx → offset, max_size).
|
||||||
@@ -216,11 +263,29 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
codec_private_filled.push(true);
|
codec_private_filled.push(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultDuration — frame duration in nanoseconds
|
||||||
|
if track.default_duration_ns > 0 {
|
||||||
|
ebml::write_uint(&mut writer, ebml::DEFAULT_DURATION, track.default_duration_ns)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Video-specific
|
// Video-specific
|
||||||
if track.track_type == ebml::TRACK_TYPE_VIDEO && track.pixel_width > 0 {
|
if track.track_type == ebml::TRACK_TYPE_VIDEO && track.pixel_width > 0 {
|
||||||
let vid_pos = ebml::start_master(&mut writer, ebml::VIDEO)?;
|
let vid_pos = ebml::start_master(&mut writer, ebml::VIDEO)?;
|
||||||
ebml::write_uint(&mut writer, ebml::PIXEL_WIDTH, track.pixel_width as u64)?;
|
ebml::write_uint(&mut writer, ebml::PIXEL_WIDTH, track.pixel_width as u64)?;
|
||||||
ebml::write_uint(&mut writer, ebml::PIXEL_HEIGHT, track.pixel_height as u64)?;
|
ebml::write_uint(&mut writer, ebml::PIXEL_HEIGHT, track.pixel_height as u64)?;
|
||||||
|
if track.display_width > 0 && track.display_height > 0 {
|
||||||
|
ebml::write_uint(&mut writer, ebml::DISPLAY_WIDTH, track.display_width as u64)?;
|
||||||
|
ebml::write_uint(&mut writer, ebml::DISPLAY_HEIGHT, track.display_height as u64)?;
|
||||||
|
}
|
||||||
|
// Colour metadata (HDR)
|
||||||
|
if track.colour_matrix > 0 || track.colour_transfer > 0 {
|
||||||
|
let col_pos = ebml::start_master(&mut writer, ebml::COLOUR)?;
|
||||||
|
ebml::write_uint(&mut writer, ebml::MATRIX_COEFFICIENTS, track.colour_matrix as u64)?;
|
||||||
|
ebml::write_uint(&mut writer, ebml::TRANSFER_CHARACTERISTICS, track.colour_transfer as u64)?;
|
||||||
|
ebml::write_uint(&mut writer, ebml::PRIMARIES, track.colour_primaries as u64)?;
|
||||||
|
ebml::write_uint(&mut writer, ebml::RANGE, track.colour_range as u64)?;
|
||||||
|
ebml::end_master(&mut writer, col_pos)?;
|
||||||
|
}
|
||||||
ebml::end_master(&mut writer, vid_pos)?;
|
ebml::end_master(&mut writer, vid_pos)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +330,7 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
cluster_pos: 0,
|
cluster_pos: 0,
|
||||||
cluster_size_pos: 0,
|
cluster_size_pos: 0,
|
||||||
cluster_ts_ms: 0,
|
cluster_ts_ms: 0,
|
||||||
|
base_pts_ms: None,
|
||||||
cues: Vec::new(),
|
cues: Vec::new(),
|
||||||
frame_count: 0,
|
frame_count: 0,
|
||||||
codec_private_slots,
|
codec_private_slots,
|
||||||
@@ -280,7 +346,9 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
keyframe: bool,
|
keyframe: bool,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let pts_ms = pts_ns / 1_000_000;
|
let raw_ms = pts_ns / 1_000_000;
|
||||||
|
let base = *self.base_pts_ms.get_or_insert(raw_ms);
|
||||||
|
let pts_ms = raw_ms - base;
|
||||||
|
|
||||||
// Start new cluster if needed
|
// Start new cluster if needed
|
||||||
if !self.cluster_open || (pts_ms - self.cluster_ts_ms) >= CLUSTER_DURATION_MS {
|
if !self.cluster_open || (pts_ms - self.cluster_ts_ms) >= CLUSTER_DURATION_MS {
|
||||||
@@ -450,6 +518,13 @@ mod tests {
|
|||||||
is_forced: false,
|
is_forced: false,
|
||||||
pixel_width: 1920,
|
pixel_width: 1920,
|
||||||
pixel_height: 1080,
|
pixel_height: 1080,
|
||||||
|
default_duration_ns: 41708333,
|
||||||
|
display_width: 1920,
|
||||||
|
display_height: 1080,
|
||||||
|
colour_matrix: 0,
|
||||||
|
colour_transfer: 0,
|
||||||
|
colour_primaries: 0,
|
||||||
|
colour_range: 0,
|
||||||
sample_rate: 0.0,
|
sample_rate: 0.0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -467,6 +542,13 @@ mod tests {
|
|||||||
is_forced: false,
|
is_forced: false,
|
||||||
pixel_width: 0,
|
pixel_width: 0,
|
||||||
pixel_height: 0,
|
pixel_height: 0,
|
||||||
|
default_duration_ns: 0,
|
||||||
|
display_width: 0,
|
||||||
|
display_height: 0,
|
||||||
|
colour_matrix: 0,
|
||||||
|
colour_transfer: 0,
|
||||||
|
colour_primaries: 0,
|
||||||
|
colour_range: 0,
|
||||||
sample_rate: 48000.0,
|
sample_rate: 48000.0,
|
||||||
channels: 6,
|
channels: 6,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -760,3 +842,4 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,22 @@ impl MkvStream {
|
|||||||
/// Codec privates come from title.codec_privates (populated by input stream).
|
/// Codec privates come from title.codec_privates (populated by input stream).
|
||||||
pub fn create(writer: Box<dyn WriteSeek>, title: &DiscTitle) -> io::Result<Self> {
|
pub fn create(writer: Box<dyn WriteSeek>, title: &DiscTitle) -> io::Result<Self> {
|
||||||
let mut tracks = Vec::new();
|
let mut tracks = Vec::new();
|
||||||
|
let mut has_default_video = false;
|
||||||
|
let mut has_default_audio = false;
|
||||||
for (idx, s) in title.streams.iter().enumerate() {
|
for (idx, s) in title.streams.iter().enumerate() {
|
||||||
let mut track = match s {
|
let mut track = match s {
|
||||||
crate::disc::Stream::Video(v) => MkvTrack::video(v),
|
crate::disc::Stream::Video(v) => MkvTrack::video(v),
|
||||||
crate::disc::Stream::Audio(a) => MkvTrack::audio(a),
|
crate::disc::Stream::Audio(a) => MkvTrack::audio(a),
|
||||||
crate::disc::Stream::Subtitle(s) => MkvTrack::subtitle(s),
|
crate::disc::Stream::Subtitle(s) => MkvTrack::subtitle(s),
|
||||||
};
|
};
|
||||||
|
// Only first video and first audio are default
|
||||||
|
if track.is_default {
|
||||||
|
match track.track_type {
|
||||||
|
1 if !has_default_video => has_default_video = true,
|
||||||
|
2 if !has_default_audio => has_default_audio = true,
|
||||||
|
_ => track.is_default = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(cp) = title.codec_privates.get(idx).and_then(|c| c.as_ref()) {
|
if let Some(cp) = title.codec_privates.get(idx).and_then(|c| c.as_ref()) {
|
||||||
track.codec_private = Some(cp.clone());
|
track.codec_private = Some(cp.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub struct TsMuxer<W: Write> {
|
|||||||
continuity: Vec<u8>, // per-PID continuity counter (0-15)
|
continuity: Vec<u8>, // per-PID continuity counter (0-15)
|
||||||
codec_privates: Vec<Option<Vec<u8>>>, // per-track codec_private (for video parameter sets)
|
codec_privates: Vec<Option<Vec<u8>>>, // per-track codec_private (for video parameter sets)
|
||||||
params_written: Vec<bool>, // per-track: have we written parameter sets?
|
params_written: Vec<bool>, // per-track: have we written parameter sets?
|
||||||
|
base_pts_ns: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: Write> TsMuxer<W> {
|
impl<W: Write> TsMuxer<W> {
|
||||||
@@ -26,6 +27,7 @@ impl<W: Write> TsMuxer<W> {
|
|||||||
continuity: vec![0u8; n],
|
continuity: vec![0u8; n],
|
||||||
codec_privates: vec![None; n],
|
codec_privates: vec![None; n],
|
||||||
params_written: vec![false; n],
|
params_written: vec![false; n],
|
||||||
|
base_pts_ns: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +46,9 @@ impl<W: Write> TsMuxer<W> {
|
|||||||
if track >= self.pids.len() {
|
if track >= self.pids.len() {
|
||||||
return Ok(()); // unknown track, skip
|
return Ok(()); // unknown track, skip
|
||||||
}
|
}
|
||||||
|
let base = *self.base_pts_ns.get_or_insert(pts_ns);
|
||||||
|
let pts_ns = pts_ns - base;
|
||||||
|
|
||||||
let pid = self.pids[track];
|
let pid = self.pids[track];
|
||||||
let is_video = (0x1011..=0x101F).contains(&pid);
|
let is_video = (0x1011..=0x101F).contains(&pid);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user