1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
/*
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>
*/
#![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<Option<Config>> = Mutex::new(None);
static CONF: LazyLock<Config> = LazyLock::new(|| {
CONF_PRELOAD
.lock()
.unwrap()
.take()
.expect("stream config not preloaded. logic error")
});
#[derive(Debug)]
pub struct SMediaInfo {
pub title: Option<String>,
pub files: BTreeSet<PathBuf>,
}
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<SMediaInfo>,
spec: StreamSpec,
range: Range<u64>,
) -> Result<Box<dyn Read + Send + Sync>> {
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<SMediaInfo>,
track: usize,
range: Range<u64>,
) -> Result<Box<dyn Read+ Send + Sync>> {
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)))
}
|