aboutsummaryrefslogtreecommitdiff
path: root/stream/src/lib.rs
blob: 60c283c278c5e1b52c2704d37acc9f9bac417ea6 (plain)
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)))
}