Skip to content

Commit

Permalink
Add side-loading external text tracks.
Browse files Browse the repository at this point in the history
Now can add text tracks to the manifest after it is loaded.

Closes #206

Change-Id: If7afc51e0690eb900ead35d26142160756b9b48c
  • Loading branch information
TheModMaker committed Mar 21, 2016
1 parent b9e85d2 commit aa27d60
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 42 deletions.
119 changes: 78 additions & 41 deletions lib/media/streaming_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,47 +327,11 @@ shaka.media.StreamingEngine.prototype.init = function() {
return;
}

// Init MediaSourceEngine.
var typeConfig = {};

for (var type in streamsByType) {
var stream = streamsByType[type];
typeConfig[type] =
stream.mimeType +
(stream.codecs ? '; codecs="' + stream.codecs + '"' : '');
}

this.mediaSourceEngine_.init(typeConfig);
this.setDuration_();

// Setup the initial set of Streams and then begin each update cycle. After
// startup completes onUpdate_() will set up the remaining Periods.
// TODO: Use MapUtils.
var streams = Object.keys(streamsByType)
.map(function(type) { return streamsByType[type]; });
this.setupStreams_(streams).then(function() {
this.initStreams_(streamsByType).then(function() {
shaka.log.debug('init: completed initial Stream setup');

for (var type in streamsByType) {
var stream = streamsByType[type];
this.mediaStates_[type] = {
stream: stream,
type: type,
lastSegmentPeriodIndex: null,
lastSegmentReference: null,
drift: null,
needInitSegment: true,
needRebuffering: false,
needPeriodIndex: needPeriodIndex,
endOfStream: false,
performingUpdate: false,
updateTimer: null,
waitingToClearBuffer: false,
clearingBuffer: false
};
this.scheduleUpdate_(this.mediaStates_[type], 0);
}

// Subtlety: onInitialStreamsSetup_() may call switch() or seeked(), so we
// must schedule an update beforehand so |updateTimer| is set.
if (this.onInitialStreamsSetup_) {
Expand Down Expand Up @@ -409,6 +373,23 @@ shaka.media.StreamingEngine.prototype.getActiveStreams = function() {
};


/**
* Notifies StreamingEngine that a new stream was added to the manifest. This
* initializes the given stream. This returns a Promise that resolves when
* the stream has been set up.
*
* @param {string} type
* @param {shakaExtern.Stream} stream
* @return {!Promise}
*/
shaka.media.StreamingEngine.prototype.notifyNewStream = function(type, stream) {
/** @type {!Object.<string, shakaExtern.Stream>} */
var streamsByType = {};
streamsByType[type] = stream;
return this.initStreams_(streamsByType);
};


/**
* Switches to the given Stream. |stream| may be from any StreamSet or any
* Period.
Expand Down Expand Up @@ -539,6 +520,66 @@ shaka.media.StreamingEngine.prototype.seeked = function() {
};


/**
* Initializes the given streams and media states if required. This will
* schedule updates for the given types.
*
* @param {!Object.<string, shakaExtern.Stream>} streamsByType
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.initStreams_ = function(streamsByType) {
goog.asserts.assert(this.config_,
'StreamingEngine configure() must be called before init()!');

// Determine which Period we must buffer.
var playheadTime = this.playhead_.getTime();
var needPeriodIndex = this.findPeriodContainingTime_(playheadTime);

// Init MediaSourceEngine.
var typeConfig = {};

for (var type in streamsByType) {
var stream = streamsByType[type];
typeConfig[type] =
stream.mimeType +
(stream.codecs ? '; codecs="' + stream.codecs + '"' : '');
}

this.mediaSourceEngine_.init(typeConfig);
this.setDuration_();

// Setup the initial set of Streams and then begin each update cycle. After
// startup completes onUpdate_() will set up the remaining Periods.
// TODO: Use MapUtils.
var streams = Object.keys(streamsByType)
.map(function(type) { return streamsByType[type]; });
return this.setupStreams_(streams).then(function() {
for (var type in streamsByType) {
var stream = streamsByType[type];
if (!this.mediaStates_[type]) {
this.mediaStates_[type] = {
stream: stream,
type: type,
lastSegmentPeriodIndex: null,
lastSegmentReference: null,
drift: null,
needInitSegment: true,
needRebuffering: false,
needPeriodIndex: needPeriodIndex,
endOfStream: false,
performingUpdate: false,
updateTimer: null,
waitingToClearBuffer: false,
clearingBuffer: false
};
}
this.scheduleUpdate_(this.mediaStates_[type], 0);
}
}.bind(this));
};


/**
* Sets up the given Period if necessary. Calls onError_() if an error
* occurs.
Expand Down Expand Up @@ -1260,10 +1301,6 @@ shaka.media.StreamingEngine.prototype.handleDrift_ = function(
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];

goog.asserts.assert(
!this.startupComplete_,
logPrefix + ' startup should not be complete');

var bufferStart = this.mediaSourceEngine_.bufferStart(mediaState.type);
if (bufferStart == null) {
// The segment did not contain any actual media content.
Expand Down
98 changes: 97 additions & 1 deletion lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.MediaSourceEngine');
goog.require('shaka.media.Playhead');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.media.StreamingEngine');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
Expand Down Expand Up @@ -93,6 +94,13 @@ shaka.Player = function(video) {
/** @private {?shakaExtern.Manifest} */
this.manifest_ = null;

/**
* Contains an ID for use with creating streams. The manifest parser should
* start with small IDs, so this starts with a large one.
* @private {number}
*/
this.nextExternalStreamId_ = 1e9;

/** @private {boolean} */
this.buffering_ = false;

Expand Down Expand Up @@ -601,7 +609,95 @@ shaka.Player.prototype.getStats = function() {
};


// TODO: API for external captions
/**
* Adds the given text track to the current Period. Load() must resolve before
* calling. The current Period or the presentation must have a duration. This
* returns a Promise that will resolve when the track can be switched to and
* will resolve with the track that was created.
*
* @param {string} uri
* @param {string} language
* @param {string} kind
* @param {string} mime
* @param {string=} opt_codec
* @return {!Promise.<shakaExtern.Track>}
* @export
*/
shaka.Player.prototype.addTextTrack = function(
uri, language, kind, mime, opt_codec) {
if (!this.manifest_) {
shaka.log.error(
'Must call load() and wait for it to resolve before adding text ' +
'tracks.');
return Promise.reject();
}

// Get the Period duration.
var period = this.streamingEngine_.getCurrentPeriod();
/** @type {number} */
var periodDuration;
for (var i = 0; i < this.manifest_.periods.length; i++) {
if (this.manifest_.periods[i] == period) {
if (i == this.manifest_.periods.length - 1) {
periodDuration = this.manifest_.presentationTimeline.getDuration() -
period.startTime;
if (periodDuration == Number.POSITIVE_INFINITY) {
shaka.log.error(
'The current Period or the presentation must have a duration ' +
'to add external text tracks.');
return Promise.reject();
}
} else {
var nextPeriod = this.manifest_.periods[i + 1];
periodDuration = nextPeriod.startTime - period.startTime;
}
break;
}
}

/** @type {shakaExtern.Stream} */
var stream = {
id: this.nextExternalStreamId_++,
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: function(time) { return 1; },
getSegmentReference: function(ref) {
if (ref != 1) return null;
return new shaka.media.SegmentReference(
1, 0, periodDuration, [uri], 0, null);
},
initSegmentReference: null,
presentationTimeOffset: 0,
mimeType: mime,
codecs: opt_codec || '',
bandwidth: 0,
kind: kind,
keyId: null
};
/** @type {shakaExtern.StreamSet} */
var streamSet = {
language: language,
type: 'text',
primary: true,
drmInfos: [],
streams: [stream]
};

return this.streamingEngine_.notifyNewStream('text', stream).then(function() {
// Only add the stream once it has been initialized. This ensures that
// calls to getTracks do not return the uninitialized stream.
period.streamSets.push(streamSet);
return {
id: stream.id,
active: false,
type: 'text',
bandwidth: 0,
language: language,
kind: kind,
width: null,
height: null
};
});
};


/**
Expand Down

0 comments on commit aa27d60

Please sign in to comment.