'use strict'; var extend = require('extend-object'), isFunction = require('is-function'), bean = require('bean'), slider = require('./ext/ui/slider'), barSlider = require('./ext/ui/bar-slider'), common = require('./common'), events = require('./ext/events'); var instances = [], extensions = []; var oldHandler = window.onbeforeunload; window.onbeforeunload = function(ev) { instances.forEach(function(api) { if (api.conf.splash) { api.unload(); } else { api.bind("error", function () { common.find('.flowplayer.is-error .fp-message').forEach(common.removeNode); }); } }); if (oldHandler) return oldHandler(ev); }; var isSafari = /Safari/.exec(navigator.userAgent) && !/Chrome/.exec(navigator.userAgent), m = /(\d+\.\d+) Safari/.exec(navigator.userAgent), safariVersion = m ? Number(m[1]) : 100; /* flowplayer() */ var flowplayer = module.exports = function(fn, opts, callback) { if (isFunction(fn)) return extensions.push(fn); if (typeof fn == 'number' || typeof fn === 'undefined') return instances[fn || 0]; if (fn.nodeType) { // Is an element if (fn.getAttribute('data-flowplayer-instance-id') !== null) { // Already flowplayer instance return instances[fn.getAttribute('data-flowplayer-instance-id')]; } if (!opts) return; // Can't initialize without data return initializePlayer(fn, opts, callback); } if (fn.jquery) return flowplayer(fn[0], opts, callback); if (typeof fn === 'string') { var el = common.find(fn)[0]; return el && flowplayer(el, opts, callback); } }; extend(flowplayer, { version: '@VERSION', engines: [], engine: function(name) { return flowplayer.engines.filter(function(e) { return e.engineName === name; })[0]; }, extensions: [], conf: {}, set: function(key, value) { if (typeof key === 'string') flowplayer.conf[key] = value; else extend(flowplayer.conf, key); }, registerExtension: function(js, css) { flowplayer.extensions.push([js, css]); }, support: {}, defaults: { debug: false, // true = forced playback disabled: false, fullscreen: window == window.top, // keyboard shortcuts keyboard: true, // default aspect ratio ratio: 9 / 16, adaptiveRatio: false, rtmp: 0, proxy: 'best', hlsQualities: true, seekStep: false, splash: false, live: false, livePositionOffset: 120, swf: "//@CDN/@VERSION/@CDN_PATHflowplayer.swf", swfHls: "//@CDN/@VERSION/@CDN_PATHflowplayerhls.swf", speeds: [0.25, 0.5, 1, 1.5, 2], tooltip: true, mouseoutTimeout: 5000, mutedAutoplay: true, clickToUnMute: true, // initial volume level volume: 1, // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#error-codes errors: [ // video exceptions '', 'Video loading aborted', 'Network error', 'Video not properly encoded', 'Video file not found', // player exceptions 'Unsupported video', 'Skin not found', 'SWF file not found', 'Subtitles not found', 'Invalid RTMP URL', 'Unsupported video format. Try installing Adobe Flash.' ], errorUrls: ['','','','','','','','','','', 'http://get.adobe.com/flashplayer/' ], playlist: [], hlsFix: isSafari && safariVersion < 8, disableInline: false }, // Expose utilities for plugins bean: bean, common: common, slider: slider, barSlider: barSlider, extend: extend }); // keep track of players var playerCount = 0; var URLResolver = require('./ext/resolve'); if (typeof window.jQuery !== 'undefined') { var $ = window.jQuery; // auto-install (any video tag with parent .flowplayer) $(function() { if (typeof $.fn.flowplayer == 'function') { $('.flowplayer:has(video:not(.fp-engine),script[type="application/json"])').flowplayer(); } }); // jQuery plugin var videoTagConfig = function(videoTag) { if (!videoTag.length) return {}; var clip = videoTag.data() || {}, conf = {}; $.each(['autoplay', 'loop', 'preload', 'poster'], function(i, key) { var val = videoTag.attr(key); if (val !== undefined && ['autoplay', 'poster'].indexOf(key) !== -1) conf[key] = val ? val : true; else if (val !== undefined) clip[key] = val ? val : true; }); videoTag[0].autoplay = videoTag[0].preload = false; clip.subtitles = videoTag.find('track').map(function() { var tr = $(this); return { src: tr.attr('src'), kind: tr.attr('kind'), label: tr.attr('label'), srclang: tr.attr('srclang'), 'default': tr.prop('default') }; }).get(); clip.sources = (new URLResolver()).sourcesFromVideoTag(videoTag, $); return extend(conf, {clip: clip}); }; $.fn.flowplayer = function(opts, callback) { return this.each(function() { if (typeof opts == 'string') opts = { swf: opts }; if (isFunction(opts)) { callback = opts; opts = {}; } var root = $(this), scriptConf = root.find('script[type="application/json"]'), confObject = scriptConf.length ? JSON.parse(scriptConf.text()) : videoTagConfig(root.find('video')), conf = $.extend({}, opts || {}, confObject, root.data()); var api = initializePlayer(this, conf, callback); events.EVENTS.forEach(function(evName) { api.on(evName + '.jquery', function(ev) { root.trigger.call(root, ev.type, ev.detail && ev.detail.args); }); }); root.data('flowplayer', api); }); }; } function initializePlayer(element, opts, callback) { if (opts && opts.embed) opts.embed = extend({}, flowplayer.defaults.embed, opts.embed); var supportLocalStorage = false; try { if (typeof flowplayer.conf.storage === 'undefined' && typeof window.localStorage == "object") { window.localStorage.flowplayerTestStorage = "test"; supportLocalStorage = true; } } catch (ignored) {} var root = element, conf = extend({}, flowplayer.defaults, flowplayer.conf, opts), storage = {}, originalClass = root.className, lastSeekPosition, engine, urlResolver = new URLResolver(); common.addClass(root, 'is-loading'); common.toggleClass(root, 'no-flex', !flowplayer.support.flex); common.toggleClass(root, 'no-svg', !flowplayer.support.svg); try { storage = flowplayer.conf.storage || (supportLocalStorage ? window.localStorage : storage); } catch(e) {} conf.volume = storage.muted === "true" ? 0 : conf.volume !== flowplayer.defaults.volume ? conf.volume : !isNaN(storage.volume) ? storage.volume : conf.volume; conf.debug = !!storage.flowplayerDebug || conf.debug; if (conf.aspectRatio && typeof conf.aspectRatio === 'string') { var parts = conf.aspectRatio.split(/[:\/]/); conf.ratio = parts[1] / parts[0]; } var isRTL = (root.currentStyle && root.currentStyle.direction === 'rtl') || (window.getComputedStyle && window.getComputedStyle(root, null) !== null && window.getComputedStyle(root, null).getPropertyValue('direction') === 'rtl'); if (isRTL) common.addClass(root, 'is-rtl'); /*** API ***/ var api = { // properties conf: conf, currentSpeed: 1, volumeLevel: conf.muted ? 0 : typeof conf.volume === "undefined" ? storage.volume * 1 : conf.volume, video: {}, // states disabled: false, finished: false, loading: false, muted: storage.muted == "true" || conf.muted, paused: false, playing: false, ready: false, splash: false, rtl: isRTL, // methods // hijack: function(hijack) { try { api.engine.suspendEngine(); } catch (e) { /* */ } api.hijacked = hijack; }, release: function() { try { api.engine.resumeEngine(); } catch (e) { /* */ } api.hijacked = false; }, debug: function() { if (!conf.debug) return; console.log.apply(console, ['DEBUG'].concat([].slice.call(arguments))); }, load: function(video, callback) { if (api.error || api.loading) return; api.video = {}; api.finished = false; video = video || conf.clip; // resolve URL video = extend({}, urlResolver.resolve(video, conf.clip.sources)); if (api.playing || api.engine) video.autoplay = true; var engineImpl = selectEngine(video); if (!engineImpl) return setTimeout(function() { api.trigger("error", [api, { code: flowplayer.support.flashVideo ? 5 : 10 }]); }) && api; if (!engineImpl.engineName) throw new Error('engineName property of factory should be exposed'); if (!api.engine || engineImpl.engineName !== api.engine.engineName) { api.ready = false; if (api.engine) { api.engine.unload(); api.conf.autoplay = true; } engine = api.engine = engineImpl(api, root); api.one('ready', function() { setTimeout(function() { if (api.muted) api.mute(true, true); else engine.volume(api.volumeLevel); }); }); } extend(video, engine.pick(video.sources.filter(function(source) { // Filter out sources explicitly configured for some other engine if (!source.engine) return true; return source.engine === engine.engineName; }))); if (video.src) { var e = api.trigger('load', [api, video, engine], true); if (!e.defaultPrevented) { api.ready = false; engine.load(video); // callback if (isFunction(video)) callback = video; if (callback) api.one("ready", callback); } else { api.loading = false; } } return api; }, pause: function(fn) { if (api.hijacked) return api.hijacked.pause(fn) | api; if (api.ready && !api.seeking && !api.loading) { engine.pause(); api.one("pause", fn); } return api; }, resume: function() { var ev = api.trigger('beforeresume', [api], true); if (ev.defaultPrevented) return; if (api.hijacked) return api.hijacked.resume() | api; if (api.ready && api.paused) { engine.resume(); // Firefox (+others?) does not fire "resume" after finish if (api.finished) { api.trigger("resume", [api]); api.finished = false; } } return api; }, toggle: function() { return api.ready ? api.paused ? api.resume() : api.pause() : api.load(); }, /* seek(1.4) -> 1.4s time seek(true) -> 10% forward seek(false) -> 10% backward */ seek: function(time, callback) { if (typeof time == "boolean") { var delta = api.conf.seekStep || api.video.duration * 0.1; time = api.video.time + (time ? delta : -delta); time = Math.min(Math.max(time, 0), api.video.duration - 0.1); } if (typeof time === 'undefined') return api; if (api.hijacked) return api.hijacked.seek(time, callback) | api; if (api.ready) { lastSeekPosition = time; var ev = api.trigger('beforeseek', [api, time], true); if (!ev.defaultPrevented) { engine.seek(time); if (isFunction(callback)) api.one("seek", callback); } else { api.seeking = false; common.toggleClass(root, 'is-seeking', api.seeking); // remove loading indicator } } return api; }, /* seekTo(1) -> 10% seekTo(2) -> 20% seekTo(3) -> 30% ... seekTo() -> last position */ seekTo: function(position, fn) { if (position === undefined) return api.seek(lastSeekPosition, fn); if (api.video.seekOffset !== undefined) { // Live stream return api.seek(api.video.seekOffset + (api.video.duration - api.video.seekOffset) * 0.1 * position, fn); } return api.seek(api.video.duration * 0.1 * position, fn); }, mute: function(flag, skipStore) { if (flag === undefined) flag = !api.muted; api.muted = flag; if (!skipStore) { storage.muted = flag; storage.volume = !isNaN(storage.volume) ? storage.volume : conf.volume; // make sure storage has volume } if (typeof engine.mute !== 'undefined') engine.mute(flag); else { api.volume(flag ? 0 : storage.volume, true); api.trigger("mute", [api, flag]); } return api; }, volume: function(level, skipStore) { if (api.ready) { level = Math.min(Math.max(level, 0), 1); if (!skipStore) storage.volume = level; engine.volume(level); } return api; }, speed: function(val, callback) { if (api.ready) { // increase / decrease if (typeof val == "boolean") { val = conf.speeds[conf.speeds.indexOf(api.currentSpeed) + (val ? 1 : -1)] || api.currentSpeed; } engine.speed(val); if (callback) root.one("speed", callback); } return api; }, stop: function() { if (api.ready) { api.pause(); if (!api.live || api.dvr) { api.seek(0, function() { api.trigger("stop", [api]); }); } else { api.trigger("stop", [api]); } } return api; }, unload: function() { if (conf.splash) { api.trigger("unload", [api]); if (engine) { engine.unload(); api.engine = engine = 0; } } else { api.stop(); } return api; }, shutdown: function() { api.unload(); api.trigger('shutdown', [api]); bean.off(root); delete instances[root.getAttribute('data-flowplayer-instance-id')]; root.removeAttribute('data-flowplayer-instance-id'); }, disable: function(flag) { if (flag === undefined) flag = !api.disabled; if (flag != api.disabled) { api.disabled = flag; api.trigger("disable", flag); } return api; }, registerExtension: function(jsUrls, cssUrls) { jsUrls = jsUrls || []; cssUrls = cssUrls || []; if (typeof jsUrls === 'string') jsUrls = [jsUrls]; if (typeof cssUrls === 'string') cssUrls = [cssUrls]; jsUrls.forEach(function(url) { api.extensions.js.push(url); }); cssUrls.forEach(function(url) { api.extensions.css.push(url); }); } }; api.conf = extend(api.conf, conf); api.extensions = { js: [], css: [] }; flowplayer.extensions.forEach(function(i) { api.registerExtension(i[0], i[1]); }); /* event binding / unbinding */ events(api); var selectEngine = function(clip) { var engine; var engines = flowplayer.engines; if (conf.engine) { var eng = engines.filter(function(e) { return e.engineName === conf.engine; })[0]; if (eng && clip.sources.some(function(source) { if (source.engine && source.engine !== eng.engineName) return false; return eng.canPlay(source.type, api.conf); })) return eng; } if (conf.enginePreference) engines = flowplayer.engines.filter(function(one) { return conf.enginePreference.indexOf(one.engineName) > -1; }).sort(function(a, b) { return conf.enginePreference.indexOf(a.engineName) - conf.enginePreference.indexOf(b.engineName); }); clip.sources.some(function(source) { var eng = engines.filter(function(engine) { if (source.engine && source.engine !== engine.engineName) return false; return engine.canPlay(source.type, api.conf); }).shift(); if (eng) engine = eng; return !!eng; }); return engine; }; /*** Behaviour ***/ if (!root.getAttribute('data-flowplayer-instance-id')) { // Only bind once root.setAttribute('data-flowplayer-instance-id', playerCount++); api.on('boot', function() { var support = flowplayer.support; // splash if (conf.splash || common.hasClass(root, "is-splash") || !support.firstframe) { api.forcedSplash = !conf.splash && !common.hasClass(root, "is-splash"); api.splash = true; if (!conf.splash) conf.splash = true; common.addClass(root, "is-splash"); } if (conf.splash) common.find('video', root).forEach(common.removeNode); if (conf.dvr || conf.live || common.hasClass(root, 'is-live')) { api.live = conf.live = true; api.dvr = conf.dvr = !!conf.dvr || common.hasClass(root, 'is-dvr'); common.addClass(root, 'is-live'); common.toggleClass(root, 'is-dvr', api.dvr); } // extensions extensions.forEach(function(e) { e(api, root); }); // instances instances.push(api); // start if (conf.splash) api.unload(); else api.load(); // disabled if (conf.disabled) api.disable(); // initial callback api.one("ready", callback); api.one('shutdown', function() { root.className = originalClass; }); }).on("load", function(e, api, video) { // unload others if (conf.splash) { common.find('.flowplayer.is-ready,.flowplayer.is-loading').forEach(function(el) { var playerId = el.getAttribute('data-flowplayer-instance-id'); if (playerId === root.getAttribute('data-flowplayer-instance-id')) return; var a = instances[Number(playerId)]; if (a && a.conf.splash) a.unload(); }); } // loading common.addClass(root, "is-loading"); api.loading = true; if (typeof video.live !== 'undefined' || typeof video.dvr !== 'undefined') { common.toggleClass(root, 'is-live', video.dvr || video.live); common.toggleClass(root, 'is-dvr', !!video.dvr); api.live = video.dvr || video.live; api.dvr = !!video.dvr; } }).on("ready", function(e, api, video) { video.time = 0; api.video = video; common.removeClass(root, "is-loading"); api.loading = false; // saved state if (api.muted) api.mute(true, true); else api.volume(api.volumeLevel); // see https://github.com/flowplayer/flowplayer/issues/479 var hlsFix = api.conf.hlsFix && /mpegurl/i.exec(video.type); common.toggleClass(root, 'hls-fix', !!hlsFix); }).on("unload", function() { common.removeClass(root, "is-loading"); api.loading = false; }).on("ready unload", function(e) { var is_ready = e.type == "ready"; common.toggleClass(root, 'is-splash', !is_ready); common.toggleClass(root, 'is-ready', is_ready); api.ready = is_ready; api.splash = !is_ready; }).on("progress", function(e, api, time) { api.video.time = time; }).on('buffer', function(e, api, buffered) { api.video.buffer = typeof buffered === 'number' ? buffered : buffered.length ? buffered[buffered.length - 1].end : 0; }).on("speed", function(e, api, val) { api.currentSpeed = val; }).on("volume", function(e, api, level) { api.volumeLevel = Math.round(level * 100) / 100; if (api.muted && level) api.mute(false); }).on("beforeseek seek", function(e) { api.seeking = e.type == "beforeseek"; common.toggleClass(root, 'is-seeking', api.seeking); }).on("ready pause resume unload finish stop", function(e) { // PAUSED: pause / finish api.paused = /pause|finish|unload|stop/.test(e.type); api.paused = api.paused || e.type === 'ready' && !conf.autoplay && !api.playing; // the opposite api.playing = !api.paused; // CSS classes common.toggleClass(root, 'is-paused', api.paused); common.toggleClass(root, 'is-playing', api.playing); // sanity check if (!api.load.ed) api.pause(); }).on("finish", function() { api.finished = true; }).on("error", function() { }); } // boot api.trigger('boot', [api, root]); return api; }