aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-15 13:54:52 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-15 13:54:52 +0200
commitc59abb792391e2f7540a80bb8d989021fe0a5b80 (patch)
tree33688772e3822b38441f86a08c0c595ea1ef14b0
parent3b147cb1dfcbd5c7218e0accd5784d992d5ae21c (diff)
downloadjellything-c59abb792391e2f7540a80bb8d989021fe0a5b80.tar
jellything-c59abb792391e2f7540a80bb8d989021fe0a5b80.tar.bz2
jellything-c59abb792391e2f7540a80bb8d989021fe0a5b80.tar.zst
refactor jsp, part 1
-rw-r--r--common/src/stream.rs5
-rw-r--r--web/script/player/mediacaps.ts96
-rw-r--r--web/script/player/mod.ts88
-rw-r--r--web/script/player/player.ts62
-rw-r--r--web/script/player/profiles.ts_ (renamed from web/script/player/profiles.ts)5
-rw-r--r--web/script/player/track/create.ts10
-rw-r--r--web/script/player/track/mod.ts2
-rw-r--r--web/script/player/track/mse.ts63
-rw-r--r--web/script/player/track/vtt.ts6
-rw-r--r--web/script/player/types_node.ts (renamed from web/script/player/jhls.d.ts)27
-rw-r--r--web/script/player/types_stream.ts39
11 files changed, 185 insertions, 218 deletions
diff --git a/common/src/stream.rs b/common/src/stream.rs
index 9fd7daf..ba91ff5 100644
--- a/common/src/stream.rs
+++ b/common/src/stream.rs
@@ -191,6 +191,11 @@ impl StreamSpec {
index: get_num("index")? as IndexNum,
container: get_container()?,
})
+ } else if query.contains_key("fragmentindex") {
+ Ok(Self::FragmentIndex {
+ segment: get_num("segment")? as SegmentNum,
+ track: get_num("track")? as TrackNum,
+ })
} else {
Err("invalid stream spec")
}
diff --git a/web/script/player/mediacaps.ts b/web/script/player/mediacaps.ts
index e44b92b..037a84b 100644
--- a/web/script/player/mediacaps.ts
+++ b/web/script/player/mediacaps.ts
@@ -4,85 +4,58 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
/// <reference lib="dom" />
-import { EncodingProfile, SourceTrack, SourceTrackKind } from "./jhls.d.ts";
+
+import { FormatInfo, StreamContainer } from "./types_stream.ts";
const cache = new Map<string, boolean>()
// TODO this testing method makes the assumption, that if the codec is supported on its own, it can be
// TODO arbitrarly combined with others that are supported. in reality this is true but the spec does not gurantee it.
-export async function test_media_capability(track: SourceTrack): Promise<boolean> {
- const cache_key = `${get_track_kind(track.kind)};${track.codec}`
+export async function test_media_capability(format: FormatInfo, container: StreamContainer): Promise<boolean> {
+ const cache_key = JSON.stringify(format) + container
const cached = cache.get(cache_key);
if (cached !== undefined) return cached
- const r = await test_media_capability_inner(track)
- console.log(`${r ? "positive" : "negative"} media capability test finished for codec=${track.codec}`);
+ const r = await test_media_capability_inner(format, container)
+ console.log(`${r ? "positive" : "negative"} media capability test finished for codec=${format.codec}`);
cache.set(cache_key, r)
return r
}
-async function test_media_capability_inner(track: SourceTrack) {
- if (track.kind == "subtitles") {
+async function test_media_capability_inner(format: FormatInfo, container: StreamContainer) {
+ if (format.codec.startsWith("S_") || format.codec.startsWith("V_") || format.codec.startsWith("D_")) {
// TODO do we need to check this?
- return track.codec == "V_TEXT/WEBVTT" || track.codec == "D_WEBVTT/SUBTITLES"
+ return format.codec == "V_TEXT/WEBVTT" || format.codec == "D_WEBVTT/SUBTITLES"
}
let res;
- const codec = MASTROSKA_CODEC_MAP[track.codec]
- if (!codec) return console.warn(`unknown codec: ${track.codec}`), false
- if ("audio" in track.kind) {
+ if (format.codec.startsWith("A_")) {
res = await navigator.mediaCapabilities.decodingInfo({
type: "media-source",
audio: {
- contentType: `audio/webm; codecs=${codec}`,
- samplerate: track.kind.audio.sample_rate,
- channels: "" + track.kind.audio.channels,
- bitrate: 128 * 1000,
+ contentType: track_to_content_type(format, container),
+ samplerate: format.samplerate,
+ channels: "" + format.channels,
+ bitrate: format.bitrate,
}
})
}
- if ("video" in track.kind) {
+ if (format.codec.startsWith("V_")) {
res = await navigator.mediaCapabilities.decodingInfo({
type: "media-source",
video: {
- contentType: `video/webm; codecs=${codec}`,
- framerate: track.kind.video.fps || 30,
- width: track.kind.video.width,
- height: track.kind.video.height,
- bitrate: 5 * 1000 * 1000 // TODO we dont know this but we should in the future
+ contentType: track_to_content_type(format, container),
+ framerate: 30, // TODO get average framerate from server
+ width: format.width ?? 1920,
+ height: format.height ?? 1080,
+ bitrate: format.bitrate
}
})
}
+ console.log(format, res);
return res?.supported ?? false
}
-export function track_to_content_type(track: SourceTrack): string | undefined {
- if (track.kind == "subtitles") return "video/webm"
- const codec = MASTROSKA_CODEC_MAP[track.codec]
- if (!codec) return
- return `${get_track_kind(track.kind)}/webm; codecs="${codec}"`
-}
-export function profile_to_partial_track(profile: EncodingProfile): SourceTrack {
- if (profile.audio) {
- return {
- codec: FFMPEG_ENCODER_CODEC_MAP[profile.audio.codec],
- kind: { audio: { bit_depth: 16, channels: 2, sample_rate: 48000 } },
- name: "test audio",
- language: "en"
- }
- } else if (profile.video) {
- return {
- codec: FFMPEG_ENCODER_CODEC_MAP[profile.video.codec],
- kind: { video: { fps: 30, height: 1080, width: 1090 } },
- language: "en",
- name: "test video"
- }
- } else if (profile.subtitles) {
- return {
- codec: FFMPEG_ENCODER_CODEC_MAP[profile.subtitles.codec],
- kind: "subtitles",
- language: "en",
- name: "test subtitle"
- }
- } else throw new Error("unreachable");
+export function track_to_content_type(format: FormatInfo, container: StreamContainer): string {
+ return `${CONTAINER_TO_MIME_TYPE[container]}; codecs="${MASTROSKA_CODEC_MAP[format.codec]}"`
}
const MASTROSKA_CODEC_MAP: { [key: string]: string } = {
@@ -96,21 +69,10 @@ const MASTROSKA_CODEC_MAP: { [key: string]: string } = {
"S_TEXT/WEBVTT": "webvtt",
"D_WEBVTT/SUBTITLES": "webvtt",
}
-
-const FFMPEG_ENCODER_CODEC_MAP: { [key: string]: string } = {
- "libsvtav1": "V_AV1",
- "libvpx": "V_VP8",
- "libvpx-vp9": "V_VP9",
- "opus": "A_OPUS",
- "libopus": "A_OPUS",
-}
-
-export type TrackKind = "audio" | "video" | "subtitles"
-export function get_track_kind(track: SourceTrackKind): TrackKind {
- // TODO why different encodings for "subtitles"?
- if (track == "subtitles") return "subtitles"
- if ("subtitles" in track) return "subtitles"
- if ("audio" in track) return "audio"
- if ("video" in track) return "video"
- throw new Error("invalid track");
+const CONTAINER_TO_MIME_TYPE: { [key in StreamContainer]: string } = {
+ webvtt: "text/webvtt",
+ webm: "video/webm",
+ matroska: "video/x-matroska",
+ mpeg4: "video/mp4",
+ jvtt: "application/jellything-vtt+json"
}
diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts
index 15c37da..82ee287 100644
--- a/web/script/player/mod.ts
+++ b/web/script/player/mod.ts
@@ -7,11 +7,11 @@
import { OVar, show } from "../jshelper/mod.ts";
import { e } from "../jshelper/mod.ts";
import { Logger } from "../jshelper/src/log.ts";
-import { EncodingProfile } from "./jhls.d.ts";
-import { TrackKind, get_track_kind } from "./mediacaps.ts";
import { Player } from "./player.ts";
import { Popup } from "./popup.ts";
import { Playersync, playersync_controls } from "./sync.ts"
+import { WatchedState } from "./types_node.ts";
+import { FormatInfo, TrackKind } from "./types_stream.ts";
globalThis.addEventListener("DOMContentLoaded", () => {
if (document.body.classList.contains("player")) {
@@ -36,12 +36,26 @@ function toggle_fullscreen() {
else document.documentElement.requestFullscreen()
}
+function get_continue_time(w: WatchedState): number {
+ if (typeof w == "string") return 0
+ else return w.progress
+}
+
+function get_query_start_time() {
+ const u = new URL(globalThis.location.href)
+ const p = u.searchParams.get("t")
+ if (!p) return
+ const x = parseFloat(p)
+ if (Number.isNaN(x)) return
+ return x
+}
function initialize_player(el: HTMLElement, node_id: string) {
el.innerHTML = "" // clear the body
const logger = new Logger<string>(s => e("p", s))
- const player = new Player(node_id, logger)
+ const start_time = get_query_start_time() ?? 0 // TODO get_continue_time(ndata.userdata.watched);
+ const player = new Player(`/n/${encodeURIComponent(node_id)}/stream`, `/n/${encodeURIComponent(node_id)}/poster`, start_time, logger)
const show_stats = new OVar(false);
const idle_inhibit = new OVar(false)
const sync_state = new OVar<Playersync | undefined>(undefined)
@@ -72,13 +86,13 @@ function initialize_player(el: HTMLElement, node_id: string) {
const step_track_kind = (kind: TrackKind) => {
// TODO cycle through all of them
const active = player.active_tracks.value.filter(
- ts => get_track_kind(player.tracks![ts.track_index].kind) == kind)
+ ts => player.tracks![ts.track_index].kind == kind)
if (active.length > 0) {
for (const t of active) player.set_track_enabled(t.track_index, false)
} else {
const all_kind = (player.tracks ?? [])
.map((track, index) => ({ index, track }))
- .filter(({ track }) => get_track_kind(track.kind) == kind)
+ .filter(({ track }) => track.kind == kind)
if (all_kind.length < 1) return logger.log(`No ${kind} tracks available`)
player.set_track_enabled(all_kind[0].index, true)
}
@@ -92,7 +106,7 @@ function initialize_player(el: HTMLElement, node_id: string) {
const track_select = (kind: TrackKind) => {
const button = e("div", player.active_tracks.map(_ => {
const active = player.active_tracks.value.filter(
- ts => get_track_kind(player.tracks![ts.track_index].kind) == kind)
+ ts => player.tracks![ts.track_index].kind == kind)
const enabled = active.length > 0
return e("button", MEDIA_KIND_ICONS[kind][+enabled], {
class: "icon",
@@ -105,7 +119,7 @@ function initialize_player(el: HTMLElement, node_id: string) {
} else {
const all_kind = (player.tracks ?? [])
.map((track, index) => ({ index, track }))
- .filter(({ track }) => get_track_kind(track.kind) == kind)
+ .filter(({ track }) => track.kind == kind)
if (all_kind.length < 1) return
player.set_track_enabled(all_kind[0].index, true)
}
@@ -138,7 +152,7 @@ function initialize_player(el: HTMLElement, node_id: string) {
player.active_tracks.map(_ => {
const tracks_avail = (player.tracks ?? [])
.map((track, index) => ({ index, track }))
- .filter(({ track }) => get_track_kind(track.kind) == kind);
+ .filter(({ track }) => track.kind == kind);
if (!tracks_avail.length) return e("p", `No ${kind} tracks available.`) as HTMLElement;
return e("ul", { class: "jsp-track-list" }, ...tracks_avail
.map(({ track, index }): HTMLElement => {
@@ -184,15 +198,16 @@ function initialize_player(el: HTMLElement, node_id: string) {
),
pri = e("div", { class: "jsp-pri" },
pri_current = e("div", { class: "jsp-pri-current" }),
- player.chapters.map(
- chapters => e("div", ...chapters.map(chap => e("div", {
- class: "jsp-chapter",
- style: {
- left: pri_map(chap.time_start ?? 0),
- width: pri_map((chap.time_end ?? player.duration.value) - (chap.time_start ?? 0))
- }
- }, e("p", chap.labels[0][1]))))
- ),
+ // TODO
+ // player.chapters.map(
+ // chapters => e("div", ...chapters.map(chap => e("div", {
+ // class: "jsp-chapter",
+ // style: {
+ // left: pri_map(chap.time_start ?? 0),
+ // width: pri_map((chap.time_end ?? player.duration.value) - (chap.time_start ?? 0))
+ // }
+ // }, e("p", chap.labels[0][1]))))
+ // ),
player.active_tracks.map(
tracks => e("div", ...tracks.map((t, i) => t.buffered.map(
ranges => e("div", ...ranges.map(
@@ -275,8 +290,8 @@ function initialize_player(el: HTMLElement, node_id: string) {
else if (k.code == "ArrowRight") player.seek(player.position.value + 5)
else if (k.code == "ArrowUp") player.seek(player.position.value - 60)
else if (k.code == "ArrowDown") player.seek(player.position.value + 60)
- else if (k.code == "PageUp") player.seek(find_closest_chaps(player).prev?.time_start ?? 0)
- else if (k.code == "PageDown") player.seek(find_closest_chaps(player).next?.time_start ?? player.duration.value)
+ // else if (k.code == "PageUp") player.seek(find_closest_chaps(player).prev?.time_start ?? 0)
+ // else if (k.code == "PageDown") player.seek(find_closest_chaps(player).next?.time_start ?? player.duration.value)
else return;
k.preventDefault()
})
@@ -339,25 +354,26 @@ function mouse_idle(e: HTMLElement, timeout: number): OVar<boolean> {
return idle
}
-export function show_profile(profile: EncodingProfile): string {
- if (profile.audio) return `codec=${profile.audio.codec} br=${show.metric(profile.audio.bitrate, "b/s")}${profile.audio.sample_rate ? ` sr=${show.metric(profile.audio.sample_rate, "Hz")}` : ""}`
- if (profile.video) return `codec=${profile.video.codec} br=${show.metric(profile.video.bitrate, "b/s")} w=${profile.video.width} preset=${profile.video.preset}`
- if (profile.subtitles) return `codec=${profile.subtitles.codec}`
- return `???`
+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`
}
export function show_volume(v: number): string {
return `${v == 0 ? "-∞" : (Math.log10(v) * 10).toFixed(2)}dB | ${(v * 100).toFixed(2)}%`
}
-function find_closest_chaps(player: Player) {
- const now = player.position.value
- const chaps = player.chapters.value
- let prev, next;
- for (const c of chaps) {
- const t_start = (c.time_start ?? 0)
- next = c;
- if (t_start > now) break
- prev = c;
- }
- return { next, prev }
-}
+// TODO
+// function find_closest_chaps(player: Player) {
+// const now = player.position.value
+// const chaps = player.chapters.value
+// let prev, next;
+// for (const c of chaps) {
+// const t_start = (c.time_start ?? 0)
+// next = c;
+// if (t_start > now) break
+// prev = c;
+// }
+// return { next, prev }
+// }
diff --git a/web/script/player/player.ts b/web/script/player/player.ts
index e0a6ddf..f44c14f 100644
--- a/web/script/player/player.ts
+++ b/web/script/player/player.ts
@@ -5,20 +5,18 @@
*/
/// <reference lib="dom" />
import { OVar, e } from "../jshelper/mod.ts";
-import { NodePublic, NodeUserData, SourceTrack, TimeRange } from "./jhls.d.ts";
import { SegmentDownloader } from "./download.ts";
import { PlayerTrack } from "./track/mod.ts";
import { Logger } from "../jshelper/src/log.ts";
-import { WatchedState, Chapter } from "./jhls.d.ts";
-import { get_track_kind } from "./mediacaps.ts";
import { create_track } from "./track/create.ts";
+import { StreamInfo, TimeRange, TrackInfo } from "./types_stream.ts";
export interface BufferRange extends TimeRange { status: "buffered" | "loading" | "queued" }
export class Player {
public video = e("video")
public media_source = new MediaSource();
- public tracks?: SourceTrack[];
- public chapters = new OVar<Chapter[]>([]);
+ public streaminfo?: StreamInfo;
+ public tracks?: TrackInfo[];
public active_tracks = new OVar<PlayerTrack[]>([]);
public downloader: SegmentDownloader = new SegmentDownloader();
@@ -35,8 +33,8 @@ export class Player {
if (s) this.cancel_buffering_pers = this.logger?.log_persistent(s)
}
- constructor(public node_id: string, public logger?: Logger<string>) {
- this.video.poster = `/n/${encodeURIComponent(node_id)}/poster`
+ constructor(public base_url: string, poster: string, private start_time: number, public logger?: Logger<string>) {
+ this.video.poster = poster
this.volume.value = this.video.volume
let skip_change = false;
this.volume.onchange(v => {
@@ -100,40 +98,38 @@ export class Player {
}
async fetch_meta() {
- this.set_pers("Loading metadata...")
- const res = await fetch(`/n/${encodeURIComponent(this.node_id)}`, { headers: { "Accept": "application/json" } })
- if (!res.ok) return this.error.value = "Cannot download node."
+ this.set_pers("Loading stream metadata...")
+ const res = await fetch(`${this.base_url}?info`, { headers: { "Accept": "application/json" } })
+ if (!res.ok) return this.error.value = "Cannot download stream info."
- let ndata!: { node: NodePublic, userdata: NodeUserData } & { error: string }
- try { ndata = await res.json() }
+ let streaminfo!: StreamInfo & { error: string }
+ try { streaminfo = await res.json() }
catch (_) { this.set_pers("Error: Node data invalid") }
- if (ndata.error) return this.set_pers("server error: " + ndata.error)
+ if (streaminfo.error) return this.set_pers("server error: " + streaminfo.error)
this.set_pers()
//! bad code: assignment order is important because chapter callbacks use duration
- this.duration.value = ndata.node.media!.duration
- this.chapters.value = ndata.node.media!.chapters
- this.tracks = ndata.node.media!.tracks
+ this.duration.value = streaminfo.segments[0].duration
+ this.streaminfo = streaminfo
+ this.tracks = streaminfo!.segments[0].tracks;
this.video.src = URL.createObjectURL(this.media_source)
this.media_source.addEventListener("sourceopen", async () => {
let video = false, audio = false, subtitles = false;
for (let i = 0; i < this.tracks!.length; i++) {
const t = this.tracks![i];
- const kind = get_track_kind(t.kind)
- if (kind == "video" && !video)
+ if (t.kind == "video" && !video)
video = true, await this.set_track_enabled(i, true, false)
- if (kind == "audio" && !audio)
+ if (t.kind == "audio" && !audio)
audio = true, await this.set_track_enabled(i, true, false)
- if (kind == "subtitles" && !subtitles)
+ if (t.kind == "subtitles" && !subtitles)
subtitles = true, await this.set_track_enabled(i, true, false)
}
this.set_pers("Buffering initial stream fragments...")
- const start_time = get_query_start_time() ?? get_continue_time(ndata.userdata.watched);
- this.update(start_time)
- this.video.currentTime = start_time
+ this.update(this.start_time)
+ this.video.currentTime = this.start_time
await this.canplay.wait_for(true)
this.set_pers()
@@ -153,7 +149,7 @@ export class Player {
track.abort.abort()
} else if (state && active_index == -1) {
this.logger?.log(`Enabled track ${index}: ${display_track(this.tracks![index])}`)
- this.active_tracks.value.push(create_track(this, this.node_id, index, this.tracks![index])!)
+ this.active_tracks.value.push(create_track(this, this.base_url, 0, index, this.tracks![index])!)
if (update) await this.update()
}
this.active_tracks.change()
@@ -172,20 +168,6 @@ export class Player {
}
}
-function get_continue_time(w: WatchedState): number {
- if (typeof w == "string") return 0
- else return w.progress
-}
-
-function get_query_start_time() {
- const u = new URL(globalThis.location.href)
- const p = u.searchParams.get("t")
- if (!p) return
- const x = parseFloat(p)
- if (Number.isNaN(x)) return
- return x
-}
-
-function display_track(t: SourceTrack): string {
- return `"${t.name}" (${t.language})`
+function display_track(t: TrackInfo): string {
+ return `${t.name}`
}
diff --git a/web/script/player/profiles.ts b/web/script/player/profiles.ts_
index 5ebdeb4..943639c 100644
--- a/web/script/player/profiles.ts
+++ b/web/script/player/profiles.ts_
@@ -5,9 +5,6 @@
*/
/// <reference lib="dom" />
import { OVar } from "../jshelper/mod.ts";
-import { EncodingProfile, SourceTrackKind } from "./jhls.d.ts";
-import { get_track_kind } from "./mediacaps.ts";
-import { profile_to_partial_track, test_media_capability } from "./mediacaps.ts";
import { Player } from "./player.ts";
import { MSEPlayerTrack } from "./track/mse.ts";
@@ -29,7 +26,7 @@ export class ProfileSelector {
for (let id = 0; id < this.track.index!.extra_profiles.length; id++) {
const p = this.track.index!.extra_profiles[id];
// TODO hacky type casting solution
- if (get_track_kind(this.track.metadata.kind) != get_track_kind(p as unknown as SourceTrackKind)) continue
+ if (get_track_kind(this.track.trackinfo.kind) != get_track_kind(p as unknown as SourceTrackKind)) continue
if (!await test_media_capability(profile_to_partial_track(p))) continue
this.profiles.push({ id, order: 0, ...p })
}
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 <metamuffin.org>
*/
-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 <metamuffin.org>
*/
/// <reference lib="dom" />
-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 <metamuffin.org>
*/
-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<number>();
private append_queue: AppendRange[] = [];
- public profile_selector: ProfileSelector;
- public profile = new OVar<EncodingProfileExt | undefined>(undefined);
- public index?: JhlsTrackIndex
+ public index?: FragmentIndex
+ public active_format = new OVar<UsableFormat | undefined>(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<void>(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 <metamuffin.org>
*/
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()
}
diff --git a/web/script/player/jhls.d.ts b/web/script/player/types_node.ts
index c7325e4..6946313 100644
--- a/web/script/player/jhls.d.ts
+++ b/web/script/player/types_node.ts
@@ -4,13 +4,6 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-export interface JhlsTrackIndex {
- fragments: TimeRange[],
- extra_profiles: EncodingProfile[],
-}
-
-export interface TimeRange { start: number, end: number }
-
export interface NodePublic {
kind: NodeKind,
title?: string,
@@ -76,28 +69,8 @@ export type SourceTrackKind = {
}
} | "subtitles";
-export interface EncodingProfile {
- video?: {
- codec: string,
- preset: number,
- bitrate: number,
- width: number,
- },
- audio?: {
- codec: string,
- bitrate: number,
- sample_rate?: number,
- },
- subtitles?: {
- codec: string,
- },
-}
-
export interface NodeUserData {
watched: WatchedState
}
export type WatchedState = "none" | "watched" | "pending" | { progress: number }
-export interface JvttCue extends TimeRange {
- content: string
-} \ No newline at end of file
diff --git a/web/script/player/types_stream.ts b/web/script/player/types_stream.ts
new file mode 100644
index 0000000..290a778
--- /dev/null
+++ b/web/script/player/types_stream.ts
@@ -0,0 +1,39 @@
+/*
+ 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>
+*/
+export type FragmentIndex = TimeRange[]
+export interface TimeRange { start: number, end: number }
+export interface JvttCue extends TimeRange {
+ content: string
+}
+export interface StreamInfo {
+ name?: string,
+ segments: SegmentInfo[],
+}
+export interface SegmentInfo {
+ name?: string,
+ duration: number,
+ tracks: TrackInfo[],
+}
+export type TrackKind = "video" | "audio" | "subtitles"
+export interface TrackInfo {
+ name?: string,
+ language?: string,
+ kind: TrackKind,
+ formats: FormatInfo[]
+}
+export type StreamContainer = "webm" | "matroska" | "mpeg4" | "jvtt" | "webvtt"
+export interface FormatInfo {
+ codec: string,
+ bitrate: number,
+ remux: boolean,
+ containers: StreamContainer[]
+
+ width?: number,
+ height?: number,
+ channels?: number,
+ samplerate?: number,
+ bit_depth?: number,
+}