/* 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) 2023 metamuffin */ use crate::{ ebml_header, ebml_segment_info, ebml_track_entry, seek_index::get_seek_index, segment_extractor::SegmentExtractIter, }; use anyhow::{anyhow, Context, Result}; use jellycommon::{LocalTrack, NodePublic}; use jellymatroska::{read::EbmlReader, write::EbmlWriter, Master, MatroskaTag}; use log::{debug, info}; use std::{fs::File, io::Write, ops::Range, path::Path}; const SNIPPET_LENGTH: f64 = 2.; pub fn snippet_index( path_base: &Path, item: &NodePublic, track_sources: &Vec, track: usize, ) -> Result>> { let media_info = item.media.as_ref().unwrap(); let private = &track_sources[track]; let source_path = path_base.join(&private.path); let index = get_seek_index(&source_path)?; let index = index .get(&(private.track as u64)) .ok_or(anyhow!("track missing"))?; let average_kf_interval = media_info.duration / index.keyframes.len() as f64; let kf_per_snip = (SNIPPET_LENGTH / average_kf_interval).ceil() as usize; debug!("average keyframe interval: {average_kf_interval}"); debug!(" => keyframes per snippet {kf_per_snip}"); let n_snips = index.keyframes.len().div_ceil(kf_per_snip); Ok((0..n_snips) .map(|i| { let start = index.blocks[index.keyframes[i * kf_per_snip]].pts as f64 / 1000.; let end = index .keyframes .get((i + 1) * kf_per_snip) .map(|i| index.blocks[*i].pts as f64 / 1000.) .unwrap_or(media_info.duration); start..end }) .collect()) } pub fn write_snippet_into( writer: impl Write, path_base: &Path, item: &NodePublic, track_sources: Vec, track: usize, webm: bool, n: usize, ) -> anyhow::Result<()> { info!("writing snippet {n} of {:?} (track #{track})", item.title); let mut output = EbmlWriter::new(writer, 0); let media_info = item.media.as_ref().unwrap(); let info = media_info .tracks .get(track) .ok_or(anyhow!("track not available"))? .to_owned(); let private = &track_sources[track]; let source_path = path_base.join(&private.path); let mapped = 1; info!( "\t- {track} {source_path:?} ({} => {mapped})", private.track ); info!("\t {}", info); let file = File::open(&source_path).context("opening source file")?; let index = get_seek_index(&source_path)?; let index = index .get(&(private.track as u64)) .ok_or(anyhow!("track missing"))? .to_owned(); debug!("\t seek index: {} blocks loaded", index.blocks.len()); let mut reader = EbmlReader::new(file); // TODO maybe refactor this to approx. contant time per snippet let average_kf_interval = media_info.duration / index.keyframes.len() as f64; let kf_per_snip = (SNIPPET_LENGTH / average_kf_interval).ceil() as usize; debug!("average keyframe interval: {average_kf_interval}"); debug!(" => keyframes per snippet {kf_per_snip}"); let start_block_index = *index .keyframes .get(n * kf_per_snip) .ok_or(anyhow!("snippet index out of range"))?; let end_block_index = *index .keyframes .get((n + 1) * kf_per_snip) .unwrap_or(&index.blocks.len()); let start_block = &index.blocks[start_block_index]; let last_block = &index.blocks[end_block_index - 1]; reader.seek(start_block.source_off, MatroskaTag::Cluster(Master::Start))?; output.write_tag(&ebml_header(webm))?; output.write_tag(&MatroskaTag::Segment(Master::Start))?; output.write_tag(&ebml_segment_info( format!("{} (track {track}; snippet {n})", item.title), (last_block.pts - start_block.pts) as f64 / 1000., ))?; output.write_tag(&MatroskaTag::Tags(Master::Collected(vec![])))?; output.write_tag(&MatroskaTag::Tracks(Master::Collected(vec![ ebml_track_entry(mapped, &info, private.codec_private.clone()), ])))?; let mut reader = SegmentExtractIter::new(&mut reader, private.track as u64); let mut blocks = vec![MatroskaTag::Timestamp(0)]; for i in start_block_index..end_block_index { let index_block = &index.blocks[i]; let mut block = reader.next()?; assert_eq!(index_block.size, block.data.len(), "seek index is wrong"); block.track = 1; block.timestamp_off = (index_block.pts - start_block.pts).try_into().unwrap(); blocks.push(MatroskaTag::SimpleBlock(block.dump())) } output.write_tag(&MatroskaTag::Cluster(Master::Collected(blocks)))?; output.write_tag(&MatroskaTag::Segment(Master::End))?; Ok(()) }