aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-16 00:09:35 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-16 00:09:35 +0200
commit39dee6820db4581fa41cfac8bcfdd399a96f5319 (patch)
treec605156a6757e5623fae36635722758947e0db65
parent50dc0e7bea02d7fc5b38edb7f943e19bd8c0285b (diff)
downloadjellything-39dee6820db4581fa41cfac8bcfdd399a96f5319.tar
jellything-39dee6820db4581fa41cfac8bcfdd399a96f5319.tar.bz2
jellything-39dee6820db4581fa41cfac8bcfdd399a96f5319.tar.zst
transcode impl but broken
-rw-r--r--common/src/stream.rs4
-rw-r--r--remuxer/src/lib.rs3
-rw-r--r--remuxer/src/mpeg4.rs34
-rw-r--r--stream/src/fragment.rs32
-rw-r--r--stream/src/stream_info.rs13
-rw-r--r--transcoder/src/fragment.rs31
-rw-r--r--web/script/player/mediacaps.ts11
-rw-r--r--web/script/player/track/mse.ts20
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);
}
}