Skip to content

Commit

Permalink
Started on better error handling and displaying in the UI when an err…
Browse files Browse the repository at this point in the history
…or has occurred.
  • Loading branch information
heff committed May 7, 2014
1 parent 740014c commit 56cbe66
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 20 deletions.
1 change: 1 addition & 0 deletions build/source-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var sourceFiles = [
"src/js/poster.js",
"src/js/loading-spinner.js",
"src/js/big-play-button.js",
"src/js/error-display.js",
"src/js/media/media.js",
"src/js/media/html5.js",
"src/js/media/flash.js",
Expand Down
46 changes: 46 additions & 0 deletions src/css/video-js.less
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ The default control bar that is a container for most of the controls.
display: none;
}

/* The control bar shouldn't show after an error */
.vjs-default-skin.vjs-error .vjs-control-bar {
display: none;
}

/* IE8 is flakey with fonts, and you have to change the actual content to force
fonts to show/hide properly.
- "\9" IE8 hack didn't work for this
Expand Down Expand Up @@ -543,6 +548,41 @@ easily in the skin designer. http://designer.videojs.com/
height: 100%;
}

.vjs-error .vjs-big-play-button {
display: none;
}

/* Error Display
--------------------------------------------------------------------------------
*/

.vjs-error .vjs-error-display {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}

.vjs-error .vjs-error-display:before {
// content: @play-icon;
// font-family: VideoJS;
content: 'X';
font-family: Arial;
font-size: 4em;
/* In order to center the play icon vertically we need to set the line height
to the same as the button height */
line-height: 1;
text-shadow: 0.05em 0.05em 0.1em #000;
text-align: center /* Needed for IE8 */;
vertical-align: middle;

position: absolute;
top: 50%;
margin-top: -0.5em;
width: 100%;
}

/* Loading Spinner
--------------------------------------------------------------------------------
*/
Expand All @@ -566,6 +606,12 @@ easily in the skin designer. http://designer.videojs.com/
.animation(spin 1.5s infinite linear);
}

/* Errors are unrecoverable without user interaction,
so hide the spinner in the case of an error */
.video-js.vjs-error .vjs-loading-spinner {
display: none;
}

