/* 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 */ use crate::{ ebml_header, ebml_segment_info, ebml_track_entry, metadata::{matroska_metadata, MatroskaMetadata}, seek_index::get_seek_index, segment_extractor::SegmentExtractIter, }; use anyhow::{anyhow, Context, Result}; use jellymatroska::{read::EbmlReader, write::EbmlWriter, Master, MatroskaTag}; use log::{debug, info}; use std::{ fs::File, io::{BufReader, BufWriter, Write}, ops::Range, path::Path, }; const FRAGMENT_LENGTH: f64 = 4.; pub fn fragment_index(path: &Path, track: u64) -> Result>> { let meta = matroska_metadata(path)?; let duration = media_duration(&meta); let force_kf = meta .as_ref() .tracks .as_ref() .unwrap() .entries .iter() .find(|t| t.track_number == track) .unwrap() .track_type == 17; let index = get_seek_index(path)?; let index = index .get(&track) .ok_or(anyhow!("seek index track missing"))?; let n_kf = if force_kf { index.blocks.len() } else { index.keyframes.len() }; let average_kf_interval = duration / n_kf as f64; let kf_per_frag = (FRAGMENT_LENGTH / average_kf_interval).ceil() as usize; debug!("average keyframe interval: {average_kf_interval}"); debug!(" => keyframes per frag {kf_per_frag}"); let n_frags = n_kf.div_ceil(kf_per_frag); Ok((0..n_frags) .map(|i| { let start = index.blocks[if force_kf { i * kf_per_frag } else { index.keyframes[i * kf_per_frag] }] .pts as f64 / 1000.; let end = if force_kf { let n = (i + 1) * kf_per_frag; if n >= index.blocks.len() { None } else { Some(n) } } else { index.keyframes.get((i + 1) * kf_per_frag).copied() } .map(|i| index.blocks[i].pts as f64 / 1000.) .unwrap_or(duration); start..end }) .collect()) } pub fn write_fragment_into( writer: impl Write, path: &Path, track: u64, webm: bool, title: &str, n: usize, ) -> anyhow::Result<()> { let meta = matroska_metadata(path)?; let duration = media_duration(&meta); let track_meta = meta .as_ref() .tracks .as_ref() .unwrap() .entries .iter() .find(|t| t.track_number == track) .unwrap(); let force_kf = track_meta.track_type == 17; info!("writing fragment {n} of {:?} (track {track})", title); let mut output = EbmlWriter::new(BufWriter::new(writer), 0); let mapped = 1; info!("\t- {track} {path:?} ({} => {mapped})", track); // info!("\t {}", info); let file = File::open(path).context("opening source file")?; let index = get_seek_index(path)?; let index = index .get(&track) .ok_or(anyhow!("track missing 2"))? .to_owned(); debug!("\t seek index: {} blocks loaded", index.blocks.len()); let mut reader = EbmlReader::new(BufReader::new(file)); let n_kf = if force_kf { index.blocks.len() } else { index.keyframes.len() }; debug!("{duration} {n_kf}"); let average_kf_interval = duration / n_kf as f64; let kf_per_frag = (FRAGMENT_LENGTH / average_kf_interval).ceil() as usize; debug!("average keyframe interval: {average_kf_interval}"); debug!(" => keyframes per frag {kf_per_frag}"); let (start_block_index, end_block_index) = if force_kf { (n * kf_per_frag, (n + 1) * kf_per_frag) } else { ( *index .keyframes .get(n * kf_per_frag) .ok_or(anyhow!("fragment index out of range"))?, *index .keyframes .get((n + 1) * kf_per_frag) .unwrap_or(&index.blocks.len()), ) }; debug!("writing blocks {start_block_index} to {end_block_index}."); let start_block = &index.blocks[start_block_index]; let last_block_pts = index .blocks .get(end_block_index) .map(|b| b.pts) .unwrap_or((duration * 1000.) as u64); output.write_tag(&ebml_header(webm))?; output.write_tag(&MatroskaTag::Segment(Master::Start))?; output.write_tag(&ebml_segment_info( title.to_string(), (last_block_pts - start_block.pts) as f64 / 1000., ))?; output.write_tag(&MatroskaTag::Tracks(Master::Collected(vec![ ebml_track_entry(mapped, track_meta), ])))?; reader.seek(start_block.source_off, MatroskaTag::Cluster(Master::Start))?; let mut reader = SegmentExtractIter::new(&mut reader, track); { // TODO this one caused fragments to get dropped by MSE for no reason // for i in start_block_index..end_block_index { // let index_block = &index.blocks[i]; // let (mut block, duration) = reader.next()?; // assert_eq!(index_block.size, block.data.len(), "seek index is wrong"); // block.track = 1; // block.timestamp_off = 0; // output.write_tag(&MatroskaTag::Cluster(Master::Collected(vec![ // MatroskaTag::Timestamp(index_block.pts - start_block.pts), // if let Some(duration) = duration { // MatroskaTag::BlockGroup(Master::Collected(vec![ // MatroskaTag::BlockDuration(duration), // MatroskaTag::Block(block), // ])) // } else { // MatroskaTag::SimpleBlock(block) // }, // ])))?; // } } { let mut blocks = vec![MatroskaTag::Timestamp(start_block.pts)]; for i in start_block_index..end_block_index { let index_block = &index.blocks[i]; let (mut block, duration) = reader.next_block()?; assert_eq!(index_block.size, block.data.len(), "seek index is wrong"); block.track = 1; // TODO this does generate overflows sometimes block.timestamp_off = (index_block.pts as i64 - start_block.pts as i64) .try_into() .unwrap(); if let Some(duration) = duration { blocks.push(MatroskaTag::BlockGroup(Master::Collected(vec![ MatroskaTag::BlockDuration(duration), MatroskaTag::Block(block), ]))) } else { blocks.push(MatroskaTag::SimpleBlock(block)) } } output.write_tag(&MatroskaTag::Cluster(Master::Collected(blocks)))?; } debug!("wrote {} bytes", output.position()); Ok(()) } fn media_duration(m: &MatroskaMetadata) -> f64 { let info = m.info.as_ref().unwrap(); (info.duration.unwrap_or_default() * info.timestamp_scale as f64) / 1_000_000_000. }