Add support for switching between tracks to the player.

This commit is contained in:
Luke Curley 2022-12-05 14:07:31 -08:00
parent 2608baac33
commit 894f26c5af
3 changed files with 117 additions and 48 deletions

View File

@ -242,7 +242,6 @@ export class Player {
const segment = new Segment(track.source, init, msg.timestamp) const segment = new Segment(track.source, init, msg.timestamp)
// The track is responsible for flushing the segments in order // The track is responsible for flushing the segments in order
track.source.initialize(init)
track.add(segment) track.add(segment)
/* TODO I'm not actually sure why this code doesn't work; something trips up the MP4 parser /* TODO I'm not actually sure why this code doesn't work; something trips up the MP4 parser

View File

@ -100,7 +100,8 @@ export class Segment {
mdat.write(stream); 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 return this.done
} }
@ -109,6 +110,9 @@ export class Segment {
finish() { finish() {
this.done = true this.done = true
this.flush() 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 // Extend the last sample so it reaches the provided timestamp

View File

@ -4,46 +4,56 @@ import { Init } from "./init"
export class Source { export class Source {
sourceBuffer?: SourceBuffer; sourceBuffer?: SourceBuffer;
mediaSource: MediaSource; mediaSource: MediaSource;
queue: Array<Uint8Array | ArrayBuffer>; queue: Array<SourceInit | SourceData | SourceTrim>;
mime: string; init?: Init;
constructor(mediaSource: MediaSource) { constructor(mediaSource: MediaSource) {
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
this.queue = []; this.queue = [];
this.mime = "";
} }
// (re)initialize the source using the provided init segment.
initialize(init: Init) { initialize(init: Init) {
if (!this.sourceBuffer) { // Check if the init segment is already in the queue.
this.sourceBuffer = this.mediaSource.addSourceBuffer(init.info.mime) for (let i = this.queue.length - 1; i >= 0; i--) {
this.sourceBuffer.addEventListener('updateend', this.flush.bind(this)) if ((this.queue[i] as SourceInit).init == init) {
// Already queued up.
return
}
}
// Add the init data to the front of the queue // Check if the init segment has already been applied.
for (let i = init.raw.length - 1; i >= 0; i -= 1) { if (this.init == init) {
this.queue.unshift(init.raw[i]) 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() 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])
}
} }
this.mime = init.info.mime // Append the segment data to the buffer.
} append(data: Uint8Array | ArrayBuffer) {
this.queue.push({
appendBuffer(data: Uint8Array | ArrayBuffer) { kind: "data",
if (!this.sourceBuffer || this.sourceBuffer.updating || this.queue.length) { data: data,
this.queue.push(data) })
} else {
this.sourceBuffer.appendBuffer(data) this.flush()
}
} }
// Return the buffered range.
buffered() { buffered() {
if (!this.sourceBuffer) { if (!this.sourceBuffer) {
return { length: 0 } return { length: 0 }
@ -52,30 +62,86 @@ export class Source {
return this.sourceBuffer.buffered 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() { flush() {
// Check if we have a mime yet while (1) {
if (!this.sourceBuffer) {
return
}
// Check if the buffer is currently busy. // Check if the buffer is currently busy.
if (this.sourceBuffer.updating) { if (this.sourceBuffer && this.sourceBuffer.updating) {
return break;
} }
const data = this.queue.shift() // Process the next item in the queue.
if (data) { const next = this.queue.shift()
// If there's data in the queue, flush it. if (!next) {
this.sourceBuffer.appendBuffer(data) break;
} 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 switch (next.kind) {
case "init":
this.init = next.init;
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) const start = this.sourceBuffer.buffered.start(0)
// Remove any range larger than 1s. if (end > start) {
if (end > start && end - start > 1.0) {
this.sourceBuffer.remove(start, end) 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;
}