Skip to content

Commit

Permalink
Add StreamingEngine and Playhead.
Browse files Browse the repository at this point in the history
Closes #101
Closes #186

Change-Id: I8d1a8d6c0f8cfb5abdd81a149318377282b2bea0
  • Loading branch information
Timothy Drews committed Jan 15, 2016
1 parent 736fe9e commit a4ff271
Show file tree
Hide file tree
Showing 10 changed files with 2,955 additions and 85 deletions.
97 changes: 80 additions & 17 deletions lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ goog.provide('shaka.media.MediaSourceEngine');

goog.require('shaka.asserts');
goog.require('shaka.media.TextSourceBuffer');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.PublicPromise');
Expand Down Expand Up @@ -225,26 +226,41 @@ shaka.media.MediaSourceEngine.prototype.init = function(typeConfig) {


/**
* Computes how far ahead of the given timestamp we have buffered.
* Gets the first timestamp in buffer for the given content type.
*
* @param {string} contentType
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferStart = function(contentType) {
return shaka.media.TimeRangesUtils.bufferStart(
this.sourceBuffers_[contentType].buffered);
};


/**
* Gets the last timestamp in buffer for the given content type.
*
* @param {string} contentType
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferEnd = function(contentType) {
return shaka.media.TimeRangesUtils.bufferEnd(
this.sourceBuffers_[contentType].buffered);
};


/**
* Computes how far ahead of the given timestamp is buffered for the given
* content type.
*
* @param {string} contentType
* @param {number} time
* @return {number} in seconds
* @return {number} The amount of time buffered ahead in seconds.
*/
shaka.media.MediaSourceEngine.prototype.bufferedAheadOf =
function(contentType, time) {
// NOTE: On IE11, buffered ranges may show appended data before the associated
// append operation is complete.
var b = this.sourceBuffers_[contentType].buffered;
var fudge = 0.000001; // 1us
// NOTE: The 1us fudge is needed on Safari, where removal up to X may leave a
// range which starts at X + 1us.
for (var i = 0; i < b.length; ++i) {
if (time + fudge >= b.start(i) && time < b.end(i)) {
return b.end(i) - time;
}
}
return 0;
return shaka.media.TimeRangesUtils.bufferedAheadOf(
this.sourceBuffers_[contentType].buffered, time);
};


Expand Down Expand Up @@ -283,6 +299,40 @@ shaka.media.MediaSourceEngine.prototype.remove =
};


