Skip to content

Commit

Permalink
Merge pull request #8 from gesinger/transmux-before-append-lhls-sourc…
Browse files Browse the repository at this point in the history
…e-updater-duration

Use source updater to update media source duration
  • Loading branch information
gesinger authored Oct 9, 2018
2 parents ddf6e11 + 571728a commit 24f1e3b
Show file tree
Hide file tree
Showing 5 changed files with 366 additions and 145 deletions.
9 changes: 5 additions & 4 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export class MasterPlaylistController extends videojs.EventTarget {

this.mediaSource = new window.MediaSource();

this.mediaSource.addEventListener('durationchange', () => {
this.tech_.trigger('durationchange');
});
// load the media source into the player
this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_.bind(this));
Expand Down Expand Up @@ -996,8 +999,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
// duration for live is actually a strategy used by some players to work around the
// issue of live seekable ranges cited above.
this.mediaSource.duration < seekable.end(seekable.length - 1)) {
this.mediaSource.duration = seekable.end(seekable.length - 1);
this.tech_.trigger('durationchange');
this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1));
}
}

Expand All @@ -1010,8 +1012,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
}

if (this.mediaSource.duration !== duration) {
this.mediaSource.duration = duration;
this.tech_.trigger('durationchange');
this.sourceUpdater_.setDuration(duration);
}
}

Expand Down
231 changes: 168 additions & 63 deletions src/source-updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,54 +24,133 @@ const actions = {
},
callback: (callback) => (type, sourceUpdater) => {
callback();
},
duration: (duration) => (sourceUpdater) => {
sourceUpdater.mediaSource.duration = duration;
}
};

const updating = (type, sourceUpdater) => {
const sourceBuffer = sourceUpdater[`${type}Buffer`];

return (sourceBuffer && sourceBuffer.updating) || sourceUpdater.queue[type].pending;
return (sourceBuffer && sourceBuffer.updating) || sourceUpdater.queuePending[type];
};

