diff options
Diffstat (limited to 'web/script/player')
| -rw-r--r-- | web/script/player/jhls.d.ts | 53 | ||||
| -rw-r--r-- | web/script/player/mod.ts | 6 | ||||
| -rw-r--r-- | web/script/player/player.ts | 30 | ||||
| -rw-r--r-- | web/script/player/profiles.ts | 56 | ||||
| -rw-r--r-- | web/script/player/track.ts | 41 | 
5 files changed, 110 insertions, 76 deletions
| 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); -          }      }  } | 
