From 740014c57b264079cf4084965a9384b49a7c0f64 Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Wed, 30 Apr 2014 16:11:33 -0700 Subject: [PATCH 1/9] Added better global log/error/warn functions. Added sinon.js for stubs in tests. Updated grunt version to satisfy peer dependency warning. --- .jshintrc | 3 +- Gruntfile.js | 2 +- package.json | 5 ++- src/js/lib.js | 74 ++++++++++++++++++++++++++++--- test/index.html | 6 +-- test/minified-api.html | 4 ++ test/minified.html | 4 ++ test/qunit-externs.js | 2 +- test/sinon-externs.js | 99 ++++++++++++++++++++++++++++++++++++++++++ test/unit/lib.js | 54 +++++++++++++++++++++++ 10 files changed, 238 insertions(+), 15 deletions(-) create mode 100644 test/sinon-externs.js diff --git a/.jshintrc b/.jshintrc index 874627e730..63438c38c4 100644 --- a/.jshintrc +++ b/.jshintrc @@ -38,6 +38,7 @@ "start", "stop", "strictEqual", - "test" + "test", + "sinon" ] } diff --git a/Gruntfile.js b/Gruntfile.js index 8be864b609..d5d3ecac4f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -57,7 +57,7 @@ module.exports = function(grunt) { }, tests: { src: ['build/files/combined.video.js', 'build/compiler/goog.base.js', 'src/js/exports.js', 'test/unit/*.js'], - externs: ['src/js/player.externs.js', 'src/js/media/flash.externs.js', 'test/qunit-externs.js'], + externs: ['src/js/player.externs.js', 'src/js/media/flash.externs.js', 'test/qunit-externs.js', 'test/sinon-externs.js'], dest: 'build/files/test.minified.video.js' } }, diff --git a/package.json b/package.json index 33951a1265..e31cb72084 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "grunt-cli": "~0.1.0", - "grunt": "~0.4", + "grunt": "0.4.2", "grunt-contrib-connect": "~0.7.1", "grunt-contrib-jshint": "~0.4.3", "grunt-contrib-watch": "~0.1.4", @@ -57,6 +57,7 @@ "grunt-tagrelease": "~0.3.3", "github": "~0.1.14", "open": "0.0.4", - "grunt-version": "~0.3.0" + "grunt-version": "~0.3.0", + "sinon": "~1.9.1" } } diff --git a/src/js/lib.js b/src/js/lib.js index 1e64d5854b..ae4a2e9090 100644 --- a/src/js/lib.js +++ b/src/js/lib.js @@ -656,14 +656,74 @@ vjs.getAbsoluteURL = function(url){ return url; }; -// usage: log('inside coolFunc',this,arguments); -// http://paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ -vjs.log = function(){ - vjs.log.history = vjs.log.history || []; // store logs to an array for reference - vjs.log.history.push(arguments); - if(window.console){ - window.console.log(Array.prototype.slice.call(arguments)); +// if there's no console then don't try to output messages +// they will still be stored in vjs.log.history +var _noop = function(){}; +var _console = window['console'] || { + 'log': _noop, + 'warn': _noop, + 'error': _noop +}; + +/** + * Log messags to the console and history based on the type of message + * + * @param {String} type The type of message, or `null` for `log` + * @param {[type]} args The args to be passed to the log + * @private + */ +function _logType(type, args){ + // convert args to an array to get array functions + var argsArray = Array.prototype.slice.call(args); + + if (type) { + // add the type to the front of the message + argsArray.unshift(type.toUpperCase()+':'); + } else { + // default to log with no prefix + type = 'log'; } + + // add to history + vjs.log.history.push(argsArray); + + // add console prefix after adding to history + argsArray.unshift('VIDEOJS:'); + + // call appropriate log function + if (_console[type].apply) { + _console[type].apply(_console, argsArray); + } else { + // ie8 doesn't allow error.apply, but it will just join() the array anyway + _console[type](argsArray.join(' ')); + } +} + +/** + * Log plain debug messages + */ +vjs.log = function(){ + _logType(null, arguments); +}; + +/** + * Keep a history of log messages + * @type {Array} + */ +vjs.log.history = []; + +/** + * Log error messages + */ +vjs.log.error = function(){ + _logType('error', arguments); +}; + +/** + * Log warning messages + */ +vjs.log.warn = function(){ + _logType('warn', arguments); }; // Offset Left diff --git a/test/index.html b/test/index.html index 8ff47176f9..38843c8c37 100644 --- a/test/index.html +++ b/test/index.html @@ -3,9 +3,9 @@ Video.js Test Suite - + + + diff --git a/test/minified-api.html b/test/minified-api.html index 377b576fea..3246f62023 100644 --- a/test/minified-api.html +++ b/test/minified-api.html @@ -3,6 +3,10 @@ Video.js Test Suite + + + + diff --git a/test/minified.html b/test/minified.html index 65aee2de06..fdc18be008 100644 --- a/test/minified.html +++ b/test/minified.html @@ -3,6 +3,10 @@ Video.js Test Suite + + + + diff --git a/test/qunit-externs.js b/test/qunit-externs.js index a8209449e8..8a5544219d 100644 --- a/test/qunit-externs.js +++ b/test/qunit-externs.js @@ -81,4 +81,4 @@ function start(increment){} /** * @param {number=} increment */ -function stop(increment){} \ No newline at end of file +function stop(increment){} diff --git a/test/sinon-externs.js b/test/sinon-externs.js new file mode 100644 index 0000000000..d7130953a1 --- /dev/null +++ b/test/sinon-externs.js @@ -0,0 +1,99 @@ +/** + * Sinon externs + */ +function sinon(){} + +sinon.stub = function(){}; +sinon.spy = function(){}; +sinon.mock = function(){}; + +Function.prototype.alwaysCalledOn = function(){}; +Function.prototype.alwaysCalledWith = function(){}; +Function.prototype.alwaysCalledWithExactly = function(){}; +Function.prototype.alwaysCalledWithMatch = function(){}; +Function.prototype.alwaysCalledWithNew = function(){}; +Function.prototype.alwaysReturned = function(){}; +Function.prototype.alwaysThrew = function(){}; +Function.prototype.args; +Function.prototype.arguments; +Function.prototype.behaviors; +Function.prototype.callArg = function(){}; +Function.prototype.callArgOn = function(){}; +Function.prototype.callArgOnWith = function(){}; +Function.prototype.callArgWith = function(){}; +Function.prototype.callCount; +Function.prototype.callIds; +Function.prototype.called; +Function.prototype.calledAfter = function(){}; +Function.prototype.calledBefore = function(){}; +Function.prototype.calledOn = function(){}; +Function.prototype.calledOnce; +Function.prototype.calledThrice; +Function.prototype.calledTwice; +Function.prototype.calledWith = function(){}; +Function.prototype.calledWithExactly = function(){}; +Function.prototype.calledWithMatch = function(){}; +Function.prototype.calledWithNew = function(){}; +Function.prototype.caller; +Function.prototype.callsArg = function(){}; +Function.prototype.callsArgAsync = function(){}; +Function.prototype.callsArgOn = function(){}; +Function.prototype.callsArgOnAsync = function(){}; +Function.prototype.callsArgOnWith = function(){}; +Function.prototype.callsArgOnWithAsync = function(){}; +Function.prototype.callsArgWith = function(){}; +Function.prototype.callsArgWithAsync = function(){}; +Function.prototype.create = function(){}; +Function.prototype.defaultBehavior; +Function.prototype.displayName; +Function.prototype.exceptions; +Function.prototype.firstCall; +Function.prototype.formatters; +Function.prototype.func; +Function.prototype.getCall = function(){}; +Function.prototype.getCalls = function(){}; +Function.prototype.id; +Function.prototype.invoke = function(){}; +Function.prototype.invokeCallback = function(){}; +Function.prototype.isPresent = function(){}; +Function.prototype.lastCall; +Function.prototype.length; +Function.prototype.matches = function(){}; +Function.prototype.name; +Function.prototype.neverCalledWith = function(){}; +Function.prototype.neverCalledWithMatch = function(){}; +Function.prototype.notCalled; +Function.prototype.onCall = function(){}; +Function.prototype.onFirstCall = function(){}; +Function.prototype.onSecondCall = function(){}; +Function.prototype.onThirdCall = function(){}; +Function.prototype.printf = function(){}; +Function.prototype.reset = function(){}; +Function.prototype.resetBehavior = function(){}; +Function.prototype.restore = function(){}; +Function.prototype.returnValues; +Function.prototype.returned = function(){}; +Function.prototype.returns = function(){}; +Function.prototype.returnsArg = function(){}; +Function.prototype.returnsThis = function(){}; +Function.prototype.secondCall; +Function.prototype.spyCall; +Function.prototype.thirdCall; +Function.prototype.thisValues; +Function.prototype.threw = function(){}; +Function.prototype['throws'] = function(){}; +Function.prototype.throwsException = function(){}; +Function.prototype.toString = function(){}; +Function.prototype.withArgs = function(){}; +Function.prototype.yield = function(){}; +Function.prototype.yieldOn = function(){}; +Function.prototype.yieldTo = function(){}; +Function.prototype.yieldToOn = function(){}; +Function.prototype.yields = function(){}; +Function.prototype.yieldsAsync = function(){}; +Function.prototype.yieldsOn = function(){}; +Function.prototype.yieldsOnAsync = function(){}; +Function.prototype.yieldsTo = function(){}; +Function.prototype.yieldsToAsync = function(){}; +Function.prototype.yieldsToOn = function(){}; +Function.prototype.yieldsToOnAsync = function(){}; diff --git a/test/unit/lib.js b/test/unit/lib.js index 3862efeb41..f84aa1e59e 100644 --- a/test/unit/lib.js +++ b/test/unit/lib.js @@ -260,3 +260,57 @@ test('vjs.findPosition should find top and left position', function() { position = vjs.findPosition(d); deepEqual(position, {left: 0, top: 0}, 'If there is no gBCR, we should get zeros'); }); + +// LOG TESTS +test('should confirm logging functions work', function() { + var console = window['console']; + var origLog = console.log; + var origWarn = console.warn; + var origError = console.error; + + // in ie8 console.log is apparently not a 'function' so sinon chokes on it + // https://github.com/cjohansen/Sinon.JS/issues/386 + // instead we'll temporarily replace them with functions + if (typeof origLog === 'object') { + console.log = function(){}; + console.warn = function(){}; + console.error = function(){}; + } + + // stub the global log functions + var log = sinon.stub(console, 'log'); + var error = sinon.stub(console, 'error'); + var warn = sinon.stub(console, 'warn'); + + vjs.log('asdf', 'fdsa'); + ok(log.called, 'log was called'); + equal(log.firstCall.args[0], 'VIDEOJS:'); + equal(log.firstCall.args[1], 'asdf'); + equal(log.firstCall.args[2], 'fdsa'); + + vjs.log.warn('asdf', 'fdsa'); + ok(warn.called, 'warn was called'); + equal(warn.firstCall.args[0], 'VIDEOJS:'); + equal(warn.firstCall.args[1], 'WARN:'); + equal(warn.firstCall.args[2], 'asdf'); + equal(warn.firstCall.args[3], 'fdsa'); + + vjs.log.error('asdf', 'fdsa'); + ok(error.called, 'error was called'); + equal(error.firstCall.args[0], 'VIDEOJS:'); + equal(error.firstCall.args[1], 'ERROR:'); + equal(error.firstCall.args[2], 'asdf'); + equal(error.firstCall.args[3], 'fdsa'); + + // tear down sinon + log.restore(); + error.restore(); + warn.restore(); + + // restore ie8 + if (typeof origLog === 'object') { + console.log = origLog; + console.warn = origWarn; + console.error = origError; + } +}); From 56cbe66f4233e54f13550367590864102f5de0fe Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Fri, 2 May 2014 13:06:49 -0700 Subject: [PATCH 2/9] 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(); +}); + From 11ca9cdd8db4d1559f5d1908c4e67be32ca7a25e Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Fri, 2 May 2014 15:35:46 -0700 Subject: [PATCH 3/9] Updated flash tech to support new errors --- src/js/media/flash.js | 11 +++++++++-- src/js/player.js | 35 ++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/js/media/flash.js b/src/js/media/flash.js index c0f47aedcb..523b4816c7 100644 --- a/src/js/media/flash.js +++ b/src/js/media/flash.js @@ -433,8 +433,15 @@ vjs.Flash['onEvent'] = function(swfID, eventName){ // Log errors from the swf vjs.Flash['onError'] = function(swfID, err){ var player = vjs.el(swfID)['player']; - player.trigger('error'); - vjs.log('Flash Error', err, swfID); + var msg = 'FLASH: '+err; + + if (err == 'srcnotfound') { + player.error({ code: 4, message: msg }); + + // errors we haven't categorized into the media errors + } else { + player.error(msg); + } }; // Flash Version Check diff --git a/src/js/player.js b/src/js/player.js index c46c0c0782..add4e5af33 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -1345,31 +1345,32 @@ vjs.Player.prototype.error = function(err){ this.addClass('vjs-error'); // log the name of the error type and any message - vjs.log.error(this.error_); + // ie8 just logs "[object object]" if you just log the error object + vjs.log.error('(CODE:'+this.error_.code+' '+vjs.MediaError.errorTypes[this.error_.code]+')', this.error_.message, this.error_); return this; }; -vjs.Player.prototype.waiting_ = false; +// vjs.Player.prototype.waiting_ = false; -vjs.Player.prototype.waiting = function(bool){ - if (bool === undefined) { - return this.waiting_; - } +// vjs.Player.prototype.waiting = function(bool){ +// if (bool === undefined) { +// return this.waiting_; +// } - var wasWaiting = this.waiting_; - this.waiting_ = bool; +// 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'); - } +// // 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; -}; +// return this; +// }; vjs.Player.prototype.ended = function(){ return this.techGet('ended'); }; vjs.Player.prototype.seeking = function(){ return this.techGet('seeking'); }; From 95d7e7027467cf96b14db6692d93c7c7f41c5810 Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Fri, 2 May 2014 15:37:44 -0700 Subject: [PATCH 4/9] Exported ErrorDisplay --- src/js/exports.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/exports.js b/src/js/exports.js index 1f03147d37..02487b2074 100644 --- a/src/js/exports.js +++ b/src/js/exports.js @@ -85,6 +85,7 @@ goog.exportSymbol('videojs.DurationDisplay', vjs.DurationDisplay); goog.exportSymbol('videojs.TimeDivider', vjs.TimeDivider); goog.exportSymbol('videojs.RemainingTimeDisplay', vjs.RemainingTimeDisplay); goog.exportSymbol('videojs.LiveDisplay', vjs.LiveDisplay); +goog.exportSymbol('videojs.ErrorDisplay', vjs.ErrorDisplay); goog.exportSymbol('videojs.Slider', vjs.Slider); goog.exportSymbol('videojs.ProgressControl', vjs.ProgressControl); goog.exportSymbol('videojs.SeekBar', vjs.SeekBar); From 22142078427ead85548c4755bf1943a0a07b22b4 Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Fri, 2 May 2014 17:18:22 -0700 Subject: [PATCH 5/9] Updated spinner to hide on all errors --- src/css/video-js.less | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/css/video-js.less b/src/css/video-js.less index ef29087374..dd8d8aa69b 100644 --- a/src/css/video-js.less +++ b/src/css/video-js.less @@ -609,7 +609,13 @@ easily in the skin designer. http://designer.videojs.com/ /* 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; + /* using !important flag because currently the loading spinner + uses hide()/show() instead of classes. The !important can be + removed when that's updated */ + display: none !important; + + /* ensure animation doesn't continue while hidden */ + .animation(none); } .vjs-default-skin .vjs-loading-spinner:before { From 561c3f844956db6f532cae8ed81a86cc39b10db1 Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Mon, 5 May 2014 10:44:47 -0700 Subject: [PATCH 6/9] Added support for displaying a message for the error. --- src/css/video-js.less | 14 ++++++++++++++ src/js/core.js | 7 +++---- src/js/error-display.js | 12 ++++++++++++ src/js/player.js | 7 ++++--- test/unit/player.js | 36 ++++++++++++++++++------------------ 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/css/video-js.less b/src/css/video-js.less index dd8d8aa69b..dbcea28e4a 100644 --- a/src/css/video-js.less +++ b/src/css/video-js.less @@ -583,6 +583,20 @@ easily in the skin designer. http://designer.videojs.com/ width: 100%; } +.vjs-error-display div { + position: absolute; + + font-size: 1.4em; + text-align: center; + bottom: 1em; + right: 1em; + left: 1em; +} + +.vjs-error-display a, .vjs-error-display a:visited { + color: #F4A460; +} + /* Loading Spinner -------------------------------------------------------------------------------- */ diff --git a/src/js/core.js b/src/js/core.js index 2067ccd444..5d85366fc4 100644 --- a/src/js/core.js +++ b/src/js/core.js @@ -100,10 +100,9 @@ vjs.options = { }, // Default message to show when a video cannot be played. - 'notSupportedMessage': 'Sorry, no compatible source and playback ' + - 'technology were found for this video. Try using another browser ' + - 'like Chrome or download the ' + - 'latest Adobe Flash Player.' + 'notSupportedMessage': 'No compatible source ' + + 'was found for this video. Learn more about' + + ' supporting video.' }; // Set CDN Version of swf diff --git a/src/js/error-display.js b/src/js/error-display.js index f381ff6c18..98803b8eff 100644 --- a/src/js/error-display.js +++ b/src/js/error-display.js @@ -7,6 +7,9 @@ vjs.ErrorDisplay = vjs.Component.extend({ init: function(player, options){ vjs.Component.call(this, player, options); + + this.update(); + player.on('error', vjs.bind(this, this.update)); } }); @@ -15,5 +18,14 @@ vjs.ErrorDisplay.prototype.createEl = function(){ className: 'vjs-error-display' }); + this.contentEl_ = vjs.createEl('div'); + el.appendChild(this.contentEl_); + return el; }; + +vjs.ErrorDisplay.prototype.update = function(){ + if (this.player().error()) { + this.contentEl_.innerHTML = this.player().error().message; + } +}; diff --git a/src/js/player.js b/src/js/player.js index add4e5af33..3d4d7031c2 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -1068,9 +1068,10 @@ vjs.Player.prototype.src = function(source){ this.loadTech(techName, source); } } else { - this.el_.appendChild(vjs.createEl('p', { - innerHTML: this.options()['notSupportedMessage'] - })); + // this.el_.appendChild(vjs.createEl('p', { + // innerHTML: this.options()['notSupportedMessage'] + // })); + this.error({ code: 4, message: this.options()['notSupportedMessage'] }); this.triggerReady(); // we could not find an appropriate tech, but let's still notify the delegate that this is it } diff --git a/test/unit/player.js b/test/unit/player.js index 8fb7595271..7a792df66e 100644 --- a/test/unit/player.js +++ b/test/unit/player.js @@ -77,7 +77,7 @@ test('should get tag, source, and track settings', function(){ var fixture = document.getElementById('qunit-fixture'); - var html = '