diff options
-rw-r--r-- | matroska/src/bin/mkvdump.rs | 1 | ||||
-rw-r--r-- | web/script/player/mod.ts | 43 | ||||
-rw-r--r-- | web/script/player/player.ts | 3 | ||||
-rw-r--r-- | web/script/player/sync.ts | 41 | ||||
-rw-r--r-- | web/script/player/track/create.ts | 10 | ||||
-rw-r--r-- | web/script/player/track/mse.ts | 44 | ||||
-rw-r--r-- | web/script/player/track/vtt.ts | 52 |
7 files changed, 104 insertions, 90 deletions
diff --git a/matroska/src/bin/mkvdump.rs b/matroska/src/bin/mkvdump.rs index b58adcc..83919d7 100644 --- a/matroska/src/bin/mkvdump.rs +++ b/matroska/src/bin/mkvdump.rs @@ -21,3 +21,4 @@ fn main() { } } } +
\ No newline at end of file diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index a53789f..0f30d45 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -11,7 +11,7 @@ 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 } from "./sync.ts" +import { Playersync, playersync_controls } from "./sync.ts" import { MSEPlayerTrack } from "./track/mse.ts"; globalThis.addEventListener("DOMContentLoaded", () => { @@ -110,49 +110,12 @@ function initialize_player(el: HTMLElement, node_id: string) { ) return button } + const settings_popup = () => { const button = e("button", "settings", { class: "icon" }) - let channel_name: HTMLInputElement; - let channel_name_copy: HTMLInputElement; new Popup(button, popups, () => e("div", { class: "jsp-settings-popup" }, e("h2", "Settings"), - e("div", { class: ["jsp-controlgroup", "jsp-playersync-controls"] }, - e("h3", "Playersync"), - sync_state.map(sync => { - console.log("aaaaa", sync); - return sync - ? e("div", - e("span", "Sync enabled."), - e("button", "Disable", { - onclick: () => { sync_state.value?.destroy(); sync_state.value = undefined } - }), - e("p", "Session ID: ", - channel_name_copy = e("input", { type: "text", disabled: true, value: sync.name }), - e("button", "content_paste_go", { - class: "icon", - onclick: () => { - logger.log("Session ID copied to clipboard.") - navigator.clipboard.writeText(channel_name_copy.value) - } - }) - )) - : e("div", - channel_name = e("input", { type: "text", placeholder: "someroom:example.org" }), - e("button", "Join", { - onclick: () => { - if (!channel_name.value.length) return - sync_state.value?.destroy() - sync_state.value = new Playersync(player, logger, channel_name.value) - } - }), e("br"), - e("button", "Create new session", { - onclick: () => { - sync_state.value?.destroy() - sync_state.value = new Playersync(player, logger) - } - })) - }) - ) + playersync_controls(sync_state, player) )) return button; } diff --git a/web/script/player/player.ts b/web/script/player/player.ts index 9cedaf5..8fccb24 100644 --- a/web/script/player/player.ts +++ b/web/script/player/player.ts @@ -129,6 +129,7 @@ export class Player { this.set_pers() }) } + async update(newt?: number) { await Promise.all(this.active_tracks.value.map(t => t.update(newt ?? this.video.currentTime))) } @@ -142,7 +143,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((await create_track(this, this.node_id, index, this.tracks![index]))!) + this.active_tracks.value.push(create_track(this, this.node_id, index, this.tracks![index])!) if (update) await this.update() } this.active_tracks.change() diff --git a/web/script/player/sync.ts b/web/script/player/sync.ts index 34a46e3..3c5e3a2 100644 --- a/web/script/player/sync.ts +++ b/web/script/player/sync.ts @@ -4,9 +4,50 @@ Copyright (C) 2024 metamuffin <metamuffin.org> */ /// <reference lib="dom" /> +import { OVar, e } from "../jshelper/mod.ts"; import { Logger } from "../jshelper/src/log.ts"; import { Player } from "./player.ts" +export function playersync_controls(sync_state: OVar<undefined | Playersync>, player: Player) { + let channel_name: HTMLInputElement; + let channel_name_copy: HTMLInputElement; + return e("div", { class: ["jsp-controlgroup", "jsp-playersync-controls"] }, + e("h3", "Playersync"), + sync_state.map(sync => sync + ? e("div", + e("span", "Sync enabled."), + e("button", "Disable", { + onclick: () => { sync_state.value?.destroy(); sync_state.value = undefined } + }), + e("p", "Session ID: ", + channel_name_copy = e("input", { type: "text", disabled: true, value: sync.name }), + e("button", "content_paste_go", { + class: "icon", + onclick: () => { + player.logger?.log("Session ID copied to clipboard.") + navigator.clipboard.writeText(channel_name_copy.value) + } + }) + )) + : e("div", + channel_name = e("input", { type: "text", placeholder: "someroom:example.org" }), + e("button", "Join", { + onclick: () => { + if (!channel_name.value.length) return + sync_state.value?.destroy() + sync_state.value = new Playersync(player, player.logger!, channel_name.value) + } + }), e("br"), + e("button", "Create new session", { + onclick: () => { + sync_state.value?.destroy() + sync_state.value = new Playersync(player, player.logger!) + } + })) + ) + ) +} + function get_username() { return document.querySelector("nav .account .username")?.textContent ?? "Unknown User" } diff --git a/web/script/player/track/create.ts b/web/script/player/track/create.ts index f674c3a..d63a9ce 100644 --- a/web/script/player/track/create.ts +++ b/web/script/player/track/create.ts @@ -1,12 +1,12 @@ import { get_track_kind } from "../mediacaps.ts"; -import { create_vtt_track } from "./vtt.ts"; -import { create_mse_track } from "./mse.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"; -export async function create_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<PlayerTrack | undefined> { +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 await create_vtt_track(player, node_id, track_index, metadata) - else return await create_mse_track(player, node_id, track_index, metadata) + if (kind == "subtitles") return new VttPlayerTrack(player, node_id, track_index, metadata) + else return new MSEPlayerTrack(player, node_id, track_index, metadata) } diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts index 2d17b87..a4320b5 100644 --- a/web/script/player/track/mse.ts +++ b/web/script/player/track/mse.ts @@ -5,25 +5,6 @@ import { BufferRange, Player } from "../player.ts"; import { EncodingProfileExt, ProfileSelector } from "../profiles.ts"; import { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts"; -export async function create_mse_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<MSEPlayerTrack | undefined> { - try { - 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 index.", 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 MSEPlayerTrack(player, node_id, track_index, metadata, index); - await t.init(); - return t; - } catch (e) { - if (e instanceof TypeError) { - player.set_pers("Cannot download index: Network Error"); - } else throw e; - } -} - export class MSEPlayerTrack extends PlayerTrack { public source_buffer!: SourceBuffer; private current_load?: AppendRange; @@ -31,18 +12,36 @@ export class MSEPlayerTrack extends PlayerTrack { private append_queue: AppendRange[] = []; public profile_selector: ProfileSelector; public profile = new OVar<EncodingProfileExt | undefined>(undefined); + public index?: JhlsTrackIndex constructor( private player: Player, private node_id: string, track_index: number, private metadata: SourceTrack, - public index: JhlsTrackIndex ) { super(track_index); this.profile_selector = new ProfileSelector(player, this, player.downloader.bandwidth); + 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&tracks=${this.track_index}`, { headers: { "Accept": "application/json" } }); + if (!res.ok) return this.player.error.value = "Cannot download index.", undefined; + let index!: JhlsTrackIndex & { 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; + this.index = index + } catch (e) { + if (e instanceof TypeError) { + this.player.set_pers("Cannot download index: Network Error"); + } else throw e; + } + this.buffered.value = [] + 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}`); @@ -71,6 +70,8 @@ export class MSEPlayerTrack extends PlayerTrack { this.source_buffer.addEventListener("abort", e => { console.error("sourcebuffer abort", e); }); + + this.update(this.player.video.currentTime) } track_from_profile(): SourceTrack { if (this.profile.value) return profile_to_partial_track(this.profile.value); @@ -78,6 +79,7 @@ export class MSEPlayerTrack extends PlayerTrack { } update_buf_ranges() { + if (!this.index) return; const ranges: BufferRange[] = []; for (let i = 0; i < this.source_buffer.buffered.length; i++) { ranges.push({ @@ -93,6 +95,7 @@ export class MSEPlayerTrack extends PlayerTrack { } async update(target: number) { + if (!this.index) return; this.update_buf_ranges(); // TODO required? const blocking = []; @@ -122,6 +125,7 @@ export class MSEPlayerTrack extends PlayerTrack { 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 => { + if (!this.index) return; if (this.abort.signal.aborted) return; this.append_queue.push({ buf, ...this.index.segments[index], index, cb }); this.tick_append(); diff --git a/web/script/player/track/vtt.ts b/web/script/player/track/vtt.ts index efdafe0..4dddbc7 100644 --- a/web/script/player/track/vtt.ts +++ b/web/script/player/track/vtt.ts @@ -2,40 +2,26 @@ import { SourceTrack, JvttCue } from "../jhls.d.ts"; import { Player } from "../player.ts"; import { PlayerTrack } from "./mod.ts"; -export async function create_vtt_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<VttPlayerTrack | undefined> { - let index: JvttCue[]; - try { - const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jvtt&tracks=${track_index}`, { headers: { "Accept": "application/json" } }); - if (!res.ok) return player.error.value = "Cannot download index.", undefined; - let ai!: JvttCue[] & { error: string; }; - try { ai = await res.json(); } - catch (_) { player.set_pers("Error: Failed to fetch node"); } - if (ai.error) return player.set_pers("server error: " + ai.error), undefined; - index = ai; - } catch (e) { - if (e instanceof TypeError) { - player.set_pers("Cannot download subtitles: Network Error"); - return undefined - } else throw e; - } - const t = new VttPlayerTrack(player, node_id, track_index, metadata, index); - return t; -} - export class VttPlayerTrack extends PlayerTrack { private track: TextTrack; + public cues?: JvttCue[] constructor( private player: Player, private node_id: string, track_index: number, private metadata: SourceTrack, - public cues: JvttCue[] ) { super(track_index); - this.buffered.value = [{ start: 0, end: player.duration.value, status: "buffered" }] - this.track = this.player.video.addTextTrack("subtitles", metadata.name, metadata.language); - for (const cue of cues) { + this.track = this.player.video.addTextTrack("subtitles", this.metadata.name, this.metadata.language); + this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "loading" }] + this.init() + } + + private on_ready() { + if (!this.cues) return + this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "buffered" }] + for (const cue of this.cues) { this.track.addCue(create_cue(cue)); } this.track.mode = "showing"; @@ -44,6 +30,24 @@ export class VttPlayerTrack extends PlayerTrack { this.track.mode = "hidden"; }); } + + async init() { + try { + const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=jvtt&tracks=${this.track_index}`, { headers: { "Accept": "application/json" } }); + if (!res.ok) return this.player.error.value = "Cannot download index.", undefined; + let ai!: JvttCue[] & { error: string; }; + try { ai = await res.json(); } + catch (_) { this.player.set_pers("Error: Failed to fetch node"); } + if (ai.error) return this.player.set_pers("server error: " + ai.error), undefined; + this.cues = ai; + } catch (e) { + if (e instanceof TypeError) { + this.player.set_pers("Cannot download subtitles: Network Error"); + return undefined + } else throw e; + } + this.on_ready() + } } function create_cue(cue: JvttCue): VTTCue { |