diff options
-rw-r--r-- | common/src/stream.rs | 4 | ||||
-rw-r--r-- | remuxer/src/lib.rs | 3 | ||||
-rw-r--r-- | remuxer/src/mpeg4.rs | 34 | ||||
-rw-r--r-- | stream/src/fragment.rs | 32 | ||||
-rw-r--r-- | stream/src/stream_info.rs | 13 | ||||
-rw-r--r-- | transcoder/src/fragment.rs | 31 | ||||
-rw-r--r-- | web/script/player/mediacaps.ts | 11 | ||||
-rw-r--r-- | web/script/player/track/mse.ts | 20 |
8 files changed, 105 insertions, 43 deletions
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 <metamuffin.org> */ +#![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 <metamuffin.org> +*/ +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::<u64>()); + 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 <metamuffin.org> */ 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<StreamFormatInfo> { 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<StreamFormatInfo> { ("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<StreamContainer> { 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<CachePath> { 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 <metamuffin.org> */ 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); } } |