UNPKG

videojs-errors

Version:

A VideoJS plugin for custom error reporting

701 lines (567 loc) 22.8 kB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ },{}],2:[function(require,module,exports){ (function (global){ var topLevel = typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : {} var minDoc = require('min-document'); if (typeof document !== 'undefined') { module.exports = document; } else { var doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; if (!doccy) { doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; } module.exports = doccy; } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"min-document":1}],3:[function(require,module,exports){ (function (global){ if (typeof window !== "undefined") { module.exports = window; } else if (typeof global !== "undefined") { module.exports = global; } else if (typeof self !== "undefined"){ module.exports = self; } else { module.exports = {}; } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],4:[function(require,module,exports){ (function (global){ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _videoJs = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null); var _videoJs2 = _interopRequireDefault(_videoJs); var _globalWindow = require('global/window'); var _globalWindow2 = _interopRequireDefault(_globalWindow); var _globalDocument = require('global/document'); var _globalDocument2 = _interopRequireDefault(_globalDocument); var FlashObj = _videoJs2['default'].getComponent('Flash'); // Default options for the plugin. var defaults = { header: '', code: '', message: '', timeout: 45 * 1000, errors: { '1': { type: 'MEDIA_ERR_ABORTED', headline: 'The video download was cancelled' }, '2': { type: 'MEDIA_ERR_NETWORK', headline: 'The video connection was lost, please confirm you are ' + 'connected to the internet' }, '3': { type: 'MEDIA_ERR_DECODE', headline: 'The video is bad or in a format that cannot be played on your browser' }, '4': { type: 'MEDIA_ERR_SRC_NOT_SUPPORTED', headline: 'This video is either unavailable or not supported in this browser' }, '5': { type: 'MEDIA_ERR_ENCRYPTED', headline: 'The video you are trying to watch is encrypted and we do not know how ' + 'to decrypt it' }, 'unknown': { type: 'MEDIA_ERR_UNKNOWN', headline: 'An unanticipated problem was encountered, check back soon and try again' }, '-1': { type: 'PLAYER_ERR_NO_SRC', headline: 'No video has been loaded' }, '-2': { type: 'PLAYER_ERR_TIMEOUT', headline: 'Could not download the video' } } }; /** * Monitors a player for signs of life during playback and * triggers PLAYER_ERR_TIMEOUT if none occur within a reasonable * timeframe. */ var initPlugin = function initPlugin(player, options) { var monitor = undefined; var listeners = []; // clears the previous monitor timeout and sets up a new one var resetMonitor = function resetMonitor() { _globalWindow2['default'].clearTimeout(monitor); monitor = _globalWindow2['default'].setTimeout(function () { // player already has an error // or is not playing under normal conditions if (player.error() || player.paused() || player.ended()) { return; } player.error({ code: -2, type: 'PLAYER_ERR_TIMEOUT' }); }, options.timeout); // clear out any existing player timeout // playback has recovered if (player.error() && player.error().code === -2) { player.error(null); } }; // clear any previously registered listeners var cleanup = function cleanup() { var listener = undefined; while (listeners.length) { listener = listeners.shift(); player.off(listener[0], listener[1]); } _globalWindow2['default'].clearTimeout(monitor); }; // creates and tracks a player listener if the player looks alive var healthcheck = function healthcheck(type, fn) { var check = function check() { // if there's an error do not reset the monitor and // clear the error unless time is progressing if (!player.error()) { // error if using Flash and its API is unavailable var tech = player.$('.vjs-tech'); if (tech && tech.type === 'application/x-shockwave-flash' && !tech.vjs_getProperty) { player.error({ code: -2, type: 'PLAYER_ERR_TIMEOUT' }); return; } // playback isn't expected if the player is paused if (player.paused()) { return resetMonitor(); } // playback isn't expected once the video has ended if (player.ended()) { return resetMonitor(); } } fn.call(this); }; player.on(type, check); listeners.push([type, check]); }; var onPlayStartMonitor = function onPlayStartMonitor() { var lastTime = 0; cleanup(); // if no playback is detected for long enough, trigger a timeout error resetMonitor(); healthcheck(['timeupdate', 'adtimeupdate'], function () { var currentTime = player.currentTime(); // playback is operating normally or has recovered if (currentTime !== lastTime) { lastTime = currentTime; resetMonitor(); } }); healthcheck('progress', resetMonitor); }; var onPlayNoSource = function onPlayNoSource() { if (!player.currentSrc()) { player.error({ code: -1, type: 'PLAYER_ERR_NO_SRC' }); } }; var onErrorHandler = function onErrorHandler() { var display = undefined; var details = ''; var error = player.error(); var content = _globalDocument2['default'].createElement('div'); // In the rare case when `error()` does not return an error object, // defensively escape the handler function. if (!error) { return; } error = _videoJs2['default'].mergeOptions(error, options.errors[error.code || 0]); if (error.message) { details = '<div class="vjs-errors-details">' + player.localize('Technical details') + '\n : <div class="vjs-errors-message">' + player.localize(error.message) + '</div>\n </div>'; } if (error.code === 4 && !FlashObj.isSupported()) { var flashMessage = player.localize(' * If you are using an older browser' + ' please try upgrading or installing Flash.'); details += '<span class="vjs-errors-flashmessage">' + flashMessage + '</span>'; } display = player.getChild('errorDisplay'); // The code snippet below is to make sure we dispose any child closeButtons before // making the display closeable if (display.getChild('closeButton')) { display.removeChild('closeButton'); } // Make the error display closeable, and we should get a close button display.closeable(true); content.className = 'vjs-errors-dialog'; content.id = 'vjs-errors-dialog'; content.innerHTML = '<div class="vjs-errors-content-container">\n <h2 class="vjs-errors-headline">' + this.localize(error.headline) + '</h2>\n <div><b>' + this.localize('Error Code') + '</b>: ' + (error.type || error.code) + '</div>\n ' + details + '\n </div>\n <div class="vjs-errors-ok-button-container">\n <button class="vjs-errors-ok-button">' + this.localize('OK') + '</button>\n </div>'; display.fillWith(content); // Get the close button inside the error display display.contentEl().firstChild.appendChild(display.getChild('closeButton').el()); if (player.width() <= 600 || player.height() <= 250) { display.addClass('vjs-xs'); } var okButton = display.el().querySelector('.vjs-errors-ok-button'); _videoJs2['default'].on(okButton, 'click', function () { display.close(); }); }; var onDisposeHandler = function onDisposeHandler() { cleanup(); player.removeClass('vjs-errors'); player.off('play', onPlayStartMonitor); player.off('play', onPlayNoSource); player.off('dispose', onDisposeHandler); player.off('error', onErrorHandler); }; var reInitPlugin = function reInitPlugin(newOptions) { onDisposeHandler(); initPlugin(player, _videoJs2['default'].mergeOptions(defaults, newOptions)); }; player.on('play', onPlayStartMonitor); player.on('play', onPlayNoSource); player.on('dispose', onDisposeHandler); player.on('error', onErrorHandler); player.ready(function () { player.addClass('vjs-errors'); }); player.errors = reInitPlugin; }; /** * Initialize the plugin. Waits until the player is ready to do anything. */ var errors = function errors(options) { initPlugin(this, _videoJs2['default'].mergeOptions(defaults, options)); }; // Register the plugin with video.js. _videoJs2['default'].plugin('errors', errors); exports['default'] = errors; module.exports = exports['default']; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"global/document":2,"global/window":3}],5:[function(require,module,exports){ (function (global){ 'use strict'; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _globalDocument = require('global/document'); var _globalDocument2 = _interopRequireDefault(_globalDocument); var _qunit = (typeof window !== "undefined" ? window['QUnit'] : typeof global !== "undefined" ? global['QUnit'] : null); var _qunit2 = _interopRequireDefault(_qunit); var _sinon = (typeof window !== "undefined" ? window['sinon'] : typeof global !== "undefined" ? global['sinon'] : null); var _sinon2 = _interopRequireDefault(_sinon); var _videoJs = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null); var _videoJs2 = _interopRequireDefault(_videoJs); var _srcPlugin = require('../src/plugin'); var _srcPlugin2 = _interopRequireDefault(_srcPlugin); var Player = _videoJs2['default'].getComponent('Player'); var sources = [{ src: 'movie.mp4', type: 'video/mp4' }, { src: 'movie.webm', type: 'video/webm' }]; _qunit2['default'].test('the environment is sane', function (assert) { assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); assert.strictEqual(typeof _sinon2['default'], 'object', 'sinon exists'); assert.strictEqual(typeof _videoJs2['default'], 'function', 'videojs exists'); assert.strictEqual(typeof _srcPlugin2['default'], 'function', 'plugin is a function'); }); _qunit2['default'].module('videojs-errors', { beforeEach: function beforeEach() { // Mock the environment's timers because certain things - particularly // player readiness - are asynchronous in video.js 5. this.clock = _sinon2['default'].useFakeTimers(); this.fixture = _globalDocument2['default'].getElementById('qunit-fixture'); this.video = _globalDocument2['default'].createElement('video'); this.fixture.appendChild(this.video); this.player = (0, _videoJs2['default'])(this.video); this.player.buffered = function () { return _videoJs2['default'].createTimeRange(0, 0); }; this.player.paused = function () { return false; }; this.player.pause = function () { return false; }; // initialize the plugin with the default options this.player.errors(); // Tick forward so the player is ready. this.clock.tick(1); }, afterEach: function afterEach() { this.player.dispose(); this.clock.restore(); } }); _qunit2['default'].test('registers itself with video.js', function (assert) { assert.expect(2); assert.strictEqual(Player.prototype.errors, _srcPlugin2['default'], 'videojs-errors plugin was registered'); this.player.errors(); // Tick the clock forward enough to trigger the player to be "ready". this.clock.tick(1); assert.ok(this.player.hasClass('vjs-errors'), 'the plugin adds a class to the player'); }); _qunit2['default'].test('play() without a src is an error', function (assert) { var errors = 0; this.player.on('error', function () { errors++; }); this.player.trigger('play'); assert.strictEqual(errors, 1, 'emitted an error'); assert.strictEqual(this.player.error().code, -1, 'error code is -1'); assert.strictEqual(this.player.error().type, 'PLAYER_ERR_NO_SRC', 'error type is no source'); }); _qunit2['default'].test('no progress for 45 seconds is an error', function (assert) { var errors = 0; this.player.on('error', function () { errors++; }); this.player.src(sources); this.player.trigger('play'); this.clock.tick(45 * 1000); assert.strictEqual(errors, 1, 'emitted an error'); assert.strictEqual(this.player.error().code, -2, 'error code is -2'); assert.strictEqual(this.player.error().type, 'PLAYER_ERR_TIMEOUT'); }); _qunit2['default'].test('Flash API is unavailable when using Flash is an error', function (assert) { this.player.tech_.el_.type = 'application/x-shockwave-flash'; // when Flash dies the object methods go away /* eslint-disable camelcase */ this.player.tech_.el_.vjs_getProperty = null; /* eslint-enable camelcase */ this.player.paused = function () { return true; }; var errors = 0; this.player.on('error', function () { errors++; }); this.player.src(sources); this.player.trigger('play'); this.player.trigger('timeupdate'); assert.strictEqual(errors, 1, 'emitted an error'); assert.strictEqual(this.player.error().code, -2, 'error code is -2'); assert.strictEqual(this.player.error().type, 'PLAYER_ERR_TIMEOUT'); }); _qunit2['default'].test('the plugin cleans up after its previous incarnation when called again', function (assert) { var errors = 0; this.player.on('error', function () { return errors++; }); // Call plugin multiple times this.player.errors(); this.player.errors(); // Tick the clock forward enough to trigger the player to be "ready". this.clock.tick(1); this.player.trigger('play'); assert.strictEqual(errors, 1, 'emitted a single error'); assert.strictEqual(this.player.error().code, -1, 'error code is -1'); assert.strictEqual(this.player.error().type, 'PLAYER_ERR_NO_SRC'); }); _qunit2['default'].test('when dispose is triggered should not throw error ', function (assert) { this.player.src(sources); this.player.trigger('play'); this.player.trigger('dispose'); this.clock.tick(45 * 1000); assert.ok(!this.player.error(), 'should not throw player error when dispose is called.'); }); _qunit2['default'].test('progress clears player timeout errors', function (assert) { var errors = 0; this.player.on('error', function () { errors++; }); this.player.src(sources); this.player.trigger('play'); this.clock.tick(45 * 1000); assert.strictEqual(errors, 1, 'emitted an error'); assert.strictEqual(this.player.error().code, -2, 'error code is -2'); assert.strictEqual(this.player.error().type, 'PLAYER_ERR_TIMEOUT'); this.player.trigger('progress'); assert.strictEqual(this.player.error(), null, 'error removed'); }); // safari 7 on OSX can emit stalls when playback is just fine _qunit2['default'].test('stalling by itself is not an error', function (assert) { this.player.src(sources); this.player.trigger('play'); this.player.trigger('stalled'); assert.ok(!this.player.error(), 'no error fired'); }); _qunit2['default'].test('timing out multiple times only throws a single error', function (assert) { var errors = 0; this.player.on('error', function () { errors++; }); this.player.src(sources); this.player.trigger('play'); // trigger a player timeout this.clock.tick(45 * 1000); assert.strictEqual(errors, 1, 'one error fired'); // wait long enough for another timeout this.clock.tick(50 * 1000); assert.strictEqual(errors, 1, 'only one error fired'); }); _qunit2['default'].test('progress events while playing reset the player timeout', function (assert) { var errors = 0; this.player.on('error', function () { errors++; }); this.player.src(sources); this.player.trigger('play'); // stalled for awhile this.clock.tick(44 * 1000); // but playback resumes! this.player.trigger('progress'); this.clock.tick(44 * 1000); assert.strictEqual(errors, 0, 'no errors emitted'); }); _qunit2['default'].test('no signs of playback triggers a player timeout', function (assert) { var errors = 0; this.player.src(sources); this.player.on('error', function () { errors++; }); // swallow any timeupdate events this.player.on('timeupdate', function (event) { event.stopImmediatePropagation(); }); this.player.trigger('play'); this.clock.tick(45 * 1000); assert.strictEqual(errors, 1, 'emitted a single error'); assert.strictEqual(this.player.error().code, -2, 'error code is -2'); assert.strictEqual(this.player.error().type, 'PLAYER_ERR_TIMEOUT', 'type is player timeout'); }); _qunit2['default'].test('time changes while playing reset the player timeout', function (assert) { var errors = 0; this.player.src(sources); this.player.on('error', function () { errors++; }); this.player.trigger('play'); this.clock.tick(44 * 1000); this.player.currentTime = function () { return 1; }; this.player.trigger('timeupdate'); this.clock.tick(10 * 1000); assert.strictEqual(errors, 0, 'no error emitted'); }); _qunit2['default'].test('time changes while playing ads reset the player timeout', function (assert) { var errors = 0; this.player.src(sources); this.player.on('error', function () { errors++; }); this.player.trigger('play'); this.clock.tick(44 * 1000); this.player.currentTime = function () { return 1; }; this.player.trigger('adtimeupdate'); this.clock.tick(10 * 1000); assert.strictEqual(errors, 0, 'no error emitted'); }); _qunit2['default'].test('time changes after a player timeout clears the error', function (assert) { this.player.src(sources); this.player.trigger('play'); this.clock.tick(45 * 1000); this.player.currentTime = function () { return 1; }; this.player.trigger('timeupdate'); assert.ok(!this.player.error(), 'cleared the timeout'); }); _qunit2['default'].test('player timeouts do not occur if the player is paused', function (assert) { var errors = 0; this.player.src(sources); this.player.on('error', function () { errors++; }); this.player.on('timeupdate', function (event) { event.stopImmediatePropagation(); }); this.player.trigger('play'); // simulate a misbehaving player that doesn't fire `paused` this.player.paused = function () { return true; }; this.clock.tick(45 * 1000); assert.strictEqual(errors, 0, 'no error emitted'); }); // video.paused is false at the end of a video on IE11, Win8 RT _qunit2['default'].test('player timeouts do not occur if the video is ended', function (assert) { var errors = 0; this.player.src(sources); this.player.on('error', function () { errors++; }); this.player.on('timeupdate', function (event) { event.stopImmediatePropagation(); }); this.player.trigger('play'); // simulate a misbehaving player that doesn't fire `ended` this.player.ended = function () { return true; }; this.clock.tick(45 * 1000); assert.strictEqual(errors, 0, 'no error emitted'); }); _qunit2['default'].test('player timeouts do not overwrite existing errors', function (assert) { this.player.src(sources); this.player.trigger('play'); this.player.error({ type: 'custom', code: -7 }); this.clock.tick(45 * 1000); assert.strictEqual(-7, this.player.error().code, 'error was not overwritten'); }); _qunit2['default'].test('unrecognized error codes do not cause exceptions', function (assert) { var errors = 0; this.player.on('error', function () { errors++; }); this.player.error({ code: 'something-custom-that-no-one-could-have-predicted', type: 'NOT_AN_ERROR_CONSTANT' }); assert.ok(true, 'does not throw an exception'); assert.strictEqual(errors, 1, 'emitted an error'); // intentionally missing properties this.player.error({}); assert.ok(true, 'does not throw an exception'); assert.strictEqual(errors, 2, 'emitted an error'); }); _qunit2['default'].test('custom error details should override defaults', function (assert) { var customError = { headline: 'test headline', message: 'test details' }; // initialize the plugin with custom options this.player.errors({ errors: { 4: customError } }); // tick forward enough to ready the player this.clock.tick(1); // trigger the error in question this.player.error(4); // confirm results assert.strictEqual(_globalDocument2['default'].querySelector('.vjs-errors-headline').textContent, customError.headline, 'headline should match custom override value'); assert.strictEqual(_globalDocument2['default'].querySelector('.vjs-errors-message').textContent, customError.message, 'message should match custom override value'); }); _qunit2['default'].test('Append Flash error details when flash is not supported', function (assert) { var oldIsSupported = _videoJs2['default'].getComponent('Flash').isSupported; // Mock up isSupported to be false _videoJs2['default'].getComponent('Flash').isSupported = function () { return false; }; // tick forward enough to ready the player this.clock.tick(1); // trigger the error in question this.player.error(4); // confirm results assert.equal(_globalDocument2['default'].querySelector('.vjs-errors-flashmessage').textContent, ' * If you are using an older browser please try upgrading or installing Flash.', 'Flash Error message should be displayed'); // Restoring isSupported to the old value _videoJs2['default'].getComponent('Flash').isSupported = oldIsSupported; }); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"../src/plugin":4,"global/document":2}]},{},[5]);