aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/src/stream.rs8
-rw-r--r--common/src/user.rs2
-rw-r--r--stream/src/hls.rs2
-rw-r--r--stream/src/lib.rs8
-rw-r--r--web/script/player/jhls.d.ts53
-rw-r--r--web/script/player/mod.ts6
-rw-r--r--web/script/player/player.ts30
-rw-r--r--web/script/player/profiles.ts56
-rw-r--r--web/script/player/track.ts41
9 files changed, 120 insertions, 86 deletions
diff --git a/common/src/stream.rs b/common/src/stream.rs
index aa1b931..4aa51d3 100644
--- a/common/src/stream.rs
+++ b/common/src/stream.rs
@@ -26,8 +26,8 @@ pub enum StreamFormat {
#[cfg_attr(feature = "rocket", field(value = "matroska"))] Matroska,
#[cfg_attr(feature = "rocket", field(value = "hlsmaster"))] HlsMaster,
#[cfg_attr(feature = "rocket", field(value = "hlsvariant"))] HlsVariant,
- #[cfg_attr(feature = "rocket", field(value = "jhls"))] Jhls,
- #[cfg_attr(feature = "rocket", field(value = "hlsseg"))] Segment,
+ #[cfg_attr(feature = "rocket", field(value = "jhlsi"))] JhlsIndex,
+ #[cfg_attr(feature = "rocket", field(value = "snippet"))] Snippet,
#[cfg_attr(feature = "rocket", field(value = "webvtt"))] Webvtt,
}
@@ -81,8 +81,8 @@ impl StreamFormat {
StreamFormat::Matroska => "matroska",
StreamFormat::HlsMaster => "hlsmaster",
StreamFormat::HlsVariant => "hlsvariant",
- StreamFormat::Jhls => "jhls",
- StreamFormat::Segment => "hlsseg",
+ StreamFormat::JhlsIndex => "jhlsi",
+ StreamFormat::Snippet => "snippet",
StreamFormat::Webvtt => "webvtt",
}
}
diff --git a/common/src/user.rs b/common/src/user.rs
index 7e654a9..7fc1233 100644
--- a/common/src/user.rs
+++ b/common/src/user.rs
@@ -96,7 +96,7 @@ impl UserPermission {
Transcode
| ManageSelf
| FederatedContent
- | StreamFormat(Jhls | HlsMaster | HlsVariant | Matroska | Segment | Webvtt)
+ | StreamFormat(JhlsIndex | HlsMaster | HlsVariant | Matroska | Snippet | Webvtt)
)
}
}
diff --git a/stream/src/hls.rs b/stream/src/hls.rs
index 74f18b4..fb7276d 100644
--- a/stream/src/hls.rs
+++ b/stream/src/hls.rs
@@ -61,7 +61,7 @@ pub async fn hls_variant_stream(
writeln!(out, "#EXTM3U")?;
writeln!(out, "#EXT-X-VERSION:4")?;
- spec.format = StreamFormat::Segment;
+ spec.format = StreamFormat::Snippet;
for (i, Range { start, end }) in snips.iter().enumerate() {
writeln!(out, "#EXTINF:{},", end - start)?;
spec.index = Some(i);
diff --git a/stream/src/lib.rs b/stream/src/lib.rs
index e86230c..8d4ee3a 100644
--- a/stream/src/lib.rs
+++ b/stream/src/lib.rs
@@ -39,9 +39,9 @@ pub fn stream_head(spec: &StreamSpec) -> StreamHead {
StreamFormat::Original => StreamHead { content_type: "video/x-matroska", range_supported: true },
StreamFormat::Matroska => StreamHead { content_type: webm_or_mkv, range_supported: true },
StreamFormat::HlsMaster | StreamFormat::HlsVariant => StreamHead { content_type: "application/vnd.apple.mpegurl", range_supported: false },
- StreamFormat::Jhls => StreamHead { content_type: "application/jellything-jhls+json", range_supported: false },
+ StreamFormat::JhlsIndex => StreamHead { content_type: "application/jellything-jhls+json", range_supported: false },
StreamFormat::Webvtt => StreamHead { content_type: "text/vtt", range_supported: false },
- StreamFormat::Segment => StreamHead { content_type: webm_or_mkv, range_supported: false },
+ StreamFormat::Snippet => StreamHead { content_type: webm_or_mkv, range_supported: false },
}
}
@@ -85,8 +85,8 @@ pub async fn stream(
StreamFormat::Matroska => remux_stream(node, local_tracks, spec, range, b).await?,
StreamFormat::HlsMaster => hls_master_stream(node, local_tracks, spec, b).await?,
StreamFormat::HlsVariant => hls_variant_stream(node, local_tracks, spec, b).await?,
- StreamFormat::Jhls => jhls_index(node, &local_tracks, spec, b, perms).await?,
- StreamFormat::Segment => segment_stream(node, local_tracks, spec, b, perms).await?,
+ StreamFormat::JhlsIndex => jhls_index(node, &local_tracks, spec, b, perms).await?,
+ StreamFormat::Snippet => segment_stream(node, local_tracks, spec, b, perms).await?,
StreamFormat::Webvtt => webvtt_stream(node, local_tracks, spec, b).await?,
}
diff --git a/web/script/player/jhls.d.ts b/web/script/player/jhls.d.ts
index e1948ca..9938365 100644
--- a/web/script/player/jhls.d.ts
+++ b/web/script/player/jhls.d.ts
@@ -4,16 +4,57 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-export interface TimeRange { start: number, end: number }
-export interface JhlsMetadata {
- tracks: JhlsTrack[],
+export interface JhlsTrackIndex {
+ segments: TimeRange[],
extra_profiles: EncodingProfile[],
+}
+
+export interface TimeRange { start: number, end: number }
+
+export interface NodePublic {
+ kind: NodeKind,
+ title?: string,
+ tagline?: string,
+ description?: string,
+ id?: string,
+ path: string[],
+ children: string[],
+ release_date?: string,
+ index?: number,
+ media?: MediaInfo,
+ ratings: { [key in Rating]: number },
+ // might be incomplete
+}
+
+export type NodeKind = "movie"
+ | "video"
+ | "collection"
+ | "channel"
+ | "show"
+ | "series"
+ | "season"
+ | "episode"
+
+export type Rating = "imdb"
+ | "tmdb"
+ | "rotten_tomatoes"
+ | "metacritic"
+ | "youtube_views"
+ | "youtube_likes"
+ | "youtube_followers"
+
+export interface MediaInfo {
duration: number,
+ tracks: SourceTrack[],
+ chapters: Chapter[],
}
-export interface JhlsTrack {
- info: SourceTrack,
- segments: TimeRange[],
+
+export interface Chapter {
+ time_start?: number,
+ time_end?: number,
+ labels: { [key: string]: string }
}
+
export interface SourceTrack {
kind: SourceTrackKind,
name: string,
diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts
index 85befcc..5db257d 100644
--- a/web/script/player/mod.ts
+++ b/web/script/player/mod.ts
@@ -55,10 +55,10 @@ function initialize_player(el: HTMLElement, node_id: string) {
new Popup(button, popups, () =>
e("div", { class: "jsp-track-select-popup" },
...(player.tracks ?? [])
- .filter(t => get_track_kind(t.info.kind) == kind)
+ .filter(t => get_track_kind(t.kind) == kind)
.map(t => e("div",
- e("span", { class: "jsp-track-name" }, t.info.name), " ",
- e("span", { class: "jsp-track-lang" }, t.info.language)
+ e("span", { class: "jsp-track-name" }, t.name), " ",
+ e("span", { class: "jsp-track-lang" }, t.language)
))
)
)
diff --git a/web/script/player/player.ts b/web/script/player/player.ts
index c07fa37..7ffdb97 100644
--- a/web/script/player/player.ts
+++ b/web/script/player/player.ts
@@ -4,20 +4,18 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
import { OVar, e } from "../jshelper/mod.ts";
-import { JhlsMetadata, JhlsTrack, TimeRange } from "./jhls.d.ts";
+import { NodePublic, SourceTrack, TimeRange } from "./jhls.d.ts";
import { SegmentDownloader } from "./download.ts";
import { PlayerTrack } from "./track.ts";
-import { ProfileSelector } from "./profiles.ts";
import { Logger } from "../jshelper/src/log.ts";
export interface BufferRange extends TimeRange { status: "buffered" | "loading" | "queued" }
export class Player {
public video = e("video")
public media_source = new MediaSource();
- public tracks?: JhlsTrack[];
+ public tracks?: SourceTrack[];
public active_tracks = new OVar<PlayerTrack[]>([]);
public downloader: SegmentDownloader = new SegmentDownloader();
- public profile_selector!: ProfileSelector
public position = new OVar(0)
public duration = new OVar(1)
@@ -31,7 +29,7 @@ export class Player {
if (s) this.cancel_buffering_pers = this.logger?.log_persistent(s)
}
- constructor(private node_id: string, public logger?: Logger<string>) {
+ constructor(public node_id: string, public logger?: Logger<string>) {
this.video.onloadedmetadata = () => { }
this.video.ondurationchange = () => { }
this.video.ontimeupdate = () => {
@@ -80,26 +78,22 @@ export class Player {
}
async fetch_meta() {
- this.set_pers("Loading media manifest...")
- const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=jhls`, { headers: { "Accept": "application/json" } })
- if (!res.ok) return this.error.value = "Cannot download JHLS metadata"
- let metadata!: JhlsMetadata & { error: string }
+ this.set_pers("Loading node...")
+ const res = await fetch(`/n/${encodeURIComponent(this.node_id)}`, { headers: { "Accept": "application/json" } })
+ if (!res.ok) return this.error.value = "Cannot download node."
+ let metadata!: NodePublic & { error: string }
try { metadata = await res.json() }
- catch (_) { this.set_pers("Error: Failed to fetch stream info") }
+ catch (_) { this.set_pers("Error: Failed to fetch node") }
if (metadata.error) return this.set_pers("server error: " + metadata.error)
this.set_pers()
- this.tracks = metadata.tracks
+ this.tracks = metadata.media!.tracks
- this.profile_selector = new ProfileSelector(this, this.downloader.bandwidth, metadata)
- this.set_pers("Checking codec support...")
- await this.profile_selector.init()
-
- this.duration.value = metadata.duration
+ this.duration.value = metadata.media!.duration
this.video.src = URL.createObjectURL(this.media_source)
this.media_source.addEventListener("sourceopen", async () => {
this.set_pers("Initializing Media Extensions...")
- this.active_tracks.value.push(await PlayerTrack.new(this, this.node_id, 0, metadata.tracks[0]))
- this.active_tracks.value.push(await PlayerTrack.new(this, this.node_id, 1, metadata.tracks[1]))
+ this.active_tracks.value.push((await PlayerTrack.new(this, this.node_id, 0, this.tracks![0]))!) // TODO unsafe and missing ui anyway
+ this.active_tracks.value.push((await PlayerTrack.new(this, this.node_id, 1, this.tracks![1]))!)
this.active_tracks.change()
this.set_pers("Downloading initial segments...")
this.update()
diff --git a/web/script/player/profiles.ts b/web/script/player/profiles.ts
index 9284ec5..ec82a6d 100644
--- a/web/script/player/profiles.ts
+++ b/web/script/player/profiles.ts
@@ -4,67 +4,57 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
import { OVar } from "../jshelper/mod.ts";
-import { EncodingProfile, JhlsMetadata } from "./jhls.d.ts";
+import { EncodingProfile } from "./jhls.d.ts";
import { profile_to_partial_track, test_media_capability } from "./mediacaps.ts";
import { Player } from "./player.ts";
+import { PlayerTrack } from "./track.ts";
const PROFILE_UP_FAC = 0.6
const PROFILE_DOWN_FAC = 0.8
export interface EncodingProfileExt extends EncodingProfile { id: number, order: number }
export class ProfileSelector {
- profiles_video: EncodingProfileExt[] = []
- profiles_audio: EncodingProfileExt[] = []
- profiles_subtitles: EncodingProfileExt[] = []
- remux_bandwidth = new Map<number, { size: number, duration: number }>()
+ profiles: EncodingProfileExt[] = []
+ is_init = false
- constructor(private player: Player, private bandwidth: OVar<number>, private metadata: JhlsMetadata) {
+ constructor(
+ private player: Player,
+ private track: PlayerTrack,
+ private bandwidth: OVar<number>
+ ) {
}
async init() {
- for (let id = 0; id < this.metadata.extra_profiles.length; id++) {
- const p = this.metadata.extra_profiles[id];
+ for (let id = 0; id < this.track.index.extra_profiles.length; id++) {
+ const p = this.track.index.extra_profiles[id];
if (!await test_media_capability(profile_to_partial_track(p))) continue
- if (p.audio) this.profiles_audio.push({ id, order: 0, ...p })
- if (p.video) this.profiles_video.push({ id, order: 0, ...p })
- if (p.subtitles) this.profiles_subtitles.push({ id, order: 0, ...p })
+ this.profiles.push({ id, order: 0, ...p })
}
- this.profiles_audio.sort((a, b) => profile_byterate(b) - profile_byterate(a))
- this.profiles_video.sort((a, b) => profile_byterate(b) - profile_byterate(a))
- this.profiles_subtitles.sort((a, b) => profile_byterate(b) - profile_byterate(a))
- for (let i = 0; i < this.profiles_audio.length; i++) this.profiles_audio[i].order = i
- for (let i = 0; i < this.profiles_video.length; i++) this.profiles_video[i].order = i
- for (let i = 0; i < this.profiles_subtitles.length; i++) this.profiles_subtitles[i].order = i
- }
- profile_list_for_track(track: number): EncodingProfileExt[] {
- const i = this.metadata.tracks[track].info.kind
- if (i.audio) return this.profiles_audio
- if (i.video) return this.profiles_video
- if (i.subtitles) return this.profiles_subtitles
- return []
+ this.profiles.sort((a, b) => profile_byterate(b) - profile_byterate(a))
+ for (let i = 0; i < this.profiles.length; i++) this.profiles[i].order = i
}
async remux_supported(track: number): Promise<boolean> {
- return await test_media_capability(this.metadata.tracks[track].info)
+ return await test_media_capability(this.player.tracks![track])
}
async select_optimal_profile(track: number, profile: OVar<EncodingProfileExt | undefined>) {
- const profs = this.profile_list_for_track(track)
+ if (!this.is_init) await this.init(), this.is_init = true;
const sup_remux = await this.remux_supported(track);
- if (!sup_remux && !profs.length) return this.player.logger?.log("None of the available codecs are supported. The Media can't be played back.")
+ if (!sup_remux && !this.profiles.length) return this.player.logger?.log("None of the available codecs are supported. The Media can't be played back.")
const min_prof = sup_remux ? -1 : 0
const co = profile.value?.order ?? min_prof
// TODO use actual bitrate as a fallback. the server should supply it.
- const current_bitrate = profile_byterate(profs[co], 500 * 1000)
- const next_bitrate = profile_byterate(profs[co - 1], 500 * 1000)
+ const current_bitrate = profile_byterate(this.profiles[co], 500 * 1000)
+ const next_bitrate = profile_byterate(this.profiles[co - 1], 500 * 1000)
// console.log({ current_bitrate, next_bitrate, co, bandwidth: this.bandwidth.value * 8 });
- if (!sup_remux && !profile.value) profile.value = profs[co];
- if (current_bitrate > this.bandwidth.value * PROFILE_DOWN_FAC && co + 1 < profs.length) {
+ if (!sup_remux && !profile.value) profile.value = this.profiles[co];
+ if (current_bitrate > this.bandwidth.value * PROFILE_DOWN_FAC && co + 1 < this.profiles.length) {
console.log("profile up");
- profile.value = profs[co + 1]
+ profile.value = this.profiles[co + 1]
this.log_change(track, profile.value)
}
if (next_bitrate < this.bandwidth.value * PROFILE_UP_FAC && co > min_prof) {
console.log("profile down");
- profile.value = profs[co - 1]
+ profile.value = this.profiles[co - 1]
this.log_change(track, profile.value)
}
diff --git a/web/script/player/track.ts b/web/script/player/track.ts
index e95ba85..c6f90b4 100644
--- a/web/script/player/track.ts
+++ b/web/script/player/track.ts
@@ -3,12 +3,11 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-import { SourceTrack, TimeRange } from "./jhls.d.ts";
+import { JhlsTrackIndex, SourceTrack, TimeRange } from "./jhls.d.ts";
import { OVar } from "../jshelper/mod.ts";
-import { JhlsTrack } from "./jhls.d.ts";
import { profile_to_partial_track, track_to_content_type } from "./mediacaps.ts";
import { BufferRange, Player } from "./player.ts";
-import { EncodingProfileExt } from "./profiles.ts";
+import { EncodingProfileExt, ProfileSelector } from "./profiles.ts";
export const TARGET_BUFFER_DURATION = 10
export const MIN_BUFFER_DURATION = 1
@@ -22,9 +21,17 @@ export class PlayerTrack {
public buffered = new OVar<BufferRange[]>([]);
private append_queue: AppendRange[] = [];
public profile = new OVar<EncodingProfileExt | undefined>(undefined);
+ public profile_selector: ProfileSelector
- public static async new(player: Player, node_id: string, track_index: number, metadata: JhlsTrack) {
- const t = new PlayerTrack(player, node_id, track_index, metadata)
+ public static async new(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<PlayerTrack | undefined> {
+ const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jhlsi&tracks=${track_index}`, { headers: { "Accept": "application/json" } })
+ if (!res.ok) return player.error.value = "Cannot download node.", undefined
+ let index!: JhlsTrackIndex & { error: string }
+ try { index = await res.json() }
+ catch (_) { player.set_pers("Error: Failed to fetch node") }
+ if (index.error) return player.set_pers("server error: " + index.error), undefined
+
+ const t = new PlayerTrack(player, node_id, track_index, metadata, index)
await t.init()
return t
}
@@ -32,10 +39,13 @@ export class PlayerTrack {
private player: Player,
private node_id: string,
private track_index: number,
- private metadata: JhlsTrack
- ) { }
+ private metadata: SourceTrack,
+ public index: JhlsTrackIndex,
+ ) {
+ this.profile_selector = new ProfileSelector(player, this, player.downloader.bandwidth)
+ }
async init() {
- await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile);
+ await this.profile_selector.select_optimal_profile(this.track_index, this.profile);
const ct = track_to_content_type(this.track_from_profile())!
console.log(`track ${this.track_index} source buffer content-type: ${ct}`);
this.source_buffer = this.player.media_source.addSourceBuffer(ct);
@@ -63,7 +73,7 @@ export class PlayerTrack {
track_from_profile(): SourceTrack {
if (this.profile.value) return profile_to_partial_track(this.profile.value)
- else return this.metadata.info
+ else return this.metadata
}
update_buf_ranges() {
@@ -76,7 +86,7 @@ export class PlayerTrack {
});
}
for (const r of this.loading) {
- ranges.push({ ...this.metadata.segments[r], status: "loading" });
+ ranges.push({ ...this.index.segments[r], status: "loading" });
}
this.buffered.value = ranges;
}
@@ -85,8 +95,8 @@ export class PlayerTrack {
this.update_buf_ranges(); // TODO required?
const blocking = [];
- for (let i = 0; i < this.metadata.segments.length; i++) {
- const seg = this.metadata.segments[i];
+ for (let i = 0; i < this.index.segments.length; i++) {
+ const seg = this.index.segments[i];
if (seg.end < target) continue;
if (seg.start >= target + TARGET_BUFFER_DURATION) break;
if (!this.check_buf_collision(seg.start, seg.end)) continue;
@@ -107,11 +117,11 @@ export class PlayerTrack {
async load(index: number) {
this.loading.add(index);
- await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile);
- const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&webm=true&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`;
+ await this.profile_selector.select_optimal_profile(this.track_index, this.profile);
+ const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=snippet&webm=true&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`;
const buf = await this.player.downloader.download(url);
await new Promise<void>(cb => {
- this.append_queue.push({ buf, ...this.metadata.segments[index], index, cb });
+ this.append_queue.push({ buf, ...this.index.segments[index], index, cb });
this.tick_append();
});
}
@@ -125,7 +135,6 @@ export class PlayerTrack {
this.source_buffer.changeType(track_to_content_type(this.track_from_profile())!);
this.source_buffer.timestampOffset = seg.start;
this.source_buffer.appendBuffer(seg.buf);
-
}
}
}