diff options
| -rw-r--r-- | remuxer/src/bin/analyze_kf_placement.rs | 49 | ||||
| -rw-r--r-- | remuxer/src/bin/average_cluster_duration.rs | 13 | ||||
| -rw-r--r-- | remuxer/src/demuxers/matroska.rs | 21 | ||||
| -rw-r--r-- | remuxer/src/muxers/matroska.rs | 2 | ||||
| -rw-r--r-- | stream/src/fragment.rs | 24 | ||||
| -rw-r--r-- | transcoder/src/fragment.rs | 23 |
6 files changed, 108 insertions, 24 deletions
diff --git a/remuxer/src/bin/analyze_kf_placement.rs b/remuxer/src/bin/analyze_kf_placement.rs new file mode 100644 index 0000000..7a5a01d --- /dev/null +++ b/remuxer/src/bin/analyze_kf_placement.rs @@ -0,0 +1,49 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ + +use anyhow::{Result, anyhow}; +use jellyremuxer::demuxers::{Demuxer, DemuxerNew, matroska::MatroskaDemuxer}; +use std::{env::args, fs::File}; +use winter_matroska::TrackType; + +fn main() -> Result<()> { + env_logger::init_from_env("LOG"); + let path = args().nth(1).ok_or(anyhow!("first arg is input path"))?; + let file = File::open(path)?; + let mut reader = MatroskaDemuxer::new(Box::new(file)); + + let tracks = reader.tracks()?.unwrap(); + let video_track = tracks + .entries + .iter() + .find(|t| matches!(t.track_type, TrackType::Video)) + .map(|t| t.track_number) + .unwrap(); + + reader.seek_cluster(None)?; + let mut num_kf_first = 0; + let mut num_kf_later = 0; + while let Some((_, cluster)) = reader.read_cluster()? { + let mut first = true; + for block in cluster.simple_blocks { + if block.track != video_track { + continue; + } + if block.flags.keyframe() { + if first { + num_kf_first += 1 + } else { + num_kf_later += 1; + } + } + first = false + } + } + + println!("{num_kf_first:>4} kf first"); + println!("{num_kf_later:>4} kf later"); + Ok(()) +} diff --git a/remuxer/src/bin/average_cluster_duration.rs b/remuxer/src/bin/average_cluster_duration.rs index 69bb79c..41effb4 100644 --- a/remuxer/src/bin/average_cluster_duration.rs +++ b/remuxer/src/bin/average_cluster_duration.rs @@ -6,7 +6,7 @@ use anyhow::{Result, anyhow}; use jellyremuxer::demuxers::{Demuxer, DemuxerNew, matroska::MatroskaDemuxer}; -use std::{env::args, fs::File}; +use std::{collections::BTreeMap, env::args, fs::File}; fn main() -> Result<()> { env_logger::init_from_env("LOG"); @@ -18,21 +18,22 @@ fn main() -> Result<()> { reader.seek_cluster(None)?; let mut num_clusters = 0; + let mut num_blocks = BTreeMap::<u64, usize>::new(); let mut last_ts = 0; let mut total_size = 0; while let Some((_, cluster)) = reader.read_cluster()? { last_ts = cluster.timestamp * info.timestamp_scale; num_clusters += 1; - total_size += cluster - .simple_blocks - .iter() - .map(|b| b.data.len()) - .sum::<usize>() as u64 + for block in &cluster.simple_blocks { + total_size += block.data.len() as u64; + *num_blocks.entry(block.track).or_default() += 1; + } } let average_duration = (last_ts / num_clusters) as f64 / 1_000_000_000.; let average_size = (total_size / num_clusters) as f64 / 1_000_000.; println!("{average_duration:>6.02}s {average_size:>6.02}MB"); + println!("{num_blocks:?}"); Ok(()) } diff --git a/remuxer/src/demuxers/matroska.rs b/remuxer/src/demuxers/matroska.rs index 21e8b2e..923db81 100644 --- a/remuxer/src/demuxers/matroska.rs +++ b/remuxer/src/demuxers/matroska.rs @@ -99,8 +99,9 @@ impl MatroskaDemuxer { Ok(self.seek_head.as_ref()) } - /// Seeks to the content of child tag of Segment possibly optimized via SeekHead. Returns the size of the content. - pub fn seek_to_segment_tag(&mut self, search_tag: u64) -> Result<Option<u64>> { + /// Seeks to the content of child tag of Segment possibly optimized via SeekHead. + /// Returns the absolute offset of the element start and size of the content. + pub fn seek_to_segment_tag(&mut self, search_tag: u64) -> Result<Option<(u64, u64)>> { if let Some(seek_head) = self.seek_head()? { let Some(segment_position) = seek_head .seeks @@ -111,22 +112,23 @@ impl MatroskaDemuxer { return Ok(None); }; let segment_offset = self.segment_offset()?; - self.reader - .seek(SeekFrom::Start(segment_offset + segment_position))?; + let absolute_offset = segment_offset + segment_position; + self.reader.seek(SeekFrom::Start(absolute_offset))?; let tag = self.reader.read_vint()?; let size = self.reader.read_vint()?; if tag != search_tag { bail!("SeekHead was lying (expected {search_tag:?}, got {tag:x})"); } - Ok(Some(size)) + Ok(Some((absolute_offset, size))) } else { self.seek_segment_start()?; loop { + let pos = self.reader.stream_position()?; let tag = self.reader.read_vint()?; let size = self.reader.read_vint()?; if tag == search_tag { - break Ok(Some(size)); + break Ok(Some((pos, size))); } if tag == Segment::TAG_CLUSTERS { break Ok(None); @@ -147,7 +149,7 @@ impl MatroskaDemuxer { tag: u64, ) -> Result<Option<Tag>> { debug!("reading {name:?}"); - let Some(size) = self.seek_to_segment_tag(tag)? else { + let Some((_, size)) = self.seek_to_segment_tag(tag)? else { return Ok(None); }; self.read_tag(size) @@ -181,7 +183,10 @@ impl Demuxer for MatroskaDemuxer { if let Some(pos) = position { self.reader.seek(SeekFrom::Start(pos))?; } else { - self.seek_to_segment_tag(Segment::TAG_CLUSTERS)?; + let Some((pos, _)) = self.seek_to_segment_tag(Segment::TAG_CLUSTERS)? else { + bail!("no clusters found"); + }; + self.reader.seek(SeekFrom::Start(pos))?; } Ok(()) } diff --git a/remuxer/src/muxers/matroska.rs b/remuxer/src/muxers/matroska.rs index c2f22e7..228f420 100644 --- a/remuxer/src/muxers/matroska.rs +++ b/remuxer/src/muxers/matroska.rs @@ -12,7 +12,7 @@ use winter_matroska::{MatroskaFile, Segment}; fn write_fragment_shared(out: &mut dyn Write, mut segment: Segment, webm: bool) -> Result<()> { segment.info.muxing_app = - concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).to_string(); + concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION")).to_string(); if webm { if let Some(tracks) = &mut segment.tracks { for track in &mut tracks.entries { diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index 4cdc55e..0039bc5 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -78,19 +78,30 @@ pub fn fragment_stream( entries: vec![mk_track.to_owned()], }; - let mut cluster = { + let (mut cluster, next_cluster) = { let media_file = File::open(&media_path)?; let mut media = create_demuxer_autodetect(Box::new(media_file))? .ok_or(anyhow!("media container unknown"))?; media.seek_cluster(Some(cluster_offset))?; - media - .read_cluster()? - .ok_or(anyhow!("cluster unexpectedly missing"))? - .1 + ( + media + .read_cluster()? + .ok_or(anyhow!("cluster unexpectedly missing"))? + .1, + media.read_cluster()?.map(|(_, x)| x), + ) }; cluster.simple_blocks.retain(|b| b.track == track_num); cluster.block_groups.retain(|b| b.block.track == track_num); + let next_kf = next_cluster + .map(|x| { + x.simple_blocks + .iter() + .find(|b| b.track == track_num) + .cloned() + }) + .flatten(); let jr_container = match container { StreamContainer::WebM => ContainerFormat::Webm, @@ -107,7 +118,7 @@ pub fn fragment_stream( ..Default::default() }; segment.info.writing_app = - concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).to_string(); + concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION")).to_string(); if !format.remux { segment = transcode( @@ -115,6 +126,7 @@ pub fn fragment_stream( &format!("{media_path:?} {track_num} {index}"), format, segment, + next_kf, )?; } diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index c94b877..985c1a4 100644 --- a/transcoder/src/fragment.rs +++ b/transcoder/src/fragment.rs @@ -14,7 +14,8 @@ use std::fs::File; use std::io::{copy, Write as W2}; use std::process::{Command, Stdio}; use std::thread::spawn; -use winter_matroska::{Segment, TrackEntry as MatroskaTrackEntry}; +use winter_matroska::block::Block; +use winter_matroska::{Cluster, Segment, TrackEntry as MatroskaTrackEntry}; // TODO odd video resolutions can cause errors when transcoding to YUV42{0,2} // TODO with an implementation that cant handle it (SVT-AV1 is such an impl). @@ -23,7 +24,8 @@ pub fn transcode( kind: TrackKind, input_key: &str, output_format: &StreamFormatInfo, - input: Segment, + mut input: Segment, + next_kf: Option<Block>, ) -> Result<Segment> { let command = transcode_command( kind, @@ -33,6 +35,9 @@ pub fn transcode( ) .unwrap(); + let input_duration = input.info.duration; + let had_next_kf = next_kf.is_some(); + let output = cache_file("frag-tc", (input_key, &command), |mut output| { let _permit = LOCAL_VIDEO_TRANSCODING_TASKS.lock().unwrap(); info!("encoding with {command:?}"); @@ -50,6 +55,11 @@ pub fn transcode( copy(&mut stdout, &mut output).unwrap(); }); + input.clusters.extend(next_kf.map(|kf| Cluster { + simple_blocks: vec![kf], + ..Default::default() + })); + write_fragment(ContainerFormat::Matroska, &mut stdin, input)?; stdin.flush()?; drop(stdin); @@ -64,12 +74,19 @@ pub fn transcode( Box::new(File::open(output.abs())?), ); - let info = demuxer.info()?; + let mut info = demuxer.info()?; + info.duration = input_duration; let tracks = demuxer.tracks()?; let mut clusters = Vec::new(); while let Some((_, cluster)) = demuxer.read_cluster()? { clusters.push(cluster); } + if had_next_kf { + if let Some(c) = clusters.last_mut() { + c.simple_blocks.pop().expect("empty last cluster"); + } + clusters.retain(|c| !c.simple_blocks.is_empty() || !c.block_groups.is_empty()); + } Ok(Segment { info, |