avmerge: Close streams to avoid errors while changing quality

If a fetchRange network request finished after the quality was
changed, there would be a "InvalidStateError: An attempt was made
to use an object that is not, or is no longer, usable" because
appendSegment was trying to append to the sourceBuffer that was
unusable after the video src was changed to a new mediaSource.

Adds a close method to the AVMerge class to properly clean and
close everything so these sorts of errors won't happen.

Signed-off-by: Jesús <heckyel@hyperbola.info>
This commit is contained in:
James Taylor 2021-08-24 22:19:49 -07:00 committed by Jesús
parent d942883c78
commit fa3b78583f
No known key found for this signature in database
GPG Key ID: F6EE7BC59A315766
2 changed files with 41 additions and 8 deletions

View File

@ -17,7 +17,6 @@
// SourceBuffer data limits: // SourceBuffer data limits:
// https://developers.google.com/web/updates/2017/10/quotaexceedederror // https://developers.google.com/web/updates/2017/10/quotaexceedederror
// TODO: AVMerge.close()
// TODO: close stream at end? // TODO: close stream at end?
// TODO: Better buffering algorithm // TODO: Better buffering algorithm
// TODO: Call abort to cancel in-progress appends? // TODO: Call abort to cancel in-progress appends?
@ -61,10 +60,19 @@ AVMerge.prototype.sourceOpen = function(_) {
this.videoStream.setup(); this.videoStream.setup();
this.audioStream.setup(); this.audioStream.setup();
this.video.ontimeupdate = this.checkBothBuffers.bind(this); this.timeUpdateEvt = addEvent(this.video, 'timeupdate',
this.video.onseeking = debounce(this.seek.bind(this), 500); this.checkBothBuffers.bind(this));
this.seekingEvt = addEvent(this.video, 'seeking',
debounce(this.seek.bind(this), 500));
//this.video.onseeked = function() {console.log('seeked')}; //this.video.onseeked = function() {console.log('seeked')};
} }
AVMerge.prototype.close = function() {
this.videoStream.close();
this.audioStream.close();
this.timeUpdateEvt.remove();
this.seekingEvt.remove();
this.mediaSource.endOfStream();
}
AVMerge.prototype.checkBothBuffers = function() { AVMerge.prototype.checkBothBuffers = function() {
this.audioStream.checkBuffer(); this.audioStream.checkBuffer();
this.videoStream.checkBuffer(); this.videoStream.checkBuffer();
@ -85,6 +93,7 @@ function Stream(avMerge, source, startTime) {
this.avMerge = avMerge; this.avMerge = avMerge;
this.video = avMerge.video; this.video = avMerge.video;
this.url = source['url']; this.url = source['url'];
this.closed = false;
this.mimeCodec = source['mime_codec'] this.mimeCodec = source['mime_codec']
this.streamType = source['acodec'] ? 'audio' : 'video'; this.streamType = source['acodec'] ? 'audio' : 'video';
if (this.streamType == 'audio') { if (this.streamType == 'audio') {
@ -106,8 +115,7 @@ function Stream(avMerge, source, startTime) {
this.sourceBuffer.addEventListener('error', (e) => { this.sourceBuffer.addEventListener('error', (e) => {
this.reportError('sourceBuffer error', e); this.reportError('sourceBuffer error', e);
}); });
this.sourceBuffer.addEventListener('updateend', (e) => { this.updateendEvt = addEvent(this.sourceBuffer, 'updateend', (e) => {
this.reportDebug('updateend', e);
if (this.appendQueue.length != 0) { if (this.appendQueue.length != 0) {
this.appendSegment(...this.appendQueue.pop()); this.appendSegment(...this.appendQueue.pop());
} }
@ -148,12 +156,20 @@ Stream.prototype.setup = async function(){
Stream.prototype.setupSegments = async function(sidxBox){ Stream.prototype.setupSegments = async function(sidxBox){
var box = unbox(sidxBox); var box = unbox(sidxBox);
this.sidx = sidx_parse(box.data, this.indexRange.end+1); this.sidx = sidx_parse(box.data, this.indexRange.end+1);
this.reportDebug('sidx', this.sidx);
this.reportDebug('appending first segment');
this.fetchSegmentIfNeeded(this.getSegmentIdx(this.startTime)); this.fetchSegmentIfNeeded(this.getSegmentIdx(this.startTime));
} }
Stream.prototype.close = function() {
// Prevents appendSegment adding to buffer if request finishes
// after closing
this.closed = true;
this.sourceBuffer.abort();
this.updateendEvt.remove();
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
}
Stream.prototype.appendSegment = function(segmentIdx, chunk) { Stream.prototype.appendSegment = function(segmentIdx, chunk) {
if (this.closed)
return;
// cannot append right now, schedule for updateend // cannot append right now, schedule for updateend
if (this.sourceBuffer.updating) { if (this.sourceBuffer.updating) {
this.reportDebug('sourceBuffer updating, queueing for later'); this.reportDebug('sourceBuffer updating, queueing for later');
@ -306,6 +322,7 @@ Stream.prototype.reportError = function(...args) {
// Utility functions // Utility functions
function fetchRange(url, start, end, cb) { function fetchRange(url, start, end, cb) {
reportDebug('fetchRange', start, end); reportDebug('fetchRange', start, end);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -342,6 +359,20 @@ function clamp(number, min, max) {
return Math.max(min, Math.min(number, max)); return Math.max(min, Math.min(number, max));
} }
// allow to remove an event listener without having a function reference
function RegisteredEvent(obj, eventName, func) {
this.obj = obj;
this.eventName = eventName;
this.func = func;
obj.addEventListener(eventName, func);
}
RegisteredEvent.prototype.remove = function() {
this.obj.removeEventListener(this.eventName, this.func);
}
function addEvent(obj, eventName, func) {
return new RegisteredEvent(obj, eventName, func);
}
function reportWarning(...args){ function reportWarning(...args){
console.log(...args); console.log(...args);
} }

View File

@ -119,6 +119,8 @@
var videoPaused = video.paused; var videoPaused = video.paused;
var videoSpeed = video.playbackRate; var videoSpeed = video.playbackRate;
var videoSource; var videoSource;
if (avMerge)
avMerge.close();
if (selection.type == 'uni'){ if (selection.type == 'uni'){
videoSource = data['uni_sources'][selection.index]; videoSource = data['uni_sources'][selection.index];
video.src = videoSource.url; video.src = videoSource.url;