From 80343d02e9e29e4bc55d790b491ce0d0c7bff201 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 3 Mar 2025 16:58:59 +0100 Subject: a --- transcoder/src/fragment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'transcoder') diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index 8822fa2..ff6a9db 100644 --- a/transcoder/src/fragment.rs +++ b/transcoder/src/fragment.rs @@ -17,7 +17,7 @@ use tokio::{ }; // TODO odd video resolutions can cause errors when transcoding to YUV42{0,2} -// TODO with an implementation that cant handle it (SVT-AV1 such an impl). +// TODO with an implementation that cant handle it (SVT-AV1 is such an impl). pub async fn transcode( key: &str, -- cgit v1.2.3-70-g09d2 From 4a36d9e96853bf04d17f8377a7fbf862d108b9f1 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 14 Apr 2025 14:33:52 +0200 Subject: start transcoding refactor --- common/src/config.rs | 61 ++++++++++------------------ common/src/jhls.rs | 27 ------------- common/src/stream.rs | 3 +- stream/src/fragment.rs | 99 ++++++++++++++++++++++++---------------------- stream/src/lib.rs | 3 +- transcoder/src/fragment.rs | 94 +++++++++++++++++++++---------------------- 6 files changed, 124 insertions(+), 163 deletions(-) (limited to 'transcoder') 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 */ -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, #[serde(default = "default::max_in_memory_cache_size")] pub max_in_memory_cache_size: usize, #[serde(default)] pub admin_username: Option, #[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, + hevc: Option, + vp8: Option, + vp9: Option, + av1: Option, +} + +#[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 { - 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, - pub fragments: Vec>, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum EncodingProfile { - Video { - codec: String, - preset: Option, - bitrate: usize, - width: Option, - }, - Audio { - codec: String, - bitrate: usize, - channels: Option, - sample_rate: Option, - }, - 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, - pub pixel_count: Option, + pub width: Option, + pub height: Option, pub samplerate: Option, pub channels: Option, pub bit_depth: Option, 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) -> 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 { 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::::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") -- cgit v1.2.3-70-g09d2 From 42e08750a5a9a112d458a5db1d6b169278e953c5 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 14 Apr 2025 16:02:42 +0200 Subject: stream info for transcoding --- common/src/config.rs | 71 ++++++------- common/src/stream.rs | 5 +- import/src/infojson.rs | 2 +- server/src/routes/compat/jellyfin/mod.rs | 18 ++-- stream/src/fragment.rs | 1 + stream/src/lib.rs | 91 +---------------- stream/src/stream_info.rs | 164 +++++++++++++++++++++++++++++++ stream/src/webvtt.rs | 18 ++-- transcoder/src/fragment.rs | 74 ++++---------- 9 files changed, 239 insertions(+), 205 deletions(-) create mode 100644 stream/src/stream_info.rs (limited to 'transcoder') diff --git a/common/src/config.rs b/common/src/config.rs index a0dc459..3a48fea 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -8,36 +8,42 @@ use crate::user::PermissionSet; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; -#[rustfmt::skip] #[derive(Debug, Deserialize, Serialize, Default)] pub struct GlobalConfig { pub hostname: String, pub brand: String, pub slogan: String, - #[serde(default = "return_true" )] pub tls: bool, - #[serde(default = "default::asset_path")] pub asset_path: PathBuf, - #[serde(default = "default::database_path")] pub database_path: PathBuf, - #[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::max_in_memory_cache_size")] pub max_in_memory_cache_size: usize, - #[serde(default)] pub admin_username: Option, - #[serde(default = "default::login_expire")] pub login_expire: i64, - #[serde(default)] pub default_permission_set: PermissionSet, - #[serde(default)] encoders: EncoderPreferences, + #[serde(default = "return_true")] + pub tls: bool, + pub asset_path: PathBuf, + pub database_path: PathBuf, + pub cache_path: PathBuf, + pub media_path: PathBuf, + pub secrets_path: PathBuf, + #[serde(default = "max_in_memory_cache_size")] + pub max_in_memory_cache_size: usize, + #[serde(default)] + pub admin_username: Option, + #[serde(default = "login_expire")] + pub login_expire: i64, + #[serde(default)] + pub default_permission_set: PermissionSet, + #[serde(default)] + pub encoders: EncoderPreferences, } #[derive(Debug, Deserialize, Serialize, Default)] pub struct EncoderPreferences { - avc: Option, - hevc: Option, - vp8: Option, - vp9: Option, - av1: Option, + pub avc: Option, + pub hevc: Option, + pub vp8: Option, + pub vp9: Option, + pub av1: Option, } #[derive(Debug, Deserialize, Serialize)] -enum EncoderClass { +#[serde(rename_all = "snake_case")] +pub enum EncoderClass { Aom, Svt, X26n, @@ -77,30 +83,11 @@ pub struct ApiSecrets { pub trakt: Option, } -mod default { - use std::path::PathBuf; - - pub fn login_expire() -> i64 { - 60 * 60 * 24 - } - pub fn asset_path() -> PathBuf { - "data/assets".into() - } - pub fn database_path() -> PathBuf { - "data/database".into() - } - pub fn cache_path() -> PathBuf { - "data/cache".into() - } - pub fn media_path() -> PathBuf { - "data/media".into() - } - pub fn secrets_path() -> PathBuf { - "data/secrets.yaml".into() - } - pub fn max_in_memory_cache_size() -> usize { - 50_000_000 - } +fn login_expire() -> i64 { + 60 * 60 * 24 +} +fn max_in_memory_cache_size() -> usize { + 50_000_000 } fn return_true() -> bool { diff --git a/common/src/stream.rs b/common/src/stream.rs index a14fd57..555a5d0 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -87,7 +87,7 @@ pub enum TrackKind { #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct StreamFormatInfo { pub codec: String, - pub byterate: f64, + pub bitrate: f64, pub remux: bool, pub containers: Vec, @@ -104,6 +104,7 @@ pub enum StreamContainer { WebM, Matroska, WebVTT, + MPEG4, JVTT, } @@ -203,6 +204,7 @@ impl Display for StreamContainer { StreamContainer::Matroska => "matroska", StreamContainer::WebVTT => "webvtt", StreamContainer::JVTT => "jvtt", + StreamContainer::MPEG4 => "mp4", }) } } @@ -214,6 +216,7 @@ impl FromStr for StreamContainer { "matroska" => StreamContainer::Matroska, "webvtt" => StreamContainer::WebVTT, "jvtt" => StreamContainer::JVTT, + "mp4" => StreamContainer::MPEG4, _ => return Err(()), }) } diff --git a/import/src/infojson.rs b/import/src/infojson.rs index 3a8d76e..1efbae9 100644 --- a/import/src/infojson.rs +++ b/import/src/infojson.rs @@ -86,7 +86,7 @@ pub struct YFormat { pub fps: Option, pub columns: Option, pub fragments: Option>, - pub resolution: String, + pub resolution: Option, pub dynamic_range: Option, pub aspect_ratio: Option, pub http_headers: HashMap, diff --git a/server/src/routes/compat/jellyfin/mod.rs b/server/src/routes/compat/jellyfin/mod.rs index 6066760..7393c5f 100644 --- a/server/src/routes/compat/jellyfin/mod.rs +++ b/server/src/routes/compat/jellyfin/mod.rs @@ -5,18 +5,14 @@ */ pub mod models; -use crate::routes::{ - stream::rocket_uri_macro_r_stream, - ui::{ - account::{login_logic, session::Session}, - assets::{ - rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop, - rocket_uri_macro_r_item_poster, - }, - error::MyResult, - node::{aspect_class, DatabaseNodeUserDataExt}, - sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty}, +use crate::routes::ui::{ + account::{login_logic, session::Session}, + assets::{ + rocket_uri_macro_r_asset, rocket_uri_macro_r_item_backdrop, rocket_uri_macro_r_item_poster, }, + error::MyResult, + node::{aspect_class, DatabaseNodeUserDataExt}, + sort::{filter_and_sort_nodes, FilterProperty, NodeFilterSort, SortOrder, SortProperty}, }; use anyhow::{anyhow, Context}; use jellybase::{database::Database, CONF}; diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index b2e254b..e0644aa 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -54,6 +54,7 @@ pub async fn fragment_stream( let location = transcode( &format!("{path:?} {track_num} {index} {format_num} {container}"), // TODO maybe not use the entire source format, + container, move |b| { tokio::task::spawn_blocking(move || { if let Err(err) = jellyremuxer::write_fragment_into( diff --git a/stream/src/lib.rs b/stream/src/lib.rs index eb56529..18ad2a7 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -7,6 +7,7 @@ pub mod fragment; pub mod fragment_index; pub mod hls; +pub mod stream_info; pub mod webvtt; use anyhow::{anyhow, Context, Result}; @@ -14,18 +15,14 @@ use fragment::fragment_stream; use fragment_index::fragment_index_stream; use hls::{hls_master_stream, hls_variant_stream}; use jellybase::common::{ - stream::{ - StreamContainer, StreamFormatInfo, StreamInfo, StreamSegmentInfo, StreamSpec, - StreamTrackInfo, TrackKind, - }, + stream::{StreamContainer, StreamSpec}, Node, }; -use jellyremuxer::metadata::{matroska_metadata, MatroskaMetadata}; use std::{collections::BTreeSet, io::SeekFrom, ops::Range, path::PathBuf, sync::Arc}; +use stream_info::{stream_info, write_stream_info}; use tokio::{ fs::File, io::{duplex, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, DuplexStream}, - task::spawn_blocking, }; use tokio_util::io::SyncIoBridge; @@ -50,6 +47,7 @@ pub fn stream_head(spec: &StreamSpec) -> StreamHead { StreamContainer::Matroska => "video/x-matroska", StreamContainer::WebVTT => "text/vtt", StreamContainer::JVTT => "application/jellything-vtt+json", + StreamContainer::MPEG4 => "video/mp4", }; match spec { StreamSpec::Whep { .. } => cons("application/x-todo", false), @@ -103,87 +101,6 @@ pub async fn stream( Ok(a) } -async fn async_matroska_metadata(path: PathBuf) -> Result> { - Ok(spawn_blocking(move || matroska_metadata(&path)).await??) -} - -pub(crate) struct InternalStreamInfo { - pub paths: Vec, - pub metadata: Vec>, - pub track_to_file: Vec<(usize, u64)>, -} - -async fn stream_info(info: Arc) -> Result<(InternalStreamInfo, StreamInfo)> { - let mut metadata = Vec::new(); - let mut paths = Vec::new(); - for path in &info.files { - metadata.push(async_matroska_metadata(path.clone()).await?); - paths.push(path.clone()); - } - - let mut tracks = Vec::new(); - let mut track_to_file = Vec::new(); - - for (i, m) in metadata.iter().enumerate() { - if let Some(t) = &m.tracks { - for t in &t.entries { - let mut formats = Vec::new(); - formats.push(StreamFormatInfo { - codec: t.codec_id.to_string(), - remux: true, - byterate: 10., // TODO - containers: [StreamContainer::Matroska].to_vec(), - 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), - 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 { - name: None, - kind: match t.track_type { - 1 => TrackKind::Video, - 2 => TrackKind::Audio, - 17 => TrackKind::Subtitle, - _ => todo!(), - }, - formats, - }); - track_to_file.push((i, t.track_number)); - } - } - } - - let segment = StreamSegmentInfo { - name: None, - duration: metadata[0] - .info - .as_ref() - .unwrap() - .duration - .unwrap_or_default(), - tracks, - }; - Ok(( - InternalStreamInfo { - metadata, - paths, - track_to_file, - }, - StreamInfo { - name: info.info.title.clone(), - segments: vec![segment], - }, - )) -} - -async fn write_stream_info(info: Arc, mut b: DuplexStream) -> Result<()> { - let (_, info) = stream_info(info).await?; - b.write_all(&serde_json::to_vec(&info)?).await?; - Ok(()) -} - async fn remux_stream( node: Arc, spec: StreamSpec, diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs new file mode 100644 index 0000000..9d3d741 --- /dev/null +++ b/stream/src/stream_info.rs @@ -0,0 +1,164 @@ +use anyhow::Result; +use ebml_struct::matroska::TrackEntry; +use jellybase::{ + common::stream::{ + StreamContainer, StreamFormatInfo, StreamInfo, StreamSegmentInfo, StreamTrackInfo, + TrackKind, + }, + CONF, +}; +use jellyremuxer::metadata::{matroska_metadata, MatroskaMetadata}; +use std::{path::PathBuf, sync::Arc}; +use tokio::{ + io::{AsyncWriteExt, DuplexStream}, + spawn, + task::spawn_blocking, +}; + +use crate::SMediaInfo; + +async fn async_matroska_metadata(path: PathBuf) -> Result> { + Ok(spawn_blocking(move || matroska_metadata(&path)).await??) +} + +pub(crate) struct InternalStreamInfo { + pub paths: Vec, + pub metadata: Vec>, + pub track_to_file: Vec<(usize, u64)>, +} + +pub(crate) async fn stream_info(info: Arc) -> Result<(InternalStreamInfo, StreamInfo)> { + let mut metadata = Vec::new(); + let mut paths = Vec::new(); + for path in &info.files { + metadata.push(async_matroska_metadata(path.clone()).await?); + paths.push(path.clone()); + } + let mut tracks = Vec::new(); + let mut track_to_file = Vec::new(); + + for (i, m) in metadata.iter().enumerate() { + if let Some(t) = &m.tracks { + for t in &t.entries { + tracks.push(StreamTrackInfo { + name: None, + kind: match t.track_type { + 1 => TrackKind::Video, + 2 => TrackKind::Audio, + 17 => TrackKind::Subtitle, + _ => todo!(), + }, + formats: stream_formats(t), + }); + track_to_file.push((i, t.track_number)); + } + } + } + + let segment = StreamSegmentInfo { + name: None, + duration: metadata[0] + .info + .as_ref() + .unwrap() + .duration + .unwrap_or_default(), + tracks, + }; + Ok(( + InternalStreamInfo { + metadata, + paths, + track_to_file, + }, + StreamInfo { + name: info.info.title.clone(), + segments: vec![segment], + }, + )) +} + +fn stream_formats(t: &TrackEntry) -> Vec { + let mut formats = Vec::new(); + formats.push(StreamFormatInfo { + codec: t.codec_id.to_string(), + remux: true, + bitrate: 2_000_000., // TODO + containers: containers_by_codec(&t.codec_id), + 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), + width: t.video.as_ref().map(|v| v.pixel_width), + height: t.video.as_ref().map(|v| v.pixel_height), + ..Default::default() + }); + + match t.track_type { + 1 => { + let sw = t.video.as_ref().unwrap().pixel_width; + let sh = t.video.as_ref().unwrap().pixel_height; + for (w, br) in [(3840, 8e6), (1920, 5e6), (1280, 3e6), (640, 1e6)] { + if w > sw { + continue; + } + let h = (w * sh) / sw; + for (cid, enable) in [ + ("V_AV1", CONF.encoders.av1.is_some()), + ("V_VP8", CONF.encoders.vp8.is_some()), + ("V_VP9", CONF.encoders.vp9.is_some()), + ("V_AVC", CONF.encoders.avc.is_some()), + ("V_HEVC", CONF.encoders.hevc.is_some()), + ] { + if enable { + formats.push(StreamFormatInfo { + codec: cid.to_string(), + bitrate: br, + remux: false, + containers: containers_by_codec(cid), + width: Some(w), + height: Some(h), + samplerate: None, + channels: None, + bit_depth: None, + }); + } + } + } + } + 2 => { + for br in [256e3, 128e3, 64e3] { + formats.push(StreamFormatInfo { + codec: "A_OPUS".to_string(), + bitrate: br, + remux: false, + containers: containers_by_codec("A_OPUS"), + width: None, + height: None, + samplerate: Some(48e3), + channels: Some(2), + bit_depth: Some(32), + }); + } + } + 17 => {} + _ => {} + } + + formats +} + +fn containers_by_codec(codec: &str) -> Vec { + use StreamContainer::*; + match codec { + "V_VP8" | "V_VP9" | "V_AV1" | "A_OPUS" | "A_VORBIS" => vec![Matroska, WebM], + "V_AVC" | "A_AAC" => vec![Matroska, MPEG4], + "S_TEXT/UTF8" | "S_TEXT/WEBVTT" => vec![Matroska, WebVTT, WebM, JVTT], + _ => vec![Matroska], + } +} + +pub(crate) async fn write_stream_info(info: Arc, mut b: DuplexStream) -> Result<()> { + let (_, info) = stream_info(info).await?; + spawn(async move { b.write_all(&serde_json::to_vec(&info)?).await }); + Ok(()) +} diff --git a/stream/src/webvtt.rs b/stream/src/webvtt.rs index 960849c..e9f0181 100644 --- a/stream/src/webvtt.rs +++ b/stream/src/webvtt.rs @@ -3,23 +3,21 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use anyhow::{anyhow, Context, Result}; -use jellybase::{ - cache::async_cache_memory, - common::{stream::StreamSpec, Node}, - CONF, -}; -use jellyremuxer::extract::extract_track; -use jellytranscoder::subtitles::{parse_subtitles, write_webvtt}; +use anyhow::Result; +use jellybase::common::{stream::StreamSpec, Node}; use std::sync::Arc; -use tokio::io::{AsyncWriteExt, DuplexStream}; +use tokio::io::DuplexStream; pub async fn vtt_stream( json: bool, node: Arc, spec: StreamSpec, - mut b: DuplexStream, + b: DuplexStream, ) -> Result<()> { + let _ = b; + let _ = spec; + let _ = node; + let _ = json; // TODO cache // TODO should use fragments too? big films take too long... diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index b88339c..3cb4c40 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::stream::StreamFormatInfo, + common::stream::{StreamContainer, StreamFormatInfo}, }; use log::{debug, info}; use std::process::Stdio; @@ -21,73 +21,41 @@ use tokio::{ pub async fn transcode( key: &str, - enc: &StreamFormatInfo, + format: &StreamFormatInfo, + container: StreamContainer, input: impl FnOnce(ChildStdin), ) -> anyhow::Result { async_cache_file( - &["frag-tc", key, &format!("{enc:?}")], + &["frag-tc", key, &format!("{format:?}")], move |mut output| async move { let _permit = LOCAL_VIDEO_TRANSCODING_TASKS.acquire().await?; - debug!("transcoding fragment with {enc:?}"); + debug!("transcoding fragment with {format:?}"); 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()); - // } - // }; + + match format.codec.as_str() { + "V_AVC" => {} + + _ => unreachable!(), + } + info!("encoding with {:?}", args.join(" ")); + let container = match container { + StreamContainer::WebM => "webm", + StreamContainer::Matroska => "matroska", + StreamContainer::WebVTT => "vtt", + StreamContainer::MPEG4 => "mp4", + StreamContainer::JVTT => unreachable!(), + }; + let mut proc = Command::new("ffmpeg") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .args(["-f", "matroska", "-i", "pipe:0"]) .args(args) - .args(["-f", "webm", "pipe:1"]) + .args(["-f", container, "pipe:1"]) .spawn()?; - // let mut proc = Command::new("cat") - // .stdin(Stdio::piped()) - // .stdout(Stdio::piped()) - // .spawn()?; let stdin = proc.stdin.take().unwrap(); let mut stdout = proc.stdout.take().unwrap(); -- cgit v1.2.3-70-g09d2 From 3b147cb1dfcbd5c7218e0accd5784d992d5ae21c Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 14 Apr 2025 18:42:16 +0200 Subject: things --- common/src/config.rs | 17 ++++++++-------- common/src/stream.rs | 2 +- server/src/routes/stream.rs | 4 ++-- stream/src/fragment.rs | 1 + stream/src/hls.rs | 26 ++++++++++++++++++++++++- stream/src/lib.rs | 38 +++++++----------------------------- stream/src/stream_info.rs | 6 +++--- transcoder/src/fragment.rs | 47 +++++++++++++++++++++++++++++++++++++-------- 8 files changed, 87 insertions(+), 54 deletions(-) (limited to 'transcoder') diff --git a/common/src/config.rs b/common/src/config.rs index 3a48fea..df16ef0 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -29,19 +29,20 @@ pub struct GlobalConfig { #[serde(default)] pub default_permission_set: PermissionSet, #[serde(default)] - pub encoders: EncoderPreferences, + pub encoders: EncoderArgs, } #[derive(Debug, Deserialize, Serialize, Default)] -pub struct EncoderPreferences { - pub avc: Option, - pub hevc: Option, - pub vp8: Option, - pub vp9: Option, - pub av1: Option, +pub struct EncoderArgs { + pub avc: Option, + pub hevc: Option, + pub vp8: Option, + pub vp9: Option, + pub av1: Option, + pub generic: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum EncoderClass { Aom, diff --git a/common/src/stream.rs b/common/src/stream.rs index 555a5d0..9fd7daf 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -76,7 +76,7 @@ pub struct StreamTrackInfo { pub formats: Vec, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum TrackKind { Video, diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index 8f97aec..0fbeb3a 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -42,8 +42,8 @@ pub async fn r_stream_head( #[get("/n//stream?")] pub async fn r_stream( - session: Session, - federation: &State, + _session: Session, + _federation: &State, db: &State, id: &str, range: Option, diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index e0644aa..26746fc 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -53,6 +53,7 @@ pub async fn fragment_stream( } else { let location = transcode( &format!("{path:?} {track_num} {index} {format_num} {container}"), // TODO maybe not use the entire source + track.kind, format, container, move |b| { diff --git a/stream/src/hls.rs b/stream/src/hls.rs index f06ac72..3dfbf01 100644 --- a/stream/src/hls.rs +++ b/stream/src/hls.rs @@ -15,7 +15,31 @@ use tokio::{ task::spawn_blocking, }; -pub async fn hls_master_stream( +pub async fn hls_supermultivariant_stream( + mut b: DuplexStream, + info: Arc, + container: StreamContainer, +) -> Result<()> { + let (_iinfo, info) = stream_info(info).await?; + let mut out = String::new(); + writeln!(out, "#EXTM3U")?; + writeln!(out, "#EXT-X-VERSION:4")?; + for (i, _seg) in info.segments.iter().enumerate() { + let uri = format!( + "stream{}", + StreamSpec::HlsMultiVariant { + segment: i, + container, + } + .to_query() + ); + writeln!(out, "{uri}")?; + } + tokio::spawn(async move { b.write_all(out.as_bytes()).await }); + Ok(()) +} + +pub async fn hls_multivariant_stream( mut b: DuplexStream, info: Arc, segment: SegmentNum, diff --git a/stream/src/lib.rs b/stream/src/lib.rs index 18ad2a7..4df87ae 100644 --- a/stream/src/lib.rs +++ b/stream/src/lib.rs @@ -10,10 +10,10 @@ pub mod hls; pub mod stream_info; pub mod webvtt; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use fragment::fragment_stream; use fragment_index::fragment_index_stream; -use hls::{hls_master_stream, hls_variant_stream}; +use hls::{hls_multivariant_stream, hls_supermultivariant_stream, hls_variant_stream}; use jellybase::common::{ stream::{StreamContainer, StreamSpec}, Node, @@ -24,7 +24,6 @@ use tokio::{ fs::File, io::{duplex, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, DuplexStream}, }; -use tokio_util::io::SyncIoBridge; #[derive(Debug)] pub struct SMediaInfo { @@ -71,13 +70,12 @@ pub async fn stream( let (a, b) = duplex(4096); match spec { - StreamSpec::Whep { track, seek } => todo!(), - StreamSpec::WhepControl { token } => todo!(), - StreamSpec::Remux { tracks, container } => todo!(), StreamSpec::Original { track } => original_stream(info, track, range, b).await?, - StreamSpec::HlsSuperMultiVariant { container } => todo!(), + StreamSpec::HlsSuperMultiVariant { container } => { + hls_supermultivariant_stream(b, info, container).await?; + } StreamSpec::HlsMultiVariant { segment, container } => { - hls_master_stream(b, info, segment, container).await? + hls_multivariant_stream(b, info, segment, container).await? } StreamSpec::HlsVariant { segment, @@ -96,34 +94,12 @@ pub async fn stream( container, format, } => fragment_stream(b, info, track, segment, index, format, container).await?, + _ => bail!("todo"), } Ok(a) } -async fn remux_stream( - node: Arc, - spec: StreamSpec, - range: Range, - b: DuplexStream, -) -> Result<()> { - let b = SyncIoBridge::new(b); - - // tokio::task::spawn_blocking(move || { - // jellyremuxer::remux_stream_into( - // b, - // range, - // CONF.media_path.to_owned(), - // &node, - // local_tracks, - // spec.track, - // spec.webm.unwrap_or(false), - // ) - // }); - - Ok(()) -} - async fn original_stream( info: Arc, track: usize, diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs index 9d3d741..a8b6989 100644 --- a/stream/src/stream_info.rs +++ b/stream/src/stream_info.rs @@ -23,7 +23,7 @@ async fn async_matroska_metadata(path: PathBuf) -> Result> pub(crate) struct InternalStreamInfo { pub paths: Vec, - pub metadata: Vec>, + pub _metadata: Vec>, pub track_to_file: Vec<(usize, u64)>, } @@ -67,7 +67,7 @@ pub(crate) async fn stream_info(info: Arc) -> Result<(InternalStream }; Ok(( InternalStreamInfo { - metadata, + _metadata: metadata, paths, track_to_file, }, @@ -83,7 +83,7 @@ fn stream_formats(t: &TrackEntry) -> Vec { formats.push(StreamFormatInfo { codec: t.codec_id.to_string(), remux: true, - bitrate: 2_000_000., // TODO + bitrate: 10_000_000., // TODO containers: containers_by_codec(&t.codec_id), 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), diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index 3cb4c40..1d06e9a 100644 --- a/transcoder/src/fragment.rs +++ b/transcoder/src/fragment.rs @@ -7,7 +7,8 @@ use crate::LOCAL_VIDEO_TRANSCODING_TASKS; use jellybase::{ cache::{async_cache_file, CachePath}, - common::stream::{StreamContainer, StreamFormatInfo}, + common::stream::{StreamContainer, StreamFormatInfo, TrackKind}, + CONF, }; use log::{debug, info}; use std::process::Stdio; @@ -21,6 +22,7 @@ use tokio::{ pub async fn transcode( key: &str, + kind: TrackKind, format: &StreamFormatInfo, container: StreamContainer, input: impl FnOnce(ChildStdin), @@ -31,15 +33,44 @@ pub async fn transcode( let _permit = LOCAL_VIDEO_TRANSCODING_TASKS.acquire().await?; debug!("transcoding fragment with {format:?}"); - let mut args = Vec::::new(); - - match format.codec.as_str() { - "V_AVC" => {} + let template = match format.codec.as_str() { + "V_AVC" => CONF.encoders.avc.as_ref(), + "V_HEVC" => CONF.encoders.hevc.as_ref(), + "V_VP8" => CONF.encoders.vp8.as_ref(), + "V_VP9" => CONF.encoders.vp9.as_ref(), + "V_AV1" => CONF.encoders.av1.as_ref(), + _ => None, + } + .or(CONF.encoders.generic.as_ref()) + .cloned() + .unwrap_or("ffmpeg %i %f %e %o".to_owned()); + let filter = match kind { + TrackKind::Video => format!("-vf scale={}:-1", format.width.unwrap()), + TrackKind::Audio => format!(""), + TrackKind::Subtitle => String::new(), + }; + let typechar = match kind { + TrackKind::Video => "v", + TrackKind::Audio => "a", + TrackKind::Subtitle => "s", + }; + let fallback_encoder = match format.codec.as_str() { + "A_OPUS" => "libopus", _ => unreachable!(), - } + }; + + let args = template + .replace("%i", "-f matroska -i pipe:0") + .replace("%o", "-f %C pipe:1") + .replace("%f", &filter) + .replace("%e", "-c:%t %c -b:%t %r") + .replace("%t", typechar) + .replace("%c", fallback_encoder) + .replace("%r", &(format.bitrate as i64).to_string()) + .replace("%C", &container.to_string()); - info!("encoding with {:?}", args.join(" ")); + info!("encoding with {:?}", args); let container = match container { StreamContainer::WebM => "webm", @@ -53,7 +84,7 @@ pub async fn transcode( .stdin(Stdio::piped()) .stdout(Stdio::piped()) .args(["-f", "matroska", "-i", "pipe:0"]) - .args(args) + .args(args.split(" ")) .args(["-f", container, "pipe:1"]) .spawn()?; -- cgit v1.2.3-70-g09d2 From 39dee6820db4581fa41cfac8bcfdd399a96f5319 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 16 Apr 2025 00:09:35 +0200 Subject: transcode impl but broken --- common/src/stream.rs | 4 ++-- remuxer/src/lib.rs | 3 +++ remuxer/src/mpeg4.rs | 34 ++++++++++++++++++++++++++++++++++ stream/src/fragment.rs | 32 ++++++++++++++++++++++++-------- stream/src/stream_info.rs | 13 +++++++++---- transcoder/src/fragment.rs | 31 +++++++++++-------------------- web/script/player/mediacaps.ts | 11 ++++++----- web/script/player/track/mse.ts | 20 ++++++++++++++++---- 8 files changed, 105 insertions(+), 43 deletions(-) create mode 100644 remuxer/src/mpeg4.rs (limited to 'transcoder') diff --git a/common/src/stream.rs b/common/src/stream.rs index ba91ff5..55f2f49 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -209,7 +209,7 @@ impl Display for StreamContainer { StreamContainer::Matroska => "matroska", StreamContainer::WebVTT => "webvtt", StreamContainer::JVTT => "jvtt", - StreamContainer::MPEG4 => "mp4", + StreamContainer::MPEG4 => "mpeg4", }) } } @@ -221,7 +221,7 @@ impl FromStr for StreamContainer { "matroska" => StreamContainer::Matroska, "webvtt" => StreamContainer::WebVTT, "jvtt" => StreamContainer::JVTT, - "mp4" => StreamContainer::MPEG4, + "mpeg4" => StreamContainer::MPEG4, _ => return Err(()), }) } diff --git a/remuxer/src/lib.rs b/remuxer/src/lib.rs index 9ddf7c1..c20197f 100644 --- a/remuxer/src/lib.rs +++ b/remuxer/src/lib.rs @@ -3,9 +3,11 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ +#![feature(random, exit_status_error)] pub mod extract; pub mod fragment; pub mod metadata; +pub mod mpeg4; pub mod remux; pub mod seek_index; pub mod segment_extractor; @@ -14,6 +16,7 @@ pub mod trim_writer; use ebml_struct::matroska::TrackEntry; pub use fragment::write_fragment_into; use jellymatroska::{Master, MatroskaTag}; +pub use mpeg4::matroska_to_mpeg4; pub use remux::remux_stream_into; pub fn ebml_header(webm: bool) -> MatroskaTag { diff --git a/remuxer/src/mpeg4.rs b/remuxer/src/mpeg4.rs new file mode 100644 index 0000000..9e59514 --- /dev/null +++ b/remuxer/src/mpeg4.rs @@ -0,0 +1,34 @@ +/* + 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 anyhow::Result; +use std::{ + fs::{remove_file, File}, + io::{copy, Read, Write}, + process::{Command, Stdio}, + random::random, +}; + +pub fn matroska_to_mpeg4( + mut input: impl Read + Send + 'static, + mut output: impl Write, +) -> Result<()> { + let path = format!("/tmp/jellything-tc-hack-{:016x}", random::()); + let args = format!("-f matroska -i pipe:0 -c copy -map 0 -f mp4 {path}"); + let mut child = Command::new("ffmpeg") + .args(args.split(" ")) + .stdin(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn()?; + + let mut stdin = child.stdin.take().unwrap(); + copy(&mut input, &mut stdin)?; + drop(stdin); + child.wait()?.exit_ok()?; + copy(&mut File::open(&path)?, &mut output)?; + remove_file(path)?; + + Ok(()) +} diff --git a/stream/src/fragment.rs b/stream/src/fragment.rs index 26746fc..2ce3c78 100644 --- a/stream/src/fragment.rs +++ b/stream/src/fragment.rs @@ -4,8 +4,9 @@ Copyright (C) 2025 metamuffin */ use crate::{stream_info, SMediaInfo}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use jellybase::common::stream::StreamContainer; +use jellyremuxer::matroska_to_mpeg4; use jellytranscoder::fragment::transcode; use log::warn; use std::sync::Arc; @@ -55,14 +56,13 @@ pub async fn fragment_stream( &format!("{path:?} {track_num} {index} {format_num} {container}"), // TODO maybe not use the entire source track.kind, format, - container, move |b| { tokio::task::spawn_blocking(move || { if let Err(err) = jellyremuxer::write_fragment_into( SyncIoBridge::new(b), &path, track_num, - container == StreamContainer::WebM, + false, &info.name.unwrap_or_default(), index, ) { @@ -72,12 +72,28 @@ pub async fn fragment_stream( }, ) .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}") + eprintln!("{:?}", location.abs()); + let mut frag = File::open(location.abs()).await?; + match container { + StreamContainer::WebM => {} + StreamContainer::Matroska => { + tokio::task::spawn(async move { + if let Err(err) = tokio::io::copy(&mut frag, &mut b).await { + warn!("cannot write stream: {err}") + } + }); } - }); + StreamContainer::MPEG4 => { + tokio::task::spawn_blocking(move || { + if let Err(err) = + matroska_to_mpeg4(SyncIoBridge::new(frag), SyncIoBridge::new(b)) + { + warn!("mpeg4 transmux failed: {err}"); + } + }); + } + _ => bail!("unsupported"), + } } Ok(()) diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs index 43c536a..c3746c6 100644 --- a/stream/src/stream_info.rs +++ b/stream/src/stream_info.rs @@ -79,7 +79,12 @@ fn stream_formats(t: &TrackEntry) -> Vec { codec: t.codec_id.to_string(), remux: true, bitrate: 10_000_000., // TODO - containers: containers_by_codec(&t.codec_id), + containers: { + let mut x = containers_by_codec(&t.codec_id); + // TODO remove this + x.retain_mut(|x| *x != StreamContainer::MPEG4); + x + }, 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), @@ -101,8 +106,8 @@ fn stream_formats(t: &TrackEntry) -> Vec { ("V_AV1", CONF.encoders.av1.is_some()), ("V_VP8", CONF.encoders.vp8.is_some()), ("V_VP9", CONF.encoders.vp9.is_some()), - ("V_AVC", CONF.encoders.avc.is_some()), - ("V_HEVC", CONF.encoders.hevc.is_some()), + ("V_MPEG4/ISO/AVC", CONF.encoders.avc.is_some()), + ("V_MPEGH/ISO/HEVC", CONF.encoders.hevc.is_some()), ] { if enable { formats.push(StreamFormatInfo { @@ -146,7 +151,7 @@ fn containers_by_codec(codec: &str) -> Vec { use StreamContainer::*; match codec { "V_VP8" | "V_VP9" | "V_AV1" | "A_OPUS" | "A_VORBIS" => vec![Matroska, WebM], - "V_AVC" | "A_AAC" => vec![Matroska, MPEG4], + "V_MPEG4/ISO/AVC" | "A_AAC" => vec![Matroska, MPEG4], "S_TEXT/UTF8" | "S_TEXT/WEBVTT" => vec![Matroska, WebVTT, WebM, JVTT], _ => vec![Matroska], } diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index 1d06e9a..8692423 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::stream::{StreamContainer, StreamFormatInfo, TrackKind}, + common::stream::{StreamFormatInfo, TrackKind}, CONF, }; use log::{debug, info}; @@ -24,7 +24,6 @@ pub async fn transcode( key: &str, kind: TrackKind, format: &StreamFormatInfo, - container: StreamContainer, input: impl FnOnce(ChildStdin), ) -> anyhow::Result { async_cache_file( @@ -34,8 +33,8 @@ pub async fn transcode( debug!("transcoding fragment with {format:?}"); let template = match format.codec.as_str() { - "V_AVC" => CONF.encoders.avc.as_ref(), - "V_HEVC" => CONF.encoders.hevc.as_ref(), + "V_MPEG4/ISO/AVC" => CONF.encoders.avc.as_ref(), + "V_MPEGH/ISO/HEVC" => CONF.encoders.hevc.as_ref(), "V_VP8" => CONF.encoders.vp8.as_ref(), "V_VP9" => CONF.encoders.vp9.as_ref(), "V_AV1" => CONF.encoders.av1.as_ref(), @@ -57,35 +56,27 @@ pub async fn transcode( }; let fallback_encoder = match format.codec.as_str() { "A_OPUS" => "libopus", - _ => unreachable!(), + "V_MPEG4/ISO/AVC" => "libx264", + "V_MPEGH/ISO/HEVC" => "libx265", + _ => "", }; let args = template .replace("%i", "-f matroska -i pipe:0") - .replace("%o", "-f %C pipe:1") + .replace("%o", "-f matroska pipe:1") .replace("%f", &filter) .replace("%e", "-c:%t %c -b:%t %r") .replace("%t", typechar) .replace("%c", fallback_encoder) - .replace("%r", &(format.bitrate as i64).to_string()) - .replace("%C", &container.to_string()); + .replace("%r", &(format.bitrate as i64).to_string()); info!("encoding with {:?}", args); - let container = match container { - StreamContainer::WebM => "webm", - StreamContainer::Matroska => "matroska", - StreamContainer::WebVTT => "vtt", - StreamContainer::MPEG4 => "mp4", - StreamContainer::JVTT => unreachable!(), - }; - - let mut proc = Command::new("ffmpeg") + let mut args = args.split(" "); + let mut proc = Command::new(args.next().unwrap()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .args(["-f", "matroska", "-i", "pipe:0"]) - .args(args.split(" ")) - .args(["-f", container, "pipe:1"]) + .args(args) .spawn()?; let stdin = proc.stdin.take().unwrap(); diff --git a/web/script/player/mediacaps.ts b/web/script/player/mediacaps.ts index 037a84b..29cd64a 100644 --- a/web/script/player/mediacaps.ts +++ b/web/script/player/mediacaps.ts @@ -22,9 +22,9 @@ export async function test_media_capability(format: FormatInfo, container: Strea return r } async function test_media_capability_inner(format: FormatInfo, container: StreamContainer) { - if (format.codec.startsWith("S_") || format.codec.startsWith("V_") || format.codec.startsWith("D_")) { + if (format.codec.startsWith("S_") || format.codec.startsWith("D_")) { // TODO do we need to check this? - return format.codec == "V_TEXT/WEBVTT" || format.codec == "D_WEBVTT/SUBTITLES" + return format.codec == "S_TEXT/WEBVTT" || format.codec == "S_TEXT/UTF8" || format.codec == "D_WEBVTT/SUBTITLES" } let res; if (format.codec.startsWith("A_")) { @@ -50,19 +50,20 @@ async function test_media_capability_inner(format: FormatInfo, container: Stream } }) } - console.log(format, res); return res?.supported ?? false } export function track_to_content_type(format: FormatInfo, container: StreamContainer): string { - return `${CONTAINER_TO_MIME_TYPE[container]}; codecs="${MASTROSKA_CODEC_MAP[format.codec]}"` + let c = CONTAINER_TO_MIME_TYPE[container]; + if (format.codec.startsWith("A_")) c = c.replace("video/", "audio/") + return `${c}; codecs="${MASTROSKA_CODEC_MAP[format.codec]}"` } const MASTROSKA_CODEC_MAP: { [key: string]: string } = { "V_VP9": "vp9", "V_VP8": "vp8", "V_AV1": "av1", - "V_MPEG4/ISO/AVC": "h264", + "V_MPEG4/ISO/AVC": "avc1.4d002a", "V_MPEGH/ISO/HEVC": "h265", "A_OPUS": "opus", "A_VORBIS": "vorbis", diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts index 9fa5e42..199aa14 100644 --- a/web/script/player/track/mse.ts +++ b/web/script/player/track/mse.ts @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin */ import { OVar } from "../../jshelper/mod.ts"; -import { track_to_content_type } from "../mediacaps.ts"; +import { test_media_capability, track_to_content_type } from "../mediacaps.ts"; import { BufferRange, Player } from "../player.ts"; import { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts"; import { e } from "../../jshelper/src/element.ts"; @@ -49,7 +49,19 @@ export class MSEPlayerTrack extends PlayerTrack { } this.buffered.value = [] - this.active_format.value = { usable_index: 0, format_index: 0, container: "webm", format: this.trackinfo.formats[0] } + console.log(this.trackinfo); + + for (let i = 0; i < this.trackinfo.formats.length; i++) { + const format = this.trackinfo.formats[i]; + for (const container of format.containers) { + if (container != "webm" && container != "mpeg4") continue; + if (await test_media_capability(format, container)) + this.usable_formats.push({ container, format, format_index: i, usable_index: this.usable_formats.length }) + } + } + if (!this.usable_formats.length) + return this.player.logger?.log("No availble format is supported by this device. The track can't be played back.") + this.active_format.value = this.usable_formats[0] const ct = track_to_content_type(this.active_format.value!.format, this.active_format.value!.container); this.source_buffer = this.player.media_source.addSourceBuffer(ct); @@ -142,8 +154,8 @@ export class MSEPlayerTrack extends PlayerTrack { this.current_load = frag; // TODO why is appending so unreliable?! sometimes it does not add it this.source_buffer.changeType(track_to_content_type(this.active_format.value!.format, this.active_format.value!.container)); - this.source_buffer.timestampOffset = 0 // TODO send if relative PTS //this.active_format.value !== undefined ? frag.start : 0 - console.log(`append track ${this.track_index}`); + this.source_buffer.timestampOffset = this.active_format.value?.format.remux ? 0 : frag.start + console.log(`append track at ${this.source_buffer.timestampOffset} ${this.trackinfo.kind} ${this.track_index}`); this.source_buffer.appendBuffer(frag.buf); } } -- cgit v1.2.3-70-g09d2 From a9c897c7d7df5509a195055e95dfa821fe7aa274 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 16 Apr 2025 14:39:27 +0200 Subject: the typical mse problems again... --- remuxer/src/mpeg4.rs | 2 +- transcoder/src/fragment.rs | 8 ++++++-- web/script/player/download.ts | 2 +- web/script/player/mediacaps.ts | 4 ++-- web/script/player/track/mse.ts | 3 ++- 5 files changed, 12 insertions(+), 7 deletions(-) (limited to 'transcoder') diff --git a/remuxer/src/mpeg4.rs b/remuxer/src/mpeg4.rs index 9e59514..da66fe2 100644 --- a/remuxer/src/mpeg4.rs +++ b/remuxer/src/mpeg4.rs @@ -16,7 +16,7 @@ pub fn matroska_to_mpeg4( mut output: impl Write, ) -> Result<()> { let path = format!("/tmp/jellything-tc-hack-{:016x}", random::()); - let args = format!("-f matroska -i pipe:0 -c copy -map 0 -f mp4 {path}"); + let args = format!("-f matroska -i pipe:0 -copyts -c copy -f mp4 {path}"); let mut child = Command::new("ffmpeg") .args(args.split(" ")) .stdin(Stdio::piped()) diff --git a/transcoder/src/fragment.rs b/transcoder/src/fragment.rs index 8692423..88a311e 100644 --- a/transcoder/src/fragment.rs +++ b/transcoder/src/fragment.rs @@ -56,19 +56,23 @@ pub async fn transcode( }; let fallback_encoder = match format.codec.as_str() { "A_OPUS" => "libopus", + "V_VP8" => "libvpx", + "V_VP9" => "libvpx-vp9", + "V_AV1" => "libaom", // svtav1 is x86 only :( "V_MPEG4/ISO/AVC" => "libx264", "V_MPEGH/ISO/HEVC" => "libx265", _ => "", }; let args = template - .replace("%i", "-f matroska -i pipe:0") + .replace("%i", "-f matroska -i pipe:0 -copyts") .replace("%o", "-f matroska pipe:1") .replace("%f", &filter) .replace("%e", "-c:%t %c -b:%t %r") .replace("%t", typechar) .replace("%c", fallback_encoder) - .replace("%r", &(format.bitrate as i64).to_string()); + .replace("%r", &(format.bitrate as i64).to_string()) + .replace(" ", " "); info!("encoding with {:?}", args); diff --git a/web/script/player/download.ts b/web/script/player/download.ts index 18f1e8d..8294d2a 100644 --- a/web/script/player/download.ts +++ b/web/script/player/download.ts @@ -20,7 +20,7 @@ export class SegmentDownloader { const dl_start = performance.now(); const res = await fetch(url) const dl_header = performance.now(); - if (!res.ok) throw new Error("aaaaa"); + if (!res.ok) throw new Error("aaaaaa"); const buf = await res.arrayBuffer() const dl_body = performance.now(); diff --git a/web/script/player/mediacaps.ts b/web/script/player/mediacaps.ts index 29cd64a..3c55aa9 100644 --- a/web/script/player/mediacaps.ts +++ b/web/script/player/mediacaps.ts @@ -63,8 +63,8 @@ const MASTROSKA_CODEC_MAP: { [key: string]: string } = { "V_VP9": "vp9", "V_VP8": "vp8", "V_AV1": "av1", - "V_MPEG4/ISO/AVC": "avc1.4d002a", - "V_MPEGH/ISO/HEVC": "h265", + "V_MPEG4/ISO/AVC": "avc1.42C01F", + "V_MPEGH/ISO/HEVC": "hev1.1.6.L93.90", "A_OPUS": "opus", "A_VORBIS": "vorbis", "S_TEXT/WEBVTT": "webvtt", diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts index 199aa14..6bb77e0 100644 --- a/web/script/player/track/mse.ts +++ b/web/script/player/track/mse.ts @@ -154,7 +154,8 @@ export class MSEPlayerTrack extends PlayerTrack { this.current_load = frag; // TODO why is appending so unreliable?! sometimes it does not add it this.source_buffer.changeType(track_to_content_type(this.active_format.value!.format, this.active_format.value!.container)); - this.source_buffer.timestampOffset = this.active_format.value?.format.remux ? 0 : frag.start + // this.source_buffer.timestampOffset = this.active_format.value?.format.remux ? 0 : frag.start + this.source_buffer.timestampOffset = 0 console.log(`append track at ${this.source_buffer.timestampOffset} ${this.trackinfo.kind} ${this.track_index}`); this.source_buffer.appendBuffer(frag.buf); } -- cgit v1.2.3-70-g09d2