From 894f26c5af0c2d36a08a7a11154a9ec76bb5d550 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 5 Dec 2022 14:07:31 -0800 Subject: [PATCH] Add support for switching between tracks to the player. --- player/src/player.ts | 1 - player/src/segment.ts | 6 +- player/src/source.ts | 158 ++++++++++++++++++++++++++++++------------ 3 files changed, 117 insertions(+), 48 deletions(-) diff --git a/player/src/player.ts b/player/src/player.ts index 3e68f0e..5eac5d8 100644 --- a/player/src/player.ts +++ b/player/src/player.ts @@ -242,7 +242,6 @@ export class Player { const segment = new Segment(track.source, init, msg.timestamp) // The track is responsible for flushing the segments in order - track.source.initialize(init) track.add(segment) /* TODO I'm not actually sure why this code doesn't work; something trips up the MP4 parser diff --git a/player/src/segment.ts b/player/src/segment.ts index 3e7a63d..09f7649 100644 --- a/player/src/segment.ts +++ b/player/src/segment.ts @@ -100,7 +100,8 @@ export class Segment { mdat.write(stream); } - this.source.appendBuffer(stream.buffer as ArrayBuffer) + this.source.initialize(this.init) + this.source.append(stream.buffer as ArrayBuffer) return this.done } @@ -109,6 +110,9 @@ export class Segment { finish() { this.done = true this.flush() + + // Trim the buffer to 30s long after each segment. + this.source.trim(30) } // Extend the last sample so it reaches the provided timestamp diff --git a/player/src/source.ts b/player/src/source.ts index 90bdd56..dc299a7 100644 --- a/player/src/source.ts +++ b/player/src/source.ts @@ -4,46 +4,56 @@ import { Init } from "./init" export class Source { sourceBuffer?: SourceBuffer; mediaSource: MediaSource; - queue: Array; - mime: string; + queue: Array; + init?: Init; constructor(mediaSource: MediaSource) { this.mediaSource = mediaSource; this.queue = []; - this.mime = ""; } + // (re)initialize the source using the provided init segment. initialize(init: Init) { - if (!this.sourceBuffer) { - this.sourceBuffer = this.mediaSource.addSourceBuffer(init.info.mime) - this.sourceBuffer.addEventListener('updateend', this.flush.bind(this)) - - // Add the init data to the front of the queue - for (let i = init.raw.length - 1; i >= 0; i -= 1) { - this.queue.unshift(init.raw[i]) - } - - this.flush() - } else if (init.info.mime != this.mime) { - this.sourceBuffer.changeType(init.info.mime) - - // Add the init data to the front of the queue - for (let i = init.raw.length - 1; i >= 0; i -= 1) { - this.queue.unshift(init.raw[i]) + // Check if the init segment is already in the queue. + for (let i = this.queue.length - 1; i >= 0; i--) { + if ((this.queue[i] as SourceInit).init == init) { + // Already queued up. + return } } - this.mime = init.info.mime - } - - appendBuffer(data: Uint8Array | ArrayBuffer) { - if (!this.sourceBuffer || this.sourceBuffer.updating || this.queue.length) { - this.queue.push(data) - } else { - this.sourceBuffer.appendBuffer(data) + // Check if the init segment has already been applied. + if (this.init == init) { + return } + + // Add the init segment to the queue so we call addSourceBuffer or changeType + this.queue.push({ + kind: "init", + init: init, + }) + + for (let i = 0; i < init.raw.length; i += 1) { + this.queue.push({ + kind: "data", + data: init.raw[i], + }) + } + + this.flush() } + // Append the segment data to the buffer. + append(data: Uint8Array | ArrayBuffer) { + this.queue.push({ + kind: "data", + data: data, + }) + + this.flush() + } + + // Return the buffered range. buffered() { if (!this.sourceBuffer) { return { length: 0 } @@ -52,30 +62,86 @@ export class Source { return this.sourceBuffer.buffered } + // Delete any media older than x seconds from the buffer. + trim(duration: number) { + this.queue.push({ + kind: "trim", + trim: duration, + }) + + this.flush() + } + + // Flush any queued instructions flush() { - // Check if we have a mime yet - if (!this.sourceBuffer) { - return - } + while (1) { + // Check if the buffer is currently busy. + if (this.sourceBuffer && this.sourceBuffer.updating) { + break; + } - // Check if the buffer is currently busy. - if (this.sourceBuffer.updating) { - return - } + // Process the next item in the queue. + const next = this.queue.shift() + if (!next) { + break; + } - const data = this.queue.shift() - if (data) { - // If there's data in the queue, flush it. - this.sourceBuffer.appendBuffer(data) - } else if (this.sourceBuffer.buffered.length) { - // Otherwise with no data, trim anything older than 30s. - const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1) - 30.0 - const start = this.sourceBuffer.buffered.start(0) + switch (next.kind) { + case "init": + this.init = next.init; - // Remove any range larger than 1s. - if (end > start && end - start > 1.0) { - this.sourceBuffer.remove(start, end) + if (!this.sourceBuffer) { + // Create a new source buffer. + this.sourceBuffer = this.mediaSource.addSourceBuffer(this.init.info.mime) + + // Call flush automatically after each update finishes. + this.sourceBuffer.addEventListener('updateend', this.flush.bind(this)) + } else { + this.sourceBuffer.changeType(next.init.info.mime) + } + + break; + case "data": + if (!this.sourceBuffer) { + throw "failed to call initailize before append" + } + + this.sourceBuffer.appendBuffer(next.data) + + break; + case "trim": + if (!this.sourceBuffer) { + throw "failed to call initailize before trim" + } + + const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1) - next.trim; + const start = this.sourceBuffer.buffered.start(0) + + if (end > start) { + this.sourceBuffer.remove(start, end) + } + + break; + default: + throw "impossible; unknown SourceItem" } } } } + +interface SourceItem {} + +class SourceInit implements SourceItem { + kind!: "init"; + init!: Init; +} + +class SourceData implements SourceItem { + kind!: "data"; + data!: Uint8Array | ArrayBuffer; +} + +class SourceTrim implements SourceItem { + kind!: "trim"; + trim!: number; +} \ No newline at end of file