diff options
-rw-r--r-- | common/src/config.rs | 61 | ||||
-rw-r--r-- | common/src/jhls.rs | 27 | ||||
-rw-r--r-- | common/src/stream.rs | 3 | ||||
-rw-r--r-- | stream/src/fragment.rs | 99 | ||||
-rw-r--r-- | stream/src/lib.rs | 3 | ||||
-rw-r--r-- | transcoder/src/fragment.rs | 94 |
6 files changed, 124 insertions, 163 deletions
diff --git a/common/src/config.rs b/common/src/config.rs index d7682df..a0dc459 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{jhls::EncodingProfile, user::PermissionSet}; +use crate::user::PermissionSet; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; @@ -20,11 +20,30 @@ pub struct GlobalConfig { #[serde(default = "default::cache_path")] pub cache_path: PathBuf, #[serde(default = "default::media_path")] pub media_path: PathBuf, #[serde(default = "default::secrets_path")] pub secrets_path: PathBuf, - #[serde(default = "default::transcoding_profiles")] pub transcoding_profiles: Vec<EncodingProfile>, #[serde(default = "default::max_in_memory_cache_size")] pub max_in_memory_cache_size: usize, #[serde(default)] pub admin_username: Option<String>, #[serde(default = "default::login_expire")] pub login_expire: i64, #[serde(default)] pub default_permission_set: PermissionSet, + #[serde(default)] encoders: EncoderPreferences, +} + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct EncoderPreferences { + avc: Option<EncoderClass>, + hevc: Option<EncoderClass>, + vp8: Option<EncoderClass>, + vp9: Option<EncoderClass>, + av1: Option<EncoderClass>, +} + +#[derive(Debug, Deserialize, Serialize)] +enum EncoderClass { + Aom, + Svt, + X26n, + Vpx, + Vaapi, + Rkmpp, } #[derive(Serialize, Deserialize, Debug, Default)] @@ -59,7 +78,6 @@ pub struct ApiSecrets { } mod default { - use crate::jhls::EncodingProfile; use std::path::PathBuf; pub fn login_expire() -> i64 { @@ -83,43 +101,6 @@ mod default { pub fn max_in_memory_cache_size() -> usize { 50_000_000 } - pub fn transcoding_profiles() -> Vec<EncodingProfile> { - vec![ - EncodingProfile::Video { - codec: "libsvtav1".to_string(), - preset: Some(8), - bitrate: 2_000_000, - width: Some(1920), - }, - EncodingProfile::Video { - codec: "libsvtav1".to_string(), - preset: Some(8), - bitrate: 1_200_000, - width: Some(1280), - }, - EncodingProfile::Video { - codec: "libsvtav1".to_string(), - preset: Some(8), - bitrate: 300_000, - width: Some(640), - }, - EncodingProfile::Audio { - codec: "libopus".to_string(), - bitrate: 128_000, - sample_rate: None, - channels: Some(2), - }, - EncodingProfile::Audio { - codec: "libopus".to_string(), - bitrate: 64_000, - sample_rate: None, - channels: Some(2), - }, - EncodingProfile::Subtitles { - codec: "webvtt".to_string(), - }, - ] - } } fn return_true() -> bool { diff --git a/common/src/jhls.rs b/common/src/jhls.rs index 6dc976b..90f48f5 100644 --- a/common/src/jhls.rs +++ b/common/src/jhls.rs @@ -5,33 +5,6 @@ */ use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; -use std::ops::Range; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct JhlsTrackIndex { - pub extra_profiles: Vec<EncodingProfile>, - pub fragments: Vec<Range<f64>>, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum EncodingProfile { - Video { - codec: String, - preset: Option<u8>, - bitrate: usize, - width: Option<usize>, - }, - Audio { - codec: String, - bitrate: usize, - channels: Option<usize>, - sample_rate: Option<f64>, - }, - Subtitles { - codec: String, - }, -} #[derive(Debug, Serialize, Deserialize, Encode, Decode)] pub struct SubtitleCue { diff --git a/common/src/stream.rs b/common/src/stream.rs index 75349cc..a14fd57 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -91,7 +91,8 @@ pub struct StreamFormatInfo { pub remux: bool, pub containers: Vec<StreamContainer>, - pub pixel_count: Option<u64>, + pub width: Option<u64>, + pub height: Option<u64>, pub samplerate: Option<f64>, pub channels: Option<usize>, pub bit_depth: Option<u8>, diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index 52d32f4..b2e254b 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -6,9 +6,10 @@ use crate::{stream_info, SMediaInfo}; use anyhow::{anyhow, Result}; use jellybase::common::stream::StreamContainer; +use jellytranscoder::fragment::transcode; use log::warn; use std::sync::Arc; -use tokio::io::DuplexStream; +use tokio::{fs::File, io::DuplexStream}; use tokio_util::io::SyncIoBridge; pub async fn fragment_stream( @@ -17,7 +18,7 @@ pub async fn fragment_stream( track: usize, segment: usize, index: usize, - format: usize, + format_num: usize, container: StreamContainer, ) -> Result<()> { let (iinfo, info) = stream_info(info).await?; @@ -26,52 +27,56 @@ pub async fn fragment_stream( .get(track) .ok_or(anyhow!("track not found"))?; let path = iinfo.paths[file_index].clone(); + let seg = info + .segments + .get(segment) + .ok_or(anyhow!("segment not found"))?; + let track = seg.tracks.get(track).ok_or(anyhow!("track not found"))?; + let format = track + .formats + .get(format_num) + .ok_or(anyhow!("format not found"))?; - // if let Some(profile) = None { - // perms.assert(&UserPermission::Transcode)?; - // let location = transcode( - // &format!("{track} {index} {:?}", node), // TODO maybe not use the entire source - // CONF.transcoding_profiles - // .get(profile) - // .ok_or(anyhow!("profile out of range"))?, - // move |b| { - // tokio::task::spawn_blocking(move || { - // if let Err(err) = jellyremuxer::write_fragment_into( - // SyncIoBridge::new(b), - // &CONF.media_path, - // &node, - // &local_track, - // track as usize, - // false, - // index, - // ) { - // warn!("segment stream error: {err}"); - // } - // }); - // }, - // ) - // .await?; - // let mut output = File::open(location.abs()).await?; - // tokio::task::spawn(async move { - // if let Err(err) = tokio::io::copy(&mut output, &mut b).await { - // warn!("cannot write stream: {err}") - // } - // }); - // } else { - let b = SyncIoBridge::new(b); - tokio::task::spawn_blocking(move || { - if let Err(err) = jellyremuxer::write_fragment_into( - b, - &path, - track_num, - container == StreamContainer::WebM, - &info.name.unwrap_or_default(), - index, - ) { - warn!("segment stream error: {err}"); - } - }); - // } + if format.remux { + tokio::task::spawn_blocking(move || { + if let Err(err) = jellyremuxer::write_fragment_into( + SyncIoBridge::new(b), + &path, + track_num, + container == StreamContainer::WebM, + &info.name.unwrap_or_default(), + index, + ) { + warn!("segment stream error: {err}"); + } + }); + } else { + let location = transcode( + &format!("{path:?} {track_num} {index} {format_num} {container}"), // TODO maybe not use the entire source + format, + move |b| { + tokio::task::spawn_blocking(move || { + if let Err(err) = jellyremuxer::write_fragment_into( + SyncIoBridge::new(b), + &path, + track_num, + container == StreamContainer::WebM, + &info.name.unwrap_or_default(), + index, + ) { + warn!("segment stream error: {err}"); + } + }); + }, + ) + .await?; + let mut output = File::open(location.abs()).await?; + tokio::task::spawn(async move { + if let Err(err) = tokio::io::copy(&mut output, &mut b).await { + warn!("cannot write stream: {err}") + } + }); + } Ok(()) } diff --git a/stream/src/lib.rs b/stream/src/lib.rs index a6faf54..eb56529 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -136,7 +136,8 @@ async fn stream_info(info: Arc<SMediaInfo>) -> Result<(InternalStreamInfo, Strea bit_depth: t.audio.as_ref().and_then(|a| a.bit_depth.map(|e| e as u8)), samplerate: t.audio.as_ref().map(|a| a.sampling_frequency), channels: t.audio.as_ref().map(|a| a.channels as usize), - pixel_count: t.video.as_ref().map(|v| v.pixel_width * v.pixel_height), + width: t.video.as_ref().map(|v| v.pixel_width), + height: t.video.as_ref().map(|v| v.pixel_height), ..Default::default() }); tracks.push(StreamTrackInfo { diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index ff6a9db..b88339c 100644 --- a/transcoder/src/fragment.rs +++ b/transcoder/src/fragment.rs @@ -7,7 +7,7 @@ use crate::LOCAL_VIDEO_TRANSCODING_TASKS; use jellybase::{ cache::{async_cache_file, CachePath}, - common::jhls::EncodingProfile, + common::stream::StreamFormatInfo, }; use log::{debug, info}; use std::process::Stdio; @@ -21,7 +21,7 @@ use tokio::{ pub async fn transcode( key: &str, - enc: &EncodingProfile, + enc: &StreamFormatInfo, input: impl FnOnce(ChildStdin), ) -> anyhow::Result<CachePath> { async_cache_file( @@ -30,51 +30,51 @@ pub async fn transcode( let _permit = LOCAL_VIDEO_TRANSCODING_TASKS.acquire().await?; debug!("transcoding fragment with {enc:?}"); - let mut args = Vec::new(); - match enc { - EncodingProfile::Video { - codec, - preset, - bitrate, - width, - } => { - if let Some(width) = width { - args.push("-vf".to_string()); - args.push(format!("scale={width}:-1")); - } - args.push("-c:v".to_string()); - args.push(codec.to_string()); - if let Some(preset) = preset { - args.push("-preset".to_string()); - args.push(format!("{preset}")); - } - args.push("-b:v".to_string()); - args.push(format!("{bitrate}")); - } - EncodingProfile::Audio { - codec, - bitrate, - sample_rate, - channels, - } => { - if let Some(channels) = channels { - args.push("-ac".to_string()); - args.push(format!("{channels}")) - } - if let Some(sample_rate) = sample_rate { - args.push("-ar".to_string()); - args.push(format!("{sample_rate}")) - } - args.push("-c:a".to_string()); - args.push(codec.to_string()); - args.push("-b:a".to_string()); - args.push(format!("{bitrate}")); - } - EncodingProfile::Subtitles { codec } => { - args.push("-c:s".to_string()); - args.push(codec.to_string()); - } - }; + let mut args = Vec::<String>::new(); + // match enc { + // EncodingProfile::Video { + // codec, + // preset, + // bitrate, + // width, + // } => { + // if let Some(width) = width { + // args.push("-vf".to_string()); + // args.push(format!("scale={width}:-1")); + // } + // args.push("-c:v".to_string()); + // args.push(codec.to_string()); + // if let Some(preset) = preset { + // args.push("-preset".to_string()); + // args.push(format!("{preset}")); + // } + // args.push("-b:v".to_string()); + // args.push(format!("{bitrate}")); + // } + // EncodingProfile::Audio { + // codec, + // bitrate, + // sample_rate, + // channels, + // } => { + // if let Some(channels) = channels { + // args.push("-ac".to_string()); + // args.push(format!("{channels}")) + // } + // if let Some(sample_rate) = sample_rate { + // args.push("-ar".to_string()); + // args.push(format!("{sample_rate}")) + // } + // args.push("-c:a".to_string()); + // args.push(codec.to_string()); + // args.push("-b:a".to_string()); + // args.push(format!("{bitrate}")); + // } + // EncodingProfile::Subtitles { codec } => { + // args.push("-c:s".to_string()); + // args.push(codec.to_string()); + // } + // }; info!("encoding with {:?}", args.join(" ")); let mut proc = Command::new("ffmpeg") |