diff options
Diffstat (limited to 'web/script/player')
| -rw-r--r-- | web/script/player/mod.ts | 132 | 
1 files changed, 85 insertions, 47 deletions
| diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index aed504a..17cd77e 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -34,7 +34,8 @@ export interface SourceTrackKind {      subtitles?: boolean,  } -const TARGET_BUFFER_DURATION = 30 +const TARGET_BUFFER_DURATION = 15 +const MIN_BUFFER_DURATION = 1  document.addEventListener("DOMContentLoaded", () => {      if (document.body.classList.contains("player")) { @@ -64,15 +65,19 @@ function initialize_player(el: HTMLElement, node_id: string) {          ),          pri = e("div", { class: "jsp-pri" },              pri_current = e("div", { class: "jsp-pri-current" }), -            player.buffered.map( -                ranges => e("div", ...ranges.map( -                    r => e("div", { -                        class: "jsp-pri-buffered", style: { -                            width: pri_map(r.end - r.start), -                            left: pri_map(r.start) -                        } -                    }) -                )) +            player.tracks.map( +                tracks => e("div", ...tracks.map((t, i) => t.buffered.map( +                    ranges => e("div", ...ranges.map( +                        r => e("div", { +                            class: ["jsp-pri-buffer", `jsp-pri-buffer-${r.status}`], +                            style: { +                                width: pri_map(r.end - r.start), +                                top: `calc(var(--pribufsize)*${i})`, +                                left: pri_map(r.start) +                            } +                        }) +                    )) +                )))              )          ),          e("button", "X", { @@ -147,12 +152,12 @@ interface BufferRange extends Range { status: "buffered" | "downloading" | "queu  class Player {      public video = e("video")      private media_source = new MediaSource(); -    public tracks: PlayerTrack[] = []; +    public tracks = new OVar<PlayerTrack[]>([]);      public position = new OVar(0)      public duration = new OVar(1)      public playing = new OVar(false) -    public buffered = new OVar<BufferRange[]>([]) +    public canplay = new OVar(false)      public buffering_status = new OVar<string | undefined>(undefined)      public error = new OVar<string | undefined>(undefined) @@ -162,13 +167,14 @@ class Player {          this.video.ontimeupdate = () => {              this.position.value = this.video.currentTime              this.update() // TODO maybe not here -            this.update_buffered_ranges()          }          this.video.onplay = () => {              this.buffering_status.value = undefined;          }          this.video.onwaiting = () => { +            console.log("waiting");              this.buffering_status.value = "Buffering..."; +            this.canplay.value = false;          }          this.video.onplaying = () => {              this.playing.value = true; @@ -176,19 +182,12 @@ class Player {          this.video.onpause = () => {              this.playing.value = false          } -        this.fetch_meta() -    } - -    update_buffered_ranges() { -        const o: BufferRange[] = [] -        for (let i = 0; i < this.video.buffered.length; i++) { -            o.push({ -                start: this.video.buffered.start(i), -                end: this.video.buffered.end(i), -                status: "buffered" -            }) +        this.video.oncanplay = () => { +            this.buffering_status.value = undefined +            console.log("canplay"); +            this.canplay.value = true          } -        this.buffered.value = o; +        this.fetch_meta()      }      async fetch_meta() { @@ -200,15 +199,18 @@ class Player {          this.duration.value = metadata.duration          this.video.src = URL.createObjectURL(this.media_source) -        this.media_source.addEventListener("sourceopen", () => { -            this.tracks.push(new PlayerTrack(this.media_source, this.node_id, 0, metadata.tracks[0])) -            this.tracks.push(new PlayerTrack(this.media_source, this.node_id, 1, metadata.tracks[1])) +        this.media_source.addEventListener("sourceopen", async () => { +            this.tracks.value.push(new PlayerTrack(this.media_source, this.node_id, 0, metadata.tracks[0])) +            this.tracks.value.push(new PlayerTrack(this.media_source, this.node_id, 1, metadata.tracks[1])) +            this.tracks.change()              this.buffering_status.value = "Fetching initial segments..."              this.update() +            await this.canplay.wait_for(true) +            this.buffering_status.value = undefined          })      } -    update() { -        this.tracks.forEach(t => t.update(this.video.currentTime)) +    async update(newt?: number) { +        await Promise.all(this.tracks.value.map(t => t.update(newt ?? this.video.currentTime)))      }      play() { @@ -219,61 +221,97 @@ class Player {          console.log("pause");          this.video.pause()      } -    seek(p: number) { +    async seek(p: number) { +        this.pause() +        await this.update(p)          this.video.currentTime = p -        this.update() +        this.play()      }  } +interface AppendRange extends Range { buf: ArrayBuffer, index: number, cb: () => void }  class PlayerTrack {      private source_buffer: SourceBuffer -    private loaded = new Set<number>() +    private current_load?: AppendRange      private loading = new Set<number>() -    private append_queue: (Range & { buf: ArrayBuffer, cb: () => void })[] = [] +    public buffered = new OVar<BufferRange[]>([]) +    private append_queue: AppendRange[] = []      constructor(media_source: MediaSource, private node_id: string, private track_index: number, private metadata: JhlsTrack) {          this.source_buffer = media_source.addSourceBuffer("video/webm")          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.update_buf_ranges()              this.tick_append()          })          this.source_buffer.addEventListener("error", e => {              console.error("sourcebuffer error", e);          })      } + +    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.metadata.segments[r], status: "downloading" }) +        } +        this.buffered.value = ranges +    } +      async update(target: number) { -        console.log(`update track ${this.track_index}`, target, this.loaded, this.loading); +        this.update_buf_ranges() // TODO required? +          for (let i = 0; i < this.metadata.segments.length; i++) { -            const segment = this.metadata.segments[i]; -            if (segment.start >= target - 1 && segment.end < target + TARGET_BUFFER_DURATION) { -                if (!this.loaded.has(i) && !this.loading.has(i)) { -                    this.loading.add(i) -                    this.load(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.start <= target + MIN_BUFFER_DURATION) +                await this.load(i) +            else +                this.load(i)          }      } +    check_buf_collision(start: number, end: number) { +        for (const r of this.buffered.value) +            if (r.end > start && r.start < end) +                return false +        return true +    } +      async load(index: number) { +        this.loading.add(index) +        console.log("load", index);          const res = await fetch(`/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&tracks=${this.track_index}&index=${index}`)          if (!res.ok) throw new Error(`segment fail i=${index} t=${this.track_index}`);          const buf = await res.arrayBuffer() -        this.loading.delete(index) -        this.loaded.add(index)          await new Promise<void>(cb => { -            this.append_queue.push({ buf, ...this.metadata.segments[index], cb }) +            this.append_queue.push({ buf, ...this.metadata.segments[index], index, cb })              this.tick_append()          })      }      tick_append() { -        console.log("tick", this.append_queue); +        // console.log("tick", this.append_queue);          if (this.source_buffer.updating) return          if (this.append_queue.length) {              const seg = this.append_queue[0]; -            console.log("append", this.track_index, seg); +            // console.log("append", this.track_index, seg);              this.source_buffer.timestampOffset = seg.start              this.source_buffer.appendBuffer(seg.buf);              this.append_queue.splice(0, 1) +            this.current_load = seg          }      }  }
\ No newline at end of file | 
