From 56cbe66f4233e54f13550367590864102f5de0fe Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Fri, 2 May 2014 13:06:49 -0700 Subject: [PATCH] Started on better error handling and displaying in the UI when an error has occurred. --- build/source-loader.js | 1 + src/css/video-js.less | 46 ++++++++++++++ src/js/core.js | 3 +- src/js/error-display.js | 19 ++++++ src/js/loading-spinner.js | 1 - src/js/media/html5.js | 20 ++++-- src/js/player.js | 126 ++++++++++++++++++++++++++++++++++---- test/unit/player.js | 51 +++++++++++++++ 8 files changed, 247 insertions(+), 20 deletions(-) create mode 100644 src/js/error-display.js diff --git a/build/source-loader.js b/build/source-loader.js index 2dfa497666..5fa092b61a 100644 --- a/build/source-loader.js +++ b/build/source-loader.js @@ -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", diff --git a/src/css/video-js.less b/src/css/video-js.less index cec3f712a1..ef29087374 100644 --- a/src/css/video-js.less +++ b/src/css/video-js.less @@ -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 @@ -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 -------------------------------------------------------------------------------- */ @@ -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; diff --git a/src/js/core.js b/src/js/core.js index 933004a3d1..2067ccd444 100644 --- a/src/js/core.js +++ b/src/js/core.js @@ -95,7 +95,8 @@ vjs.options = { 'textTrackDisplay': {}, 'loadingSpinner': {}, 'bigPlayButton': {}, - 'controlBar': {} + 'controlBar': {}, + 'errorDisplay': {} }, // Default message to show when a video cannot be played. diff --git a/src/js/error-display.js b/src/js/error-display.js new file mode 100644 index 0000000000..f381ff6c18 --- /dev/null +++ b/src/js/error-display.js @@ -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; +}; diff --git a/src/js/loading-spinner.js b/src/js/loading-spinner.js index 807d7922f2..ea78f4d26e 100644 --- a/src/js/loading-spinner.js +++ b/src/js/loading-spinner.js @@ -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. diff --git a/src/js/media/html5.js b/src/js/media/html5.js index 3c47baaea6..668ed6cc02 100644 --- a/src/js/media/html5.js +++ b/src/js/media/html5.js @@ -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(){ diff --git a/src/js/player.js b/src/js/player.js index 0d05c50270..c46c0c0782 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -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 @@ -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 // ================================================================================ */ @@ -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. @@ -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; }; @@ -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'); }; diff --git a/test/unit/player.js b/test/unit/player.js index 1cea1bf2be..8fb7595271 100644 --- a/test/unit/player.js +++ b/test/unit/player.js @@ -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(); +}); +