diff options
-rw-r--r-- | web/script/player/mediacaps.ts | 33 | ||||
-rw-r--r-- | web/script/player/player.ts | 5 | ||||
-rw-r--r-- | web/script/player/track.ts | 109 |
3 files changed, 98 insertions, 49 deletions
diff --git a/web/script/player/mediacaps.ts b/web/script/player/mediacaps.ts index b722904..1fec967 100644 --- a/web/script/player/mediacaps.ts +++ b/web/script/player/mediacaps.ts @@ -1,4 +1,4 @@ -import { SourceTrack, SourceTrackKind } from "./jhls.d.ts"; +import { EncodingProfile, SourceTrack, SourceTrackKind } from "./jhls.d.ts"; const cache = new Map<string, boolean>() @@ -47,6 +47,29 @@ async function test_media_capability_inner(track: SourceTrack) { return res?.supported ?? false } +export function track_to_content_type(track: SourceTrack): string | undefined { + 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 throw new Error("todo: subtitles"); +} + const MASTROSKA_CODEC_MAP: { [key: string]: string } = { "V_VP9": "vp9", "V_VP8": "vp8", @@ -58,6 +81,14 @@ const MASTROSKA_CODEC_MAP: { [key: string]: string } = { "S_TEXT/WEBVTT": "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 function get_track_kind(track: SourceTrackKind): "audio" | "video" | "subtitles" { if (track.audio) return "audio" if (track.video) return "video" diff --git a/web/script/player/player.ts b/web/script/player/player.ts index 8425279..696fab2 100644 --- a/web/script/player/player.ts +++ b/web/script/player/player.ts @@ -85,8 +85,9 @@ export class Player { this.profile_selector = new ProfileSelector(this, this.downloader.bandwidth, metadata) this.media_source.addEventListener("sourceopen", async () => { - this.tracks.value.push(new PlayerTrack(this, this.node_id, 0, metadata.tracks[0])) - this.tracks.value.push(new PlayerTrack(this, this.node_id, 1, metadata.tracks[1])) + this.set_pers("Initializing Media Extensions...") + this.tracks.value.push(await PlayerTrack.new(this, this.node_id, 0, metadata.tracks[0])) + this.tracks.value.push(await PlayerTrack.new(this, this.node_id, 1, metadata.tracks[1])) this.tracks.change() this.set_pers("Fetching initial segments...") this.update() diff --git a/web/script/player/track.ts b/web/script/player/track.ts index 3f1e073..cc21763 100644 --- a/web/script/player/track.ts +++ b/web/script/player/track.ts @@ -1,103 +1,120 @@ +import { SourceTrack, TimeRange } from "./jhls.d.ts"; import { OVar } from "../jshelper/mod.ts"; -import { JhlsTrack, TimeRange } from "./jhls.d.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"; -const TARGET_BUFFER_DURATION = 10 -const MIN_BUFFER_DURATION = 1 +export const TARGET_BUFFER_DURATION = 10 +export const MIN_BUFFER_DURATION = 1 export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } + export class PlayerTrack { - private source_buffer: SourceBuffer - private current_load?: AppendRange - private loading = new Set<number>() - public buffered = new OVar<BufferRange[]>([]) - private append_queue: AppendRange[] = [] - public profile = new OVar<EncodingProfileExt | undefined>(undefined) + private source_buffer!: SourceBuffer; + private current_load?: AppendRange; + private loading = new Set<number>(); + public buffered = new OVar<BufferRange[]>([]); + private append_queue: AppendRange[] = []; + public profile = new OVar<EncodingProfileExt | undefined>(undefined); + public static async new(player: Player, node_id: string, track_index: number, metadata: JhlsTrack) { + const t = new PlayerTrack(player, node_id, track_index, metadata) + await t.init() + return t + } constructor( private player: Player, private node_id: string, private track_index: number, private metadata: JhlsTrack - ) { - this.source_buffer = this.player.media_source.addSourceBuffer("video/webm; codecs=\"opus,vorbis,vp8,vp9,av1\"") - this.source_buffer.mode = "segments" + ) { } + async init() { + await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile); + const ct = track_to_content_type(this.track_from_profile())! + console.log("source buffer content-type: " + ct); + this.source_buffer = this.player.media_source.addSourceBuffer(ct); + this.source_buffer.mode = "segments"; this.source_buffer.addEventListener("updateend", () => { if (this.current_load) { - this.current_load.cb() - this.loading.delete(this.current_load.index) - this.current_load = undefined + this.current_load.cb(); + this.loading.delete(this.current_load.index); + this.current_load = undefined; } - this.update_buf_ranges() - this.tick_append() - }) + this.update_buf_ranges(); + this.tick_append(); + }); this.source_buffer.addEventListener("error", e => { console.error("sourcebuffer error", e); - }) + }); this.source_buffer.addEventListener("abort", e => { console.error("sourcebuffer abort", e); - }) + }); + } + + track_from_profile(): SourceTrack { + if (this.profile.value) return profile_to_partial_track(this.profile.value) + else return this.metadata.info } update_buf_ranges() { - const ranges: BufferRange[] = [] + const ranges: BufferRange[] = []; for (let i = 0; i < this.source_buffer.buffered.length; i++) { ranges.push({ start: this.source_buffer.buffered.start(i), end: this.source_buffer.buffered.end(i), status: "buffered" - }) + }); } for (const r of this.loading) { - ranges.push({ ...this.metadata.segments[r], status: "loading" }) + ranges.push({ ...this.metadata.segments[r], status: "loading" }); } - this.buffered.value = ranges + this.buffered.value = ranges; } async update(target: number) { - this.update_buf_ranges() // TODO required? + this.update_buf_ranges(); // TODO required? - const blocking = [] + const blocking = []; for (let i = 0; i < this.metadata.segments.length; i++) { const seg = this.metadata.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 + if (seg.end < target) continue; + if (seg.start >= target + TARGET_BUFFER_DURATION) break; + if (!this.check_buf_collision(seg.start, seg.end)) continue; if (seg.start <= target + MIN_BUFFER_DURATION) - blocking.push(this.load(i)) + blocking.push(this.load(i)); else - this.load(i) + this.load(i); } - await Promise.all(blocking) + await Promise.all(blocking); } check_buf_collision(start: number, end: number) { - const EPSILON = 0.01 + const EPSILON = 0.01; for (const r of this.buffered.value) if (r.end - EPSILON > start && r.start < end - EPSILON) - return false - return true + return false; + return true; } async load(index: number) { - this.loading.add(index) - await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile) + 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}` : ""}`; - const buf = await this.player.downloader.download(url) + 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.tick_append() - }) + this.append_queue.push({ buf, ...this.metadata.segments[index], index, cb }); + this.tick_append(); + }); } tick_append() { - if (this.source_buffer.updating) return + if (this.source_buffer.updating) return; if (this.append_queue.length) { const seg = this.append_queue[0]; - this.append_queue.splice(0, 1) - this.current_load = seg + this.append_queue.splice(0, 1); + this.current_load = seg; // TODO why is appending so unreliable?! sometimes it does not add it - this.source_buffer.changeType("video/webm; codecs=\"opus,vorbis,vp8,vp9,av1\"") - this.source_buffer.timestampOffset = seg.start + this.source_buffer.changeType(track_to_content_type(this.track_from_profile())!); + this.source_buffer.timestampOffset = seg.start; this.source_buffer.appendBuffer(seg.buf); } |