Merge pull request #17 from yokuze/fix_no_preload_no_resume_play_16

Refs #16 Fix video does not resume playing after quality change
This commit is contained in:
Ethan Smith 2018-01-09 16:20:37 -05:00 committed by GitHub
commit 94a9648b03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 118 additions and 7 deletions

View file

@ -3,6 +3,12 @@
In general, this project adheres to [Semantic Versioning](http://semver.org/). If for some In general, this project adheres to [Semantic Versioning](http://semver.org/). If for some
reason we do something that's not strictly semantic, it will be clearly called out below. reason we do something that's not strictly semantic, it will be clearly called out below.
## 1.1.2
* Fixed a bug where selecting a quality menu item while a video was playing did not resume
playback after the source changed. Affected Safari and players whose `preload` attribute
was `none` (8feeafb Fixes #16).
## 1.1.1 ## 1.1.1
* Reference underscore as a dependency since we depend on it (931d8a4 See #12) * Reference underscore as a dependency since we depend on it (931d8a4 See #12)

View file

@ -26,7 +26,6 @@
"homepage": "https://github.com/silvermine/videojs-quality-selector#readme", "homepage": "https://github.com/silvermine/videojs-quality-selector#readme",
"devDependencies": { "devDependencies": {
"autoprefixer": "7.1.1", "autoprefixer": "7.1.1",
"class.extend": "0.9.2",
"coveralls": "2.13.1", "coveralls": "2.13.1",
"eslint": "4.0.0", "eslint": "4.0.0",
"eslint-config-silvermine": "1.3.0", "eslint-config-silvermine": "1.3.0",
@ -51,6 +50,7 @@
"video.js": "6.x" "video.js": "6.x"
}, },
"dependencies": { "dependencies": {
"class.extend": "0.9.2",
"underscore": "1.8.3" "underscore": "1.8.3"
} }
} }

View file

@ -3,7 +3,8 @@
var _ = require('underscore'), var _ = require('underscore'),
events = require('./events'), events = require('./events'),
qualitySelectorFactory = require('./components/QualitySelector'), qualitySelectorFactory = require('./components/QualitySelector'),
sourceInterceptorFactory = require('./middleware/SourceInterceptor'); sourceInterceptorFactory = require('./middleware/SourceInterceptor'),
SafeSeek = require('./util/SafeSeek');
module.exports = function(videojs) { module.exports = function(videojs) {
videojs = videojs || window.videojs; videojs = videojs || window.videojs;
@ -12,8 +13,7 @@ module.exports = function(videojs) {
sourceInterceptorFactory(videojs); sourceInterceptorFactory(videojs);
videojs.hook('setup', function(player) { videojs.hook('setup', function(player) {
// Add handler to switch sources when the user requests a change function changeQuality(event, newSource) {
player.on(events.QUALITY_REQUESTED, function(event, newSource) {
var sources = player.currentSources(), var sources = player.currentSources(),
currentTime = player.currentTime(), currentTime = player.currentTime(),
isPaused = player.paused(), isPaused = player.paused(),
@ -29,15 +29,36 @@ module.exports = function(videojs) {
// following updates the original object in `sources`. // following updates the original object in `sources`.
selectedSource.selected = true; selectedSource.selected = true;
if (player._qualitySelectorSafeSeek) {
player._qualitySelectorSafeSeek.onQualitySelectionChange();
}
player.src(sources); player.src(sources);
player.one('loadeddata', function() { player.ready(function() {
player.currentTime(currentTime); if (!player._qualitySelectorSafeSeek || player._qualitySelectorSafeSeek.hasFinished()) {
// Either we don't have a pending seek action or the one that we have is no
// longer applicable. This block must be within a `player.ready` callback
// because the call to `player.src` above is asynchronous, and so not
// having it within this `ready` callback would cause the SourceInterceptor
// to execute after this block instead of before.
//
// We save the `currentTime` within the SafeSeek instance because if
// multiple QUALITY_REQUESTED events are received before the SafeSeek
// operation finishes, the player's `currentTime` will be `0` if the
// player's `src` is updated but the player's `currentTime` has not yet
// been set by the SafeSeek operation.
player._qualitySelectorSafeSeek = new SafeSeek(player, currentTime);
}
if (!isPaused) { if (!isPaused) {
player.play(); player.play();
} }
}); });
}); }
// Add handler to switch sources when the user requests a change
player.on(events.QUALITY_REQUESTED, changeQuality);
}); });
}; };

View file

@ -13,6 +13,10 @@ module.exports = function(videojs) {
var sources = player.currentSources(), var sources = player.currentSources(),
userSelectedSource, chosenSource; userSelectedSource, chosenSource;
if (player._qualitySelectorSafeSeek) {
player._qualitySelectorSafeSeek.onPlayerSourcesChange();
}
// There are generally two source options, the one that videojs // There are generally two source options, the one that videojs
// auto-selects and the one that a "user" of this plugin has // auto-selects and the one that a "user" of this plugin has
// supplied via the `selected` property. `selected` can come from // supplied via the `selected` property. `selected` can come from

80
src/js/util/SafeSeek.js Normal file
View file

@ -0,0 +1,80 @@
'use strict';
var Class = require('class.extend');
module.exports = Class.extend({
init: function(player, seekToTime) {
this._player = player;
this._seekToTime = seekToTime;
this._hasFinished = false;
this._keepThisInstanceWhenPlayerSourcesChange = false;
this._seekWhenSafe();
},
_seekWhenSafe: function() {
var HAVE_FUTURE_DATA = 3;
// `readyState` in Video.js is the same as the HTML5 Media element's `readyState`
// property.
//
// `readyState` is an enum of 5 values (0-4), each of which represent a state of
// readiness to play. The meaning of the values range from HAVE_NOTHING (0), meaning
// no data is available to HAVE_ENOUGH_DATA (4), meaning all data is loaded and the
// video can be played all the way through.
//
// In order to seek successfully, the `readyState` must be at least HAVE_FUTURE_DATA
// (3).
//
// @see http://docs.videojs.com/player#readyState
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
// @see https://dev.w3.org/html5/spec-preview/media-elements.html#seek-the-media-controller
if (this._player.readyState() < HAVE_FUTURE_DATA) {
this._seekFn = this._seek.bind(this);
// The `canplay` event means that the `readyState` is at least HAVE_FUTURE_DATA.
this._player.one('canplay', this._seekFn);
} else {
this._seek();
}
},
onPlayerSourcesChange: function() {
if (this._keepThisInstanceWhenPlayerSourcesChange) {
// By setting this to `false`, we know that if the player sources change again
// the change did not originate from a quality selection change, the new sources
// are likely different from the old sources, and so this pending seek no longer
// applies.
this._keepThisInstanceWhenPlayerSourcesChange = false;
} else {
this.cancel();
}
},
onQualitySelectionChange: function() {
// `onPlayerSourcesChange` will cancel this pending seek unless we tell it not to.
// We need to reuse this same pending seek instance because when the player is
// paused, the `preload` attribute is set to `none`, and the user selects one
// quality option and then another, the player cannot seek until the player has
// enough data to do so (and the `canplay` event is fired) and thus on the second
// selection the player's `currentTime()` is `0` and when the video plays we would
// seek to `0` instead of the correct time.
if (!this.hasFinished()) {
this._keepThisInstanceWhenPlayerSourcesChange = true;
}
},
_seek: function() {
this._player.currentTime(this._seekToTime);
this._keepThisInstanceWhenPlayerSourcesChange = false;
this._hasFinished = true;
},
hasFinished: function() {
return this._hasFinished;
},
cancel: function() {
this._player.off('canplay', this._seekFn);
this._keepThisInstanceWhenPlayerSourcesChange = false;
this._hasFinished = true;
},
});