/* 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 */ #![feature(iterator_try_collect)] pub mod cues; mod fragment; mod fragment_index; mod hls; pub mod metadata; mod stream_info; mod webvtt; use anyhow::{anyhow, bail, Context, Result}; use fragment::fragment_stream; use fragment_index::fragment_index_stream; use hls::{hls_multivariant_stream, hls_variant_stream}; use jellystream_types::{StreamContainer, StreamSpec}; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeSet, fs::File, io::{Read, Seek, SeekFrom}, ops::Range, path::PathBuf, sync::{Arc, LazyLock, Mutex}, }; use stream_info::{stream_info, write_stream_info}; #[rustfmt::skip] #[derive(Debug, Deserialize, Serialize, Default)] pub struct Config { #[serde(default)] pub offer_avc: bool, #[serde(default)] pub offer_hevc: bool, #[serde(default)] pub offer_vp8: bool, #[serde(default)] pub offer_vp9: bool, #[serde(default)] pub offer_av1: bool, } pub static CONF_PRELOAD: Mutex> = Mutex::new(None); static CONF: LazyLock = LazyLock::new(|| { CONF_PRELOAD .lock() .unwrap() .take() .expect("stream config not preloaded. logic error") }); #[derive(Debug)] pub struct SMediaInfo { pub title: Option, pub files: BTreeSet, } pub struct StreamHead { pub content_type: &'static str, pub range_supported: bool, } pub fn stream_head(spec: &StreamSpec) -> StreamHead { use StreamContainer::*; use StreamSpec::*; let container_ct = |x: StreamContainer| match x { WebM => "video/webm", Matroska => "video/x-matroska", WebVTT => "text/vtt", JVTT => "application/jellything-vtt+json", MPEG4 => "video/mp4", }; let range_supported = matches!(spec, Remux { .. } | Original { .. }); let content_type = match spec { Original { .. } => "video/x-matroska", HlsMultiVariant { .. } => "application/vnd.apple.mpegurl", HlsVariant { .. } => "application/vnd.apple.mpegurl", Info { .. } => "application/jellything-stream-info+json", FragmentIndex { .. } => "application/jellything-frag-index+json", Fragment { container, .. } => container_ct(*container), Remux { container, .. } => container_ct(*container), }; StreamHead { content_type, range_supported, } } pub fn stream( info: Arc, spec: StreamSpec, range: Range, ) -> Result> { match spec { StreamSpec::Original { track } => original_stream(info, track, range), StreamSpec::HlsMultiVariant { container } => hls_multivariant_stream(info, container), StreamSpec::HlsVariant { track, container, format, } => hls_variant_stream(info, track, format, container), StreamSpec::Info => write_stream_info(info), StreamSpec::FragmentIndex { track } => fragment_index_stream(info, track), StreamSpec::Fragment { track, index, container, format, } => fragment_stream(info, track, index, format, container), _ => bail!("todo"), } } fn original_stream( info: Arc, track: usize, range: Range, ) -> Result> { let (iinfo, _info) = stream_info(info)?; let (file_index, _) = *iinfo .track_to_file .get(track) .ok_or(anyhow!("unknown track"))?; let mut file = File::open(&iinfo.paths[file_index]).context("opening source")?; file.seek(SeekFrom::Start(range.start as u64)) .context("seek source")?; Ok(Box::new(file.take(range.end - range.start))) }