/**
* Enqueue an operation to clear the SourceBuffer.
*
* @param {string} contentType
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.clear = function(contentType) {
// Note that not all platforms allow clearing to Number.POSITIVE_INFINITY.
return this.enqueueOperation_(
contentType,
this.remove_.bind(this, contentType, 0, this.mediaSource_.duration));
};


/**
* Sets the timestamp offset for the given content type.
*
* @param {string} contentType
* @param {number} timestampOffset The timestamp offset. Segments which start
* at time t will be inserted at time t + timestampOffset instead. This
* value does not affect segments which have already been inserted.
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.setTimestampOffset = function(
contentType, timestampOffset) {
if (this.sourceBuffers_[contentType].timestampOffset == timestampOffset)
return Promise.resolve();

return this.enqueueOperation_(
contentType,
this.setTimestampOffset_.bind(this, contentType, timestampOffset));
};


/**
* @param {string=} opt_reason Valid reasons are 'network' and 'decode'.
* @return {!Promise}
Expand Down Expand Up @@ -349,6 +399,21 @@ shaka.media.MediaSourceEngine.prototype.remove_ =
};


/**
* Set the SourceBuffer's timestamp offset.
* @param {string} contentType
* @param {number} timestampOffset
* @private
*/
shaka.media.MediaSourceEngine.prototype.setTimestampOffset_ =
function(contentType, timestampOffset) {
this.sourceBuffers_[contentType].timestampOffset = timestampOffset;

// Fake 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};


/**
* @param {string} contentType
* @param {!Event} event
Expand All @@ -371,11 +436,9 @@ shaka.media.MediaSourceEngine.prototype.onError_ =

/**
* @param {string} contentType
* @param {!Event} event
* @private
*/
shaka.media.MediaSourceEngine.prototype.onUpdateEnd_ =
function(contentType, event) {
shaka.media.MediaSourceEngine.prototype.onUpdateEnd_ = function(contentType) {
var operation = this.queues_[contentType][0];
shaka.asserts.assert(operation, 'Spurious updateend event!');
shaka.asserts.assert(!this.sourceBuffers_[contentType].updating,
Expand Down
226 changes: 226 additions & 0 deletions lib/media/playhead.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

goog.provide('shaka.media.Playhead');

goog.require('shaka.asserts');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IDestroyable');



/**
* Creates a Playhead, which manages the video's current time.
*
* The Playhead provides mechanisms for setting the presentation's start time,
* restricting seeking to valid time ranges, and stopping playback for startup
* and re- buffering.
*
* @param {!HTMLVideoElement} video
* @param {!shaka.media.PresentationTimeline} timeline
* @param {number} minBufferTime
* @param {number} startTime The time, in seconds, to start the presentation.
* This time should be within the presentation timeline.
* @param {function(boolean)} onBuffering Called and passed true when stopped
* for buffering; called and passed false when proceeding after buffering.
* @param {function()} onSeek Called when the user agent seeks to a time within
* the presentation timeline.
*
* @constructor
* @struct
* @implements {shaka.util.IDestroyable}
*/
shaka.media.Playhead = function(
video, timeline, minBufferTime, startTime, onBuffering, onSeek) {
/** @private {HTMLVideoElement} */
this.video_ = video;

/** @private {shaka.media.PresentationTimeline} */
this.timeline_ = timeline;

/** @private {number} */
this.minBufferTime_ = minBufferTime;

/**
* The presentation time, in seconds, at which to begin playback.
* @private {number}
*/
this.startTime_ = startTime;

/** @private {?function(boolean)} */
this.onBuffering_ = onBuffering;

/** @private {?function()} */
this.onSeek_ = onSeek;

/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();

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

/** @private {number} */
this.lastPlaybackRate_ = 0;

// Check if the video has already loaded some metadata.
if (video.readyState > 0) {
this.onLoadedMetadata_();
} else {
this.eventManager_.listen(
video, 'loadedmetadata', this.onLoadedMetadata_.bind(this));
}
};


/** @override */
shaka.media.Playhead.prototype.destroy = function() {
var p = this.eventManager_.destroy();
this.eventManager_ = null;

this.video_ = null;
this.timeline_ = null;
this.onBuffering_ = null;
this.onSeek_ = null;

return p;
};


/**
* Gets the playhead's current (logical) position.
*
* @return {number}
*/
shaka.media.Playhead.prototype.getTime = function() {
var time = this.video_.readyState > 0 ?
this.video_.currentTime :
this.startTime_;
// Although we restrict the video's currentTime elsewhere, clamp it here to
// ensure any timing issues (e.g., the user agent seeks and calls this
// function before we receive the 'seeking' event) don't cause us to return a
// time outside the segment availability window.
return this.clampTime_(time);
};


/**
* Stops the playhead for buffering, or resumes the playhead after buffering.
*
* @param {boolean} buffering True to stop the playhead; false to allow it to
* continue.
*/
shaka.media.Playhead.prototype.setBuffering = function(buffering) {
if (buffering && !this.buffering_) {
this.lastPlaybackRate_ = this.video_.playbackRate;
this.video_.playbackRate = 0;
this.buffering_ = true;
this.onBuffering_(true);
} else if (!buffering && this.buffering_) {
if (this.video_.playbackRate == 0) {
// The app hasn't set a new playback rate, so restore the old one.
this.video_.playbackRate = this.lastPlaybackRate_;
} else {
// There's nothing we could have done to stop the app from setting a new
// rate, so we don't need to do anything here.
}
this.buffering_ = false;
this.onBuffering_(false);
}
};


/**
* Handles a 'loadedmetadata' event.
*
* @private
*/
shaka.media.Playhead.prototype.onLoadedMetadata_ = function() {
var video = /** @type {!HTMLVideoElement} */(this.video_);
this.eventManager_.unlisten(video, 'loadedmetadata');
this.eventManager_.listen(video, 'seeking', this.onSeeking_.bind(this));

// Trigger call to onSeeking_().
this.video_.currentTime = this.clampTime_(this.startTime_);
};


/**
* Handles a 'seeking' event.
*
* @private
*/
shaka.media.Playhead.prototype.onSeeking_ = function() {
shaka.asserts.assert(this.video_.readyState > 0,
'readyState should be greater than 0');

var currentTime = this.video_.currentTime;
var targetTime = currentTime;

var d = this.timeline_.getSegmentAvailabilityDuration();
var live = (d != null) && (d < Number.POSITIVE_INFINITY);

var start = this.timeline_.getSegmentAvailabilityStart();
var end = this.timeline_.getSegmentAvailabilityEnd();

if (!live && (currentTime < start)) {
targetTime = start;
} else if (live && (currentTime < start + this.minBufferTime_)) {
targetTime = Math.max(currentTime, start);
var bufferedAhead = shaka.media.TimeRangesUtils.bufferedAheadOf(
this.video_.buffered, targetTime);
if (bufferedAhead == 0) {
// The playhead is in an unbuffered region, so buffering will be
// required at the seek target; since the segment availability window
// is moving, we cannot seek to the seek target exactly; otherwise, we
// would fall outside the segment availability window again... so seek
// a bit ahead of the seek target.
targetTime = this.clampTime_(start + this.minBufferTime_);
}
} else if (currentTime > end) {
targetTime = end;
}

if (targetTime != currentTime) {
shaka.log.debug('Cannot seek outside segment availability window.');
// Triggers another call to onSeeking_().
this.video_.currentTime = targetTime;
return;
}

this.onSeek_();
};


/**
* Clamps the given time to the segment availability window.
*
* @param {number} time The time in seconds.
* @return {number} The clamped time in seconds.
* @private
*/
shaka.media.Playhead.prototype.clampTime_ = function(time) {
var start = this.timeline_.getSegmentAvailabilityStart();
if (time < start) return start;

var end = this.timeline_.getSegmentAvailabilityEnd();
if (time > end) return end;

return time;
};

0 comments on commit a4ff271

Please sign in to comment.