From c59abb792391e2f7540a80bb8d989021fe0a5b80 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 15 Apr 2025 13:54:52 +0200 Subject: refactor jsp, part 1 --- web/script/player/track/create.ts | 10 +++---- web/script/player/track/mod.ts | 2 +- web/script/player/track/mse.ts | 63 ++++++++++++++++++--------------------- web/script/player/track/vtt.ts | 6 ++-- 4 files changed, 37 insertions(+), 44 deletions(-) (limited to 'web/script/player/track') diff --git a/web/script/player/track/create.ts b/web/script/player/track/create.ts index 1aaf12c..95bccca 100644 --- a/web/script/player/track/create.ts +++ b/web/script/player/track/create.ts @@ -3,15 +3,13 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -import { get_track_kind } from "../mediacaps.ts"; import { VttPlayerTrack } from "./vtt.ts"; import { MSEPlayerTrack } from "./mse.ts"; import { Player } from "../player.ts"; -import { SourceTrack } from "../jhls.d.ts"; import { PlayerTrack } from "./mod.ts"; +import { TrackInfo } from "../types_stream.ts"; -export function create_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): PlayerTrack | undefined { - const kind = get_track_kind(metadata.kind) - if (kind == "subtitles") return new VttPlayerTrack(player, node_id, track_index, metadata) - else return new MSEPlayerTrack(player, node_id, track_index, metadata) +export function create_track(player: Player, base_url: string, segment_index: number, track_index: number, track_info: TrackInfo): PlayerTrack | undefined { + if (track_info.kind == "subtitles") return new VttPlayerTrack(player, base_url, track_index, track_info) + else return new MSEPlayerTrack(player, base_url, segment_index, track_index, track_info) } diff --git a/web/script/player/track/mod.ts b/web/script/player/track/mod.ts index 0c7c1c0..5a91209 100644 --- a/web/script/player/track/mod.ts +++ b/web/script/player/track/mod.ts @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin */ /// -import { TimeRange } from "../jhls.d.ts"; +import { TimeRange } from "../types_stream.ts"; import { OVar } from "../../jshelper/mod.ts"; import { BufferRange } from "../player.ts"; diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts index d1a8c12..066bbfd 100644 --- a/web/script/player/track/mse.ts +++ b/web/script/player/track/mse.ts @@ -3,41 +3,41 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -import { JhlsTrackIndex, SourceTrack } from "../jhls.d.ts"; import { OVar } from "../../jshelper/mod.ts"; -import { profile_to_partial_track, track_to_content_type } from "../mediacaps.ts"; +import { track_to_content_type } from "../mediacaps.ts"; import { BufferRange, Player } from "../player.ts"; -import { EncodingProfileExt, ProfileSelector } from "../profiles.ts"; import { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts"; -import { show_profile } from "../mod.ts"; import { e } from "../../jshelper/src/element.ts"; +import { FormatInfo, FragmentIndex, StreamContainer, TrackInfo } from "../types_stream.ts"; + +interface UsableFormat { format_index: number, usable_index: number, format: FormatInfo, container: StreamContainer } export class MSEPlayerTrack extends PlayerTrack { public source_buffer!: SourceBuffer; private current_load?: AppendRange; private loading = new Set(); private append_queue: AppendRange[] = []; - public profile_selector: ProfileSelector; - public profile = new OVar(undefined); - public index?: JhlsTrackIndex + public index?: FragmentIndex + public active_format = new OVar(undefined); + public usable_formats: UsableFormat[] = [] constructor( private player: Player, - private node_id: string, + private base_url: string, + private segment_index: number, track_index: number, - public metadata: SourceTrack, + public trackinfo: TrackInfo, ) { super(track_index); - this.profile_selector = new ProfileSelector(player, this, player.downloader.bandwidth_avail); this.init() } async init() { this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "loading" }] try { - const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=jhlsi&track=${this.track_index}`, { headers: { "Accept": "application/json" } }); + const res = await fetch(`${this.base_url}?fragmentindex&segment=${this.segment_index}&track=${this.track_index}`, { headers: { "Accept": "application/json" } }); if (!res.ok) return this.player.error.value = "Cannot download index.", undefined; - let index!: JhlsTrackIndex & { error: string; }; + let index!: FragmentIndex & { error: string; }; try { index = await res.json(); } catch (_) { this.player.set_pers("Error: Failed to fetch node"); } if (index.error) return this.player.set_pers("server error: " + index.error), undefined; @@ -49,10 +49,9 @@ export class MSEPlayerTrack extends PlayerTrack { } this.buffered.value = [] - const canplay = await this.profile_selector.select_optimal_profile(this.track_index, this.profile); - if (!canplay) return this.player.set_track_enabled(this.track_index, false) - const ct = track_to_content_type(this.track_from_profile())!; - console.log(`track ${this.track_index} source buffer content-type: ${ct}`); + this.active_format.value = { usable_index: 0, format_index: 0, container: "webm", format: this.trackinfo.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); this.abort.signal.addEventListener("abort", () => { console.log(`destroy source buffer for track ${this.track_index}`); @@ -81,10 +80,6 @@ export class MSEPlayerTrack extends PlayerTrack { this.update(this.player.video.currentTime) } - track_from_profile(): SourceTrack { - if (this.profile.value) return profile_to_partial_track(this.profile.value); - else return this.metadata; - } update_buf_ranges() { if (!this.index) return; @@ -97,7 +92,7 @@ export class MSEPlayerTrack extends PlayerTrack { }); } for (const r of this.loading) { - ranges.push({ ...this.index.fragments[r], status: "loading" }); + ranges.push({ ...this.index[r], status: "loading" }); } this.buffered.value = ranges; } @@ -107,8 +102,8 @@ export class MSEPlayerTrack extends PlayerTrack { this.update_buf_ranges(); // TODO required? const blocking = []; - for (let i = 0; i < this.index.fragments.length; i++) { - const frag = this.index.fragments[i]; + for (let i = 0; i < this.index.length; i++) { + const frag = this.index[i]; if (frag.end < target) continue; if (frag.start >= target + TARGET_BUFFER_DURATION) break; if (!this.check_buf_collision(frag.start, frag.end)) continue; @@ -129,13 +124,13 @@ export class MSEPlayerTrack extends PlayerTrack { async load(index: number) { this.loading.add(index); - await this.profile_selector.select_optimal_profile(this.track_index, this.profile); - const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=frag&webm=true&track=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`; + // TODO update format selection + const url = `${this.base_url}?fragment&segment=${this.segment_index}&track=${this.track_index}&format=${this.active_format.value!.format_index}&index=${index}&container=${this.active_format.value!.container}`; const buf = await this.player.downloader.download(url); await new Promise(cb => { if (!this.index) return; if (this.abort.signal.aborted) return; - this.append_queue.push({ buf, ...this.index.fragments[index], index, cb }); + this.append_queue.push({ buf, ...this.index[index], index, cb }); this.tick_append(); }); } @@ -146,8 +141,8 @@ export class MSEPlayerTrack extends PlayerTrack { this.append_queue.splice(0, 1); 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.track_from_profile())!); - this.source_buffer.timestampOffset = this.profile.value !== undefined ? frag.start : 0 + 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 !== undefined ? frag.start : 0 console.log(`append track ${this.track_index}`); this.source_buffer.appendBuffer(frag.buf); } @@ -158,13 +153,13 @@ export class MSEPlayerTrack extends PlayerTrack { const c = b.filter(r => r.status == t); return `${c.length} range${c.length != 1 ? "s" : ""}, ${c.reduce((a, v) => a + v.end - v.start, 0).toFixed(2)}s` } - return this.profile.liftA2(this.buffered, (p, b) => + return this.active_format.liftA2(this.buffered, (p, b) => e("pre", - `mse track ${this.track_index}: ${(p ? `profile ${p.id} (${show_profile(p)})` : `remux`)}` - + `\n\ttype: ${track_to_content_type(this.track_from_profile())}` - + `\n\tbuffered: ${rtype("buffered", b)}` - + `\n\tqueued: ${rtype("queued", b)}` - + `\n\tloading: ${rtype("loading", b)}` + p ? + `mse track ${this.track_index}: format ${p.format_index} (${p.format.remux ? "remux" : "transcode"})` + + `\n\ttype: ${track_to_content_type(p.format, p.container)} br=${p.format.bitrate}` + + `\n\tbuffered: ${rtype("buffered", b)} / queued: ${rtype("queued", b)} / loading: ${rtype("loading", b)}` + : "" ) as HTMLElement ) } diff --git a/web/script/player/track/vtt.ts b/web/script/player/track/vtt.ts index ea4951c..3dd7670 100644 --- a/web/script/player/track/vtt.ts +++ b/web/script/player/track/vtt.ts @@ -4,8 +4,8 @@ Copyright (C) 2025 metamuffin */ import { e } from "../../jshelper/src/element.ts"; -import { SourceTrack, JvttCue } from "../jhls.d.ts"; import { Player } from "../player.ts"; +import { JvttCue, TrackInfo } from "../types_stream.ts"; import { PlayerTrack } from "./mod.ts"; export class VttPlayerTrack extends PlayerTrack { @@ -16,10 +16,10 @@ export class VttPlayerTrack extends PlayerTrack { private player: Player, private node_id: string, track_index: number, - private metadata: SourceTrack, + private track_info: TrackInfo, ) { super(track_index); - this.track = this.player.video.addTextTrack("subtitles", this.metadata.name, this.metadata.language); + this.track = this.player.video.addTextTrack("subtitles", this.track_info.name, this.track_info.language); this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "loading" }] this.init() } -- cgit v1.2.3-70-g09d2 From 50dc0e7bea02d7fc5b38edb7f943e19bd8c0285b Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 15 Apr 2025 17:56:06 +0200 Subject: remux playback works --- stream/src/stream_info.rs | 12 ++++++------ web/script/player/track/mod.ts | 2 +- web/script/player/track/mse.ts | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) (limited to 'web/script/player/track') diff --git a/stream/src/stream_info.rs b/stream/src/stream_info.rs index a8b6989..43c536a 100644 --- a/stream/src/stream_info.rs +++ b/stream/src/stream_info.rs @@ -57,12 +57,7 @@ pub(crate) async fn stream_info(info: Arc) -> Result<(InternalStream let segment = StreamSegmentInfo { name: None, - duration: metadata[0] - .info - .as_ref() - .unwrap() - .duration - .unwrap_or_default(), + duration: media_duration(&metadata[0]), tracks, }; Ok(( @@ -162,3 +157,8 @@ pub(crate) async fn write_stream_info(info: Arc, mut b: DuplexStream spawn(async move { b.write_all(&serde_json::to_vec(&info)?).await }); Ok(()) } + +fn media_duration(m: &MatroskaMetadata) -> f64 { + let info = m.info.as_ref().unwrap(); + (info.duration.unwrap_or_default() * info.timestamp_scale as f64) / 1_000_000_000. +} diff --git a/web/script/player/track/mod.ts b/web/script/player/track/mod.ts index 5a91209..99b348c 100644 --- a/web/script/player/track/mod.ts +++ b/web/script/player/track/mod.ts @@ -8,7 +8,7 @@ import { TimeRange } from "../types_stream.ts"; import { OVar } from "../../jshelper/mod.ts"; import { BufferRange } from "../player.ts"; -export const TARGET_BUFFER_DURATION = 10 +export const TARGET_BUFFER_DURATION = 15 export const MIN_BUFFER_DURATION = 1 export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts index 066bbfd..9fa5e42 100644 --- a/web/script/player/track/mse.ts +++ b/web/script/player/track/mse.ts @@ -142,7 +142,7 @@ 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 !== undefined ? frag.start : 0 + 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.appendBuffer(frag.buf); } @@ -151,7 +151,8 @@ export class MSEPlayerTrack extends PlayerTrack { public debug(): OVar { const rtype = (t: string, b: BufferRange[]) => { const c = b.filter(r => r.status == t); - return `${c.length} range${c.length != 1 ? "s" : ""}, ${c.reduce((a, v) => a + v.end - v.start, 0).toFixed(2)}s` + // ${c.length} range${c.length != 1 ? "s" : ""} + return `${c.reduce((a, v) => a + v.end - v.start, 0).toFixed(2)}s` } return this.active_format.liftA2(this.buffered, (p, b) => e("pre", -- 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 'web/script/player/track') 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 'web/script/player/track') 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 From edfd710c055621d7ef0c8d0e9c6668b4aa2283d7 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 16 Apr 2025 14:53:58 +0200 Subject: move seek index types to remuxer --- common/src/lib.rs | 1 - common/src/seek_index.rs | 33 --------------------------------- remuxer/src/seek_index.rs | 33 +++++++++++++++++++++++++++++---- web/script/player/track/mse.ts | 1 - 4 files changed, 29 insertions(+), 39 deletions(-) delete mode 100644 common/src/seek_index.rs (limited to 'web/script/player/track') diff --git a/common/src/lib.rs b/common/src/lib.rs index 00f07b6..4480db5 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -9,7 +9,6 @@ pub mod config; pub mod helpers; pub mod r#impl; pub mod jhls; -pub mod seek_index; pub mod stream; pub mod user; diff --git a/common/src/seek_index.rs b/common/src/seek_index.rs deleted file mode 100644 index 20cf394..0000000 --- a/common/src/seek_index.rs +++ /dev/null @@ -1,33 +0,0 @@ -/* - 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 bincode::{Decode, Encode}; - -pub const SEEK_INDEX_VERSION: u32 = 0x5eef1de4; - -#[derive(Debug, Clone, Decode, Encode)] -pub struct SeekIndex { - pub version: u32, - pub blocks: Vec, - pub keyframes: Vec, -} - -#[derive(Debug, Clone, Decode, Encode)] -pub struct BlockIndex { - pub pts: u64, - // pub duration: Option, - pub source_off: u64, // points to start of SimpleBlock or BlockGroup (not the Block inside it) - pub size: usize, -} - -impl Default for SeekIndex { - fn default() -> Self { - Self { - version: SEEK_INDEX_VERSION, - blocks: Vec::new(), - keyframes: Vec::new(), - } - } -} diff --git a/remuxer/src/seek_index.rs b/remuxer/src/seek_index.rs index bd351d9..7296d93 100644 --- a/remuxer/src/seek_index.rs +++ b/remuxer/src/seek_index.rs @@ -4,10 +4,8 @@ Copyright (C) 2025 metamuffin */ use anyhow::{Context, Result}; -use jellybase::{ - cache::cache_memory, - common::seek_index::{BlockIndex, SeekIndex}, -}; +use bincode::{Decode, Encode}; +use jellybase::cache::cache_memory; use jellymatroska::{ block::Block, read::EbmlReader, @@ -17,6 +15,33 @@ use jellymatroska::{ use log::{debug, info, trace, warn}; use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path, sync::Arc}; +pub const SEEK_INDEX_VERSION: u32 = 0x5eef1de4; + +#[derive(Debug, Clone, Decode, Encode)] +pub struct SeekIndex { + pub version: u32, + pub blocks: Vec, + pub keyframes: Vec, +} + +#[derive(Debug, Clone, Decode, Encode)] +pub struct BlockIndex { + pub pts: u64, + // pub duration: Option, + pub source_off: u64, // points to start of SimpleBlock or BlockGroup (not the Block inside it) + pub size: usize, +} + +impl Default for SeekIndex { + fn default() -> Self { + Self { + version: SEEK_INDEX_VERSION, + blocks: Vec::new(), + keyframes: Vec::new(), + } + } +} + pub fn get_seek_index(path: &Path) -> anyhow::Result>>> { cache_memory(&["seekindex", path.to_str().unwrap()], move || { info!("generating seek index for {path:?}"); diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts index 6bb77e0..5565a6b 100644 --- a/web/script/player/track/mse.ts +++ b/web/script/player/track/mse.ts @@ -156,7 +156,6 @@ export class MSEPlayerTrack extends PlayerTrack { 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 = 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 From ad8016d8014af1e8dfb267fcdb51da63ab8ca4a9 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Wed, 16 Apr 2025 17:23:55 +0200 Subject: better debug info and loggin --- web/script/player/mod.ts | 19 +++++++++++-------- web/script/player/track/mse.ts | 7 +++++-- web/style/js-player.css | 2 ++ 3 files changed, 18 insertions(+), 10 deletions(-) (limited to 'web/script/player/track') diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index 82ee287..af62cde 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -66,11 +66,11 @@ function initialize_player(el: HTMLElement, node_id: string) { let mute_saved_volume = 1; const toggle_mute = () => { if (player.volume.value == 0) { - logger.log("Unmuted."); + logger.log("Unmuted.", "volume"); player.volume.value = mute_saved_volume } else { - logger.log("Muted."); + logger.log("Muted.", "volume"); mute_saved_volume = player.volume.value player.volume.value = 0. } @@ -281,8 +281,8 @@ function initialize_player(el: HTMLElement, node_id: string) { else if (k.code == "KeyS") screenshot_video(player.video) else if (k.code == "KeyJ") step_track_kind("subtitles") else if (k.code == "KeyM") toggle_mute() - else if (k.code == "Digit9") (player.volume.value /= 1.2), logger.log(`Volume decreased to ${show_volume(player.volume.value)}`) - else if (k.code == "Digit0") (player.volume.value *= 1.2), logger.log(`Volume increased to ${show_volume(player.volume.value)}`) + else if (k.code == "Digit9") (player.volume.value /= 1.2), logger.log(`Volume decreased to ${show_volume(player.volume.value)}`, "volume") + else if (k.code == "Digit0") (player.volume.value *= 1.2), logger.log(`Volume increased to ${show_volume(player.volume.value)}`, "volume") else if (k.key == "#") step_track_kind("audio") else if (k.key == "_") step_track_kind("video") else if (k.code == "KeyV") show_stats.value = !show_stats.value @@ -355,10 +355,13 @@ function mouse_idle(e: HTMLElement, timeout: number): OVar { } export function show_format(format: FormatInfo): string { - // if (format.audio) return `codec=${format.audio.codec} br=${show.metric(format.audio.bitrate, "b/s")}${format.audio.sample_rate ? ` sr=${show.metric(format.audio.sample_rate, "Hz")}` : ""}` - // if (format.video) return `codec=${format.video.codec} br=${show.metric(format.video.bitrate, "b/s")} w=${format.video.width} preset=${format.video.preset}` - // if (format.subtitles) return `codec=${format.subtitles.codec}` - return `TODO` + let o = `${format.codec} br=${show.metric(format.bitrate, "b/s")} ac=${format.containers.join(",")}` + if (format.width) o += ` w=${format.width}` + if (format.height) o += ` h=${format.height}` + if (format.samplerate) o += ` ar=${show.metric(format.samplerate, "Hz")}` + if (format.channels) o += ` ac=${format.channels}` + if (format.bit_depth) o += ` bits=${format.bit_depth}` + return o } export function show_volume(v: number): string { return `${v == 0 ? "-∞" : (Math.log10(v) * 10).toFixed(2)}dB | ${(v * 100).toFixed(2)}%` diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts index 5565a6b..237b6f6 100644 --- a/web/script/player/track/mse.ts +++ b/web/script/player/track/mse.ts @@ -9,6 +9,7 @@ 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"; import { FormatInfo, FragmentIndex, StreamContainer, TrackInfo } from "../types_stream.ts"; +import { show_format } from "../mod.ts"; interface UsableFormat { format_index: number, usable_index: number, format: FormatInfo, container: StreamContainer } @@ -154,8 +155,9 @@ 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?.container == "mpeg4" ? frag.start : 0 // this.source_buffer.timestampOffset = this.active_format.value?.format.remux ? 0 : frag.start - this.source_buffer.timestampOffset = 0 + // this.source_buffer.timestampOffset = 0 this.source_buffer.appendBuffer(frag.buf); } } @@ -170,7 +172,8 @@ export class MSEPlayerTrack extends PlayerTrack { e("pre", p ? `mse track ${this.track_index}: format ${p.format_index} (${p.format.remux ? "remux" : "transcode"})` - + `\n\ttype: ${track_to_content_type(p.format, p.container)} br=${p.format.bitrate}` + + `\n\tformat: ${show_format(p.format)}` + + `\n\tbuffer type: ${track_to_content_type(p.format, p.container)}` + `\n\tbuffered: ${rtype("buffered", b)} / queued: ${rtype("queued", b)} / loading: ${rtype("loading", b)}` : "" ) as HTMLElement diff --git a/web/style/js-player.css b/web/style/js-player.css index c9a48e9..33669a5 100644 --- a/web/style/js-player.css +++ b/web/style/js-player.css @@ -112,6 +112,8 @@ padding: 0.15em; margin: 0px; font-size: large; +} +.jsp .jsh-log-line-appear { animation-name: appear; animation-timing-function: linear; animation-duration: 0.5s; -- cgit v1.2.3-70-g09d2