diff options
Diffstat (limited to 'web/script/player/track/mse.ts')
| -rw-r--r-- | web/script/player/track/mse.ts | 140 | 
1 files changed, 140 insertions, 0 deletions
| diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts new file mode 100644 index 0000000..623e149 --- /dev/null +++ b/web/script/player/track/mse.ts @@ -0,0 +1,140 @@ +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 { 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; +  private loading = new Set<number>(); +  private append_queue: AppendRange[] = []; +  public profile_selector: ProfileSelector; +  public profile = new OVar<EncodingProfileExt | undefined>(undefined); + +  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); +  } +  async init() { +    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); +    this.abort.signal.addEventListener("abort", () => { +      console.log(`destroy source buffer for track ${this.track_index}`); +      this.player.media_source.removeSourceBuffer(this.source_buffer); +    }); +    this.source_buffer.mode = "segments"; +    this.source_buffer.addEventListener("updateend", () => { +      if (this.abort.signal.aborted) return; +      if (this.current_load) { +        this.current_load.cb(); +        this.loading.delete(this.current_load.index); +        this.current_load = undefined; +      } +      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; +  } + +  update_buf_ranges() { +    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.index.segments[r], status: "loading" }); +    } +    this.buffered.value = ranges; +  } + +  async update(target: number) { +    this.update_buf_ranges(); // TODO required? + +    const blocking = []; +    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; +      if (seg.start <= target + MIN_BUFFER_DURATION) +        blocking.push(this.load(i)); + +      else +        this.load(i); +    } +    await Promise.all(blocking); +  } +  check_buf_collision(start: number, end: number) { +    const EPSILON = 0.01; +    for (const r of this.buffered.value) +      if (r.end - EPSILON > start && r.start < end - EPSILON) +        return false; +    return true; +  } + +  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=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.abort.signal.aborted) return; +      this.append_queue.push({ buf, ...this.index.segments[index], index, cb }); +      this.tick_append(); +    }); +  } +  tick_append() { +    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; +      // 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 = seg.start; +      this.source_buffer.appendBuffer(seg.buf); +    } +  } +} | 
