/* 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::{ cues::{generate_cues, GeneratedCue}, stream_info, SMediaInfo, }; use anyhow::{anyhow, Result}; use jellyremuxer::{ demuxers::create_demuxer_autodetect, matroska::{self, Segment}, muxers::write_fragment, ContainerFormat, }; use jellystream_types::{FormatNum, IndexNum, StreamContainer, TrackNum}; use jellytranscoder::fragment::transcode; use std::{ fs::File, io::{Cursor, Read}, sync::Arc, }; pub fn fragment_stream( info: Arc, track: TrackNum, index: IndexNum, format_num: FormatNum, container: StreamContainer, ) -> Result> { let (iinfo, info) = stream_info(info)?; let (file_index, track_num) = *iinfo .track_to_file .get(track) .ok_or(anyhow!("track not found"))?; let media_path = iinfo.paths[file_index].clone(); let track = info.tracks.get(track).ok_or(anyhow!("track not found"))?; let format = track .formats .get(format_num) .ok_or(anyhow!("format not found"))?; let mk_track = iinfo.metadata[file_index] .tracks .as_ref() .unwrap() .entries .iter() .find(|t| t.track_number == track_num) .unwrap(); let timestamp_scale = iinfo.metadata[file_index].info.timestamp_scale; let total_duration = iinfo.metadata[file_index].info.duration; let cue_stat = generate_cues(&media_path)?; let start_cue = cue_stat .cues .get(index) .ok_or(anyhow!("fragment index out of range"))?; let end_cue = cue_stat .cues .get(index + 1) .copied() .unwrap_or(GeneratedCue { position: 0, time: total_duration.unwrap_or_default() as u64 * timestamp_scale, // TODO rounding? }); let cluster_offset = start_cue.position; let duration = (end_cue.time - start_cue.time) as f64 / timestamp_scale as f64; let mk_info = matroska::Info { duration: Some(duration), timestamp_scale, ..Default::default() }; let mk_tracks = matroska::Tracks { entries: vec![mk_track.to_owned()], }; let mut 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 }; cluster.simple_blocks.retain(|b| b.track == track_num); cluster.block_groups.retain(|b| b.block.track == track_num); let jr_container = match container { StreamContainer::WebM => ContainerFormat::Webm, StreamContainer::Matroska => ContainerFormat::Matroska, StreamContainer::WebVTT => todo!(), StreamContainer::MPEG4 => ContainerFormat::Mpeg4, StreamContainer::JVTT => todo!(), }; let mut segment = Segment { info: mk_info, tracks: Some(mk_tracks), clusters: vec![cluster], ..Default::default() }; segment.info.writing_app = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")).to_string(); if !format.remux { segment = transcode( track.kind, &format!("{media_path:?} {track_num} {index}"), format, segment, )?; } let mut out = Vec::new(); write_fragment(jr_container, &mut out, segment)?; Ok(Box::new(Cursor::new(out))) }