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)
// 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

View File

@ -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

View File

@ -4,46 +4,56 @@ import { Init } from "./init"
export class Source {
sourceBuffer?: SourceBuffer;
mediaSource: MediaSource;
queue: Array<Uint8Array | ArrayBuffer>;
mime: string;
queue: Array<SourceInit | SourceData | SourceTrim>;
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))
// 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
}
}
// 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 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()
} 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
}
appendBuffer(data: Uint8Array | ArrayBuffer) {
if (!this.sourceBuffer || this.sourceBuffer.updating || this.queue.length) {
this.queue.push(data)
} else {
this.sourceBuffer.appendBuffer(data)
}
// 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.updating) {
return
if (this.sourceBuffer && this.sourceBuffer.updating) {
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
// Process the next item in the queue.
const next = this.queue.shift()
if (!next) {
break;
}
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)
// Remove any range larger than 1s.
if (end > start && end - start > 1.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;
}