const shiftQueue = (type, sourceUpdater) => {
const queue = sourceUpdater.queue[type];
const nextQueueIndexOfType = (type, queue) => {
for (let i = 0; i < queue.length; i++) {
const queueEntry = queue[i];

if (queueEntry.type === 'mediaSource') {
// If the next entry is a media source entry (uses multiple source buffers), block
// processing to allow it to go through first.
return null;
}

if (queueEntry.type === type) {
return i;
}
}

if (updating(type, sourceUpdater) || !queue.actions.length || !sourceUpdater.started_) {
return null;
};

const shiftQueue = (type, sourceUpdater) => {
if (sourceUpdater.queue.length === 0) {
return;
}

const action = queue.actions.shift();
let queueIndex = 0;
let queueEntry = sourceUpdater.queue[queueIndex];

queue.pending = action[1];
action[0](type, sourceUpdater);
};
if (queueEntry.type === 'mediaSource') {
if (!updating('audio', sourceUpdater) && !updating('video', sourceUpdater)) {
sourceUpdater.queue.shift();
queueEntry.action(sourceUpdater);

const pushQueue = (type, sourceUpdater, action) => {
const queue = sourceUpdater.queue[type];
if (queueEntry.doneFn) {
queueEntry.doneFn();
}

queue.actions.push(action);
shiftQueue(type, sourceUpdater);
};
// Only specific source buffer actions must wait for async updateend events. Media
// Source actions process synchronously. Therefore, both audio and video source
// buffers are now clear to process the next queue entries.
shiftQueue('audio', sourceUpdater);
shiftQueue('video', sourceUpdater);
}

const onUpdateend = (type, sourceUpdater) => () => {
const queue = sourceUpdater.queue[type];
// Media Source actions require both source buffers, so if the media source action
// couldn't process yet (because one or both source buffers are busy), block other
// queue actions until both are available and the media source action can process.
return;
}

if (!queue.pending) {
shiftQueue(type, sourceUpdater);
if (type === 'mediaSource') {
// If the queue was shifted by a media source action (this happens when pushing a
// media source action onto the queue), then it wasn't from an updateend event from an
// audio or video source buffer, so there's no change from previous state, and no
// processing should be done.
return;
}

if (!queue.pending) {
// nothing in the queue
// Media source queue entries don't need to consider whether the source updater is
// started (i.e., source buffers are created) as they don't need the source buffers, but
// source buffer queue entries do.
if (!sourceUpdater.started_ || updating(type, sourceUpdater)) {
return;
}

const doneFn = queue.pending.doneFn;
if (queueEntry.type !== type) {
queueIndex = nextQueueIndexOfType(type, sourceUpdater.queue);

queue.pending = null;
if (queueIndex === null) {
// Either there's no queue entry that uses this source buffer type in the queue, or
// there's a media source queue entry before the next entry of this type, in which
// case wait for that action to process first.
return;
}

queueEntry = sourceUpdater.queue[queueIndex];
}

sourceUpdater.queue.splice(queueIndex, 1);
queueEntry.action(type, sourceUpdater);

if (!queueEntry.doneFn) {
// synchronous operation, process next entry
shiftQueue(type, sourceUpdater);
return;
}

if (doneFn) {
// if there's an error, report it
doneFn(sourceUpdater[`${type}Error_`]);
// asynchronous operation, so keep a record that this source buffer type is in use
sourceUpdater.queuePending[type] = queueEntry;
};

const pushQueue = ({type, sourceUpdater, action, doneFn, name}) => {
sourceUpdater.queue.push({
type,
action,
doneFn,
name
});
shiftQueue(type, sourceUpdater);
};

const onUpdateend = (type, sourceUpdater) => (e) => {
// Although there should, in theory, be a pending action for any updateend receieved,
// there are some actions that may trigger updateend events without set definitions in
// the w3c spec. For instance, setting the duration on the media source may trigger
// updateend events on source buffers. This does not appear to be in the spec. As such,
// if we encounter an updateend without a corresponding pending action from our queue
// for that source buffer type, process the next action.
if (sourceUpdater.queuePending[type]) {
const doneFn = sourceUpdater.queuePending[type].doneFn;

sourceUpdater.queuePending[type] = null;

if (doneFn) {
// if there's an error, report it
doneFn(sourceUpdater[`${type}Error_`]);
}
}

shiftQueue(type, sourceUpdater);
Expand All @@ -95,15 +174,10 @@ export default class SourceUpdater extends videojs.EventTarget {
// initial timestamp offset is 0
this.audioTimestampOffset_ = 0;
this.videoTimestampOffset_ = 0;
this.queue = {
audio: {
actions: [],
doneFn: null
},
video: {
actions: [],
doneFn: null
}
this.queue = [];
this.queuePending = {
audio: null,
video: null
};
}

Expand Down Expand Up @@ -171,10 +245,13 @@ export default class SourceUpdater extends videojs.EventTarget {
*/
appendBuffer(type, bytes, doneFn) {
this.processedAppend_ = true;
pushQueue(type, this, [
actions.appendBuffer(bytes),
{ doneFn, name: 'appendBuffer' }
]);
pushQueue({
type,
sourceUpdater: this,
action: actions.appendBuffer(bytes),
doneFn,
name: 'appendBuffer'
});
}

audioBuffered() {
Expand All @@ -191,6 +268,20 @@ export default class SourceUpdater extends videojs.EventTarget {
return buffered(this.videoBuffer, this.audioBuffer);
}

setDuration(duration, doneFn = noop) {
// In order to set the duration on the media source, it's necessary to wait for all
// source buffers to no longer be updating. "If the updating attribute equals true on
// any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and
// abort these steps." (source: https://www.w3.org/TR/media-source/#attributes).
pushQueue({
type: 'mediaSource',
sourceUpdater: this,
action: actions.duration(duration),
name: 'duration',
doneFn
});
}

/**
* Queue an update to remove a time range from the buffer.
*
Expand All @@ -206,10 +297,13 @@ export default class SourceUpdater extends videojs.EventTarget {
return;
}

pushQueue('audio', this, [
actions.remove(start, end),
{ doneFn: done, name: 'remove' }
]);
pushQueue({
type: 'audio',
sourceUpdater: this,
action: actions.remove(start, end),
doneFn: done,
name: 'remove'
});
}

/**
Expand All @@ -227,10 +321,13 @@ export default class SourceUpdater extends videojs.EventTarget {
return;
}

pushQueue('video', this, [
actions.remove(start, end),
{ doneFn: done, name: 'remove' }
]);
pushQueue({
type: 'video',
sourceUpdater: this,
action: actions.remove(start, end),
doneFn: done,
name: 'remove'
});
}

/**
Expand Down Expand Up @@ -260,11 +357,13 @@ export default class SourceUpdater extends videojs.EventTarget {
if (typeof offset !== 'undefined' &&
this.audioBuffer &&
// no point in updating if it's the same
this.audioBuffer.timestampOffset !== offset) {
pushQueue('audio', this, [
actions.timestampOffset(offset),
null
]);
this.audioTimestampOffset_ !== offset) {
pushQueue({
type: 'audio',
sourceUpdater: this,
action: actions.timestampOffset(offset),
name: 'timestampOffset'
});
this.audioTimestampOffset_ = offset;
}
return this.audioTimestampOffset_;
Expand All @@ -279,31 +378,37 @@ export default class SourceUpdater extends videojs.EventTarget {
if (typeof offset !== 'undefined' &&
this.videoBuffer &&
// no point in updating if it's the same
this.videoBuffer.timestampOffset !== offset) {
pushQueue('video', this, [
actions.timestampOffset(offset),
null
]);
this.videoTimestampOffset !== offset) {
pushQueue({
type: 'video',
sourceUpdater: this,
action: actions.timestampOffset(offset),
name: 'timestampOffset'
});
this.videoTimestampOffset_ = offset;
}
return this.videoTimestampOffset_;
}

audioQueueCallback(callback) {
if (this.audioBuffer) {
pushQueue('audio', this, [
actions.callback(callback),
null
]);
pushQueue({
type: 'audio',
sourceUpdater: this,
action: actions.callback(callback),
name: 'callback'
});
}
}

videoQueueCallback(callback) {
if (this.videoBuffer) {
pushQueue('video', this, [
actions.callback(callback),
null
]);
pushQueue({
type: 'video',
sourceUpdater: this,
action: actions.callback(callback),
name: 'callback'
});
}
}

Expand Down
Loading

0 comments on commit 24f1e3b

Please sign in to comment.