aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--remuxer/src/bin/analyze_kf_placement.rs49
-rw-r--r--remuxer/src/bin/average_cluster_duration.rs13
-rw-r--r--remuxer/src/demuxers/matroska.rs21
-rw-r--r--remuxer/src/muxers/matroska.rs2
-rw-r--r--stream/src/fragment.rs24
-rw-r--r--transcoder/src/fragment.rs23
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,