.vjs-default-skin .vjs-loading-spinner:before {
content: @spinner3-icon;
font-family: VideoJS;
Expand Down
3 changes: 2 additions & 1 deletion src/js/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ vjs.options = {
'textTrackDisplay': {},
'loadingSpinner': {},
'bigPlayButton': {},
'controlBar': {}
'controlBar': {},
'errorDisplay': {}
},

// Default message to show when a video cannot be played.
Expand Down
19 changes: 19 additions & 0 deletions src/js/error-display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Display that an error has occurred making the video unplayable
* @param {vjs.Player|Object} player
* @param {Object=} options
* @constructor
*/
vjs.ErrorDisplay = vjs.Component.extend({
init: function(player, options){
vjs.Component.call(this, player, options);
}
});

vjs.ErrorDisplay.prototype.createEl = function(){
var el = vjs.Component.prototype.createEl.call(this, 'div', {
className: 'vjs-error-display'
});

return el;
};
1 change: 0 additions & 1 deletion src/js/loading-spinner.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ vjs.LoadingSpinner = vjs.Component.extend({
// 'seeking' event
player.on('seeked', vjs.bind(this, this.hide));

player.on('error', vjs.bind(this, this.show));
player.on('ended', vjs.bind(this, this.hide));

// Not showing spinner on stalled any more. Browsers may stall and then not trigger any events that would remove the spinner.
Expand Down
20 changes: 14 additions & 6 deletions src/js/media/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,26 @@ vjs.Html5.prototype.createEl = function(){

// Make video events trigger player events
// May seem verbose here, but makes other APIs possible.
// Triggers removed using this.off when disposed
vjs.Html5.prototype.setupTriggers = function(){
for (var i = vjs.Html5.Events.length - 1; i >= 0; i--) {
vjs.on(this.el_, vjs.Html5.Events[i], vjs.bind(this.player_, this.eventHandler));
vjs.on(this.el_, vjs.Html5.Events[i], vjs.bind(this, this.eventHandler));
}
};
// Triggers removed using this.off when disposed

vjs.Html5.prototype.eventHandler = function(e){
this.trigger(e);
vjs.Html5.prototype.eventHandler = function(evt){
// In the case of an error, set the error prop on the player
// and let the player handle triggering the event.
if (evt.type == 'error') {
this.player().error(this.error().code);

// No need for media events to bubble up.
e.stopPropagation();
// in some cases we pass the event directly to the player
} else {
// No need for media events to bubble up.
evt.bubbles = false;

this.player().trigger(evt);
}
};

vjs.Html5.prototype.useNativeControls = function(){
Expand Down
126 changes: 114 additions & 12 deletions src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ vjs.Player = vjs.Component.extend({
this.on('pause', this.onPause);
this.on('progress', this.onProgress);
this.on('durationchange', this.onDurationChange);
this.on('error', this.onError);
this.on('fullscreenchange', this.onFullscreenChange);

// Make player easily findable by ID
Expand Down Expand Up @@ -552,14 +551,6 @@ vjs.Player.prototype.onFullscreenChange = function() {
}
};

/**
* Fired when there is an error in playback
* @event error
*/
vjs.Player.prototype.onError = function(e) {
vjs.log('Video Error', e);
};

// /* Player API
// ================================================================================ */

Expand Down Expand Up @@ -594,7 +585,6 @@ vjs.Player.prototype.techCall = function(method, arg){

// Get calls can't wait for the tech, and sometimes don't need to.
vjs.Player.prototype.techGet = function(method){

if (this.tech && this.tech.isReady_) {

// Flash likes to die and reload when you hide or reposition it.
Expand Down Expand Up @@ -630,7 +620,15 @@ vjs.Player.prototype.techGet = function(method){
* @return {vjs.Player} self
*/
vjs.Player.prototype.play = function(){
this.techCall('play');
if (this.error()) {
// In the case of an error, trying to play again wont fix the issue
// so we're blocking calling play in this case.
// We might log an error when this happpens, but this is probably too chatty.
// vjs.log.error('The error must be resolved before attempting to play the video');
} else {
this.techCall('play');
}

return this;
};

Expand Down Expand Up @@ -1268,7 +1266,111 @@ vjs.Player.prototype.usingNativeControls = function(bool){
return this.usingNativeControls_;
};

vjs.Player.prototype.error = function(){ return this.techGet('error'); };
/**
* Custom MediaError to mimic the HTML5 MediaError
* @param {Number} code The media error code
*/
vjs.MediaError = function(code){
if (typeof code == 'number') {
this.code = code;
} else if (typeof code == 'string') {
// default code is zero, so this is a custom error
this.message = code;
} else if (typeof code == 'object') { // object
vjs.obj.merge(this, code);
}
};

vjs.MediaError.prototype.code = 0;

// message is not part of the HTML5 video spec
// but allows for more informative custom errors
vjs.MediaError.prototype.message = '';

vjs.MediaError.prototype.status = null;

vjs.MediaError.errorTypes = [
'MEDIA_ERR_CUSTOM', // = 0
'MEDIA_ERR_ABORTED', // = 1
'MEDIA_ERR_NETWORK', // = 2
'MEDIA_ERR_DECODE', // = 3
'MEDIA_ERR_SRC_NOT_SUPPORTED', // = 4
'MEDIA_ERR_ENCRYPTED' // = 5
];

// Add types as properties on MediaError
// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
for (var errNum = 0; errNum < vjs.MediaError.errorTypes.length; errNum++) {
vjs.MediaError[vjs.MediaError.errorTypes[errNum]] = errNum;
// values should be accessible on both the class and instance
vjs.MediaError.prototype[vjs.MediaError.errorTypes[errNum]] = errNum;
}

/**
* Store the current media error
* @type {Object}
* @private
*/
vjs.Player.prototype.error_ = null;

/**
* Set or get the current MediaError
* @param {*} err A MediaError or a String/Number to be turned into a MediaError
* @return {vjs.MediaError|null} when getting
* @return {vjs.Player} when setting
*/
vjs.Player.prototype.error = function(err){
if (err === undefined) {
return this.error_;
}

// restoring to default
if (err === null) {
this.error_ = err;
this.removeClass('vjs-error');
return this;
}

// error instance
if (err instanceof vjs.MediaError) {
this.error_ = err;
} else {
this.error_ = new vjs.MediaError(err);
}

// fire an error event on the player
this.trigger('error');

// add the vjs-error classname to the player
this.addClass('vjs-error');

// log the name of the error type and any message
vjs.log.error(this.error_);

return this;
};

vjs.Player.prototype.waiting_ = false;

vjs.Player.prototype.waiting = function(bool){
if (bool === undefined) {
return this.waiting_;
}

var wasWaiting = this.waiting_;
this.waiting_ = bool;

// trigger an event if it's newly waiting
if (!wasWaiting && bool) {
this.addClass('vjs-waiting');
this.trigger('waiting');
} else {
this.removeClass('vjs-waiting');
}

return this;
};

vjs.Player.prototype.ended = function(){ return this.techGet('ended'); };
vjs.Player.prototype.seeking = function(){ return this.techGet('seeking'); };

Expand Down
51 changes: 51 additions & 0 deletions test/unit/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,54 @@ test('should remove vjs-has-started class', function(){
player.trigger('play');
ok(player.el().className.indexOf('vjs-has-started') !== -1, 'vjs-has-started class added again');
});

test('player should handle different error types', function(){
expect(8);
var player = PlayerTest.makePlayer({});
var testMsg = 'test message';

// prevent error log messages in the console
sinon.stub(vjs.log, 'error');

// error code supplied
function errCode(){
equal(player.error().code, 1, 'error code is correct');
}
player.on('error', errCode);
player.error(1);
player.off('error', errCode);

// error instance supplied
function errInst(){
equal(player.error().code, 2, 'MediaError code is correct');
equal(player.error().message, testMsg, 'MediaError message is correct');
}
player.on('error', errInst);
player.error(new vjs.MediaError({ code: 2, message: testMsg }));
player.off('error', errInst);

// error message supplied
function errMsg(){
equal(player.error().code, 0, 'error message code is correct');
equal(player.error().message, testMsg, 'error message is correct');
}
player.on('error', errMsg);
player.error(testMsg);
player.off('error', errMsg);

// error config supplied
function errConfig(){
equal(player.error().code, 3, 'error config code is correct');
equal(player.error().message, testMsg, 'error config message is correct');
}
player.on('error', errConfig);
player.error({ code: 3, message: testMsg });
player.off('error', errConfig);

// check for vjs-error classname
ok(player.el().className.indexOf('vjs-error') >= 0, 'player does not have vjs-error classname');

// restore error logging
vjs.log.error.restore();
});

0 comments on commit 56cbe66

Please sign in to comment.