aboutsummaryrefslogtreecommitdiffstats
path: root/client-website/js/symple.player.js
diff options
context:
space:
mode:
Diffstat (limited to 'client-website/js/symple.player.js')
-rw-r--r--client-website/js/symple.player.js610
1 files changed, 610 insertions, 0 deletions
diff --git a/client-website/js/symple.player.js b/client-website/js/symple.player.js
new file mode 100644
index 0000000..111d33f
--- /dev/null
+++ b/client-website/js/symple.player.js
@@ -0,0 +1,610 @@
+Symple.Media = {
+ engines: {}, // Object containing references for candidate selection
+
+ registerEngine: function(engine) {
+ Symple.log('Register media engine: ', engine)
+ if (!engine.name || typeof engine.preference == 'undefined' || typeof engine.support == 'undefined') {
+ Symple.log('Cannot register invalid engine: ', engine)
+ return false;
+ }
+ this.engines[engine.id] = engine;
+ return true;
+ },
+
+ hasEngine: function(id) {
+ return typeof this.engines[id] == 'object';
+ },
+
+ // Checks support for a given engine
+ supportsEngine: function(id) {
+ // Check support for engine
+ return !!(this.hasEngine(id) && this.engines[id].support);
+ },
+
+ // Checks support for a given format
+ supportsFormat: function(format) {
+ // Check support for engine
+ return !!preferredEngine(format);
+ },
+
+ // Returns a list of compatible engines sorted by preference
+ // The optional format argument further filters by engines
+ // which don't support the given media format.
+ compatibleEngines: function(format) {
+ var arr = [], engine;
+ // Reject non supported or disabled
+ for (var item in this.engines) {
+ engine = this.engines[item];
+ if (engine.preference == 0)
+ continue;
+ Symple.log('Symple Media: Supported: ', engine.name, engine.support)
+ if (engine.support == true)
+ arr.push(engine)
+ }
+ // Sort by preference
+ arr.sort(function (a, b) {
+ if (a.preference < b.preference) return 1;
+ if (a.preference > b.preference) return -1;
+ });
+ return arr
+ },
+
+ // Returns the highest preference compatible engine
+ // The optional format argument further filters by engines
+ // which don't support the given media format.
+ preferredCompatibleEngine: function(format) {
+ var arr = this.compatibleEngines(format), engine;
+ engine = arr.length ? arr[0] : null;
+ Symple.log('Symple Media: Preferred Engine: ', engine);
+ return engine;
+ },
+
+ // Returns the optimal video resolution for the current device
+ // TODO: Different aspect ratios
+ getOptimalVideoResolution: function() {
+ var w = $(window).width();
+ var width = w > 800 ?
+ 800 : w > 640 ?
+ 640 : w > 480 ?
+ 400 : w > 320 ?
+ 320 : w > 240 ?
+ 240 : w > 160 ?
+ 160 : w > 128 ?
+ 128 : 96;
+ var height = width * 0.75;
+ return [width, height];
+ },
+
+ buildURL: function(params) {
+ var query = [], url, addr = params.address;
+ url = addr.scheme + '://' + addr.host + ':' + addr.port + (addr.uri ? addr.uri : '/');
+ for (var p in params) {
+ if (p == 'address')
+ continue;
+ query.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p]));
+ }
+ query.push('rand=' + Math.random());
+ url += '?';
+ url += query.join("&");
+ return url;
+
+ },
+
+ // Rescales video dimensions maintaining perspective
+ // TODO: Different aspect ratios
+ rescaleVideo: function(srcW, srcH, maxW, maxH) {
+ //Symple.log('Symple Player: Rescale Video: ', srcW, srcH, maxW, maxH);
+ var maxRatio = maxW / maxH;
+ var srcRatio = 1.33; //srcW / srcH;
+ if (srcRatio < maxRatio) {
+ srcH = maxH;
+ srcW = srcH * srcRatio;
+ } else {
+ srcW = maxW;
+ srcH = srcW / srcRatio;
+ }
+ return [srcW, srcH];
+ },
+
+ // Basic checking for ICE style streaming candidates
+ // TODO: Latency checks and best candidate switching
+ checkCandidate: function(url, fn) {
+ Symple.log('Symple Media: Checking candidate: ', url);
+
+ var xhr;
+ if (window.XMLHttpRequest) {
+ xhr = new XMLHttpRequest();
+ } else if (window.ActiveXObject) {
+ xhr = new ActiveXObject("Microsoft.XMLHTTP");
+ } else {
+ fn(url, false);
+ return;
+ }
+
+ xhr.onreadystatechange = function() {
+ //Symple.log('Symple Media: Candidate state', xhr.readyState, xhr.status);
+
+ if (xhr.readyState == 2) {
+ if (fn) {
+ Symple.log('Symple Media: Candidate result: ', xhr.readyState, xhr.status);
+ fn(url, xhr.status == 200);
+ fn = null;
+
+ // Safari on windows crashes when abort is called from inside
+ // the onreadystatechange callback.
+ setTimeout(function() {
+ xhr.abort();
+ }, 0);
+ }
+ }
+ else if (xhr.readyState == 4/* && xhr.status != 0*/) {
+ if (fn) {
+ Symple.log('Symple Media: Candidate result: ', xhr.readyState, xhr.status);
+ fn(url, /*xhr.status == 200*/true);
+ fn = null;
+ }
+ }
+ };
+ xhr.open('GET', url, true);
+ xhr.send(null);
+ },
+};
+
+// ----------------------------------------------------------------------------
+// Symple Player
+//
+// Online video streaming for everyone
+// Requires JQuery
+//
+Symple.Player = Symple.Class.extend({
+ init: function(options) {
+ // TODO: Use our own options extend
+ this.options = $.extend({ //Symple.extend({
+ htmlRoot: '/javascripts/symple',
+ element: '.symple-player:first',
+
+ format: 'MJPEG', // The media format to use (MJPEG, FLV, Speex, ...)
+ engine: undefined, // Engine class name, can be specified or auto detected
+
+ //screenWidth: '100%', // player screen css width (percentage or pixel value)
+ //screenHeight: '100%', // player screen css height (percentage or pixel value)
+ //showStatus: false,
+ //assertSupport: false, // throws an exception if no browser support for given engine
+
+ // Callbacks
+ onCommand: function(player, cmd) { },
+ onStateChange: function(player, state) { },
+
+ // Markup
+ template: '\
+ <div class="symple-player">\
+ <div class="symple-player-message"></div>\
+ <div class="symple-player-status"></div>\
+ <div class="symple-player-loading"></div>\
+ <div class="symple-player-screen"></div>\
+ <div class="symple-player-controls">\
+ <a class="play-btn" rel="play" href="#">Play</a>\
+ <a class="stop-btn" rel="stop" href="#">Stop</a>\
+ <a class="fullscreen-btn" rel="fullscreen" href="#">Fullscreen</a>\
+ </div>\
+ </div>'
+
+ }, options);
+
+ this.element = $(this.options.element);
+ /*if (!this.element.hasClass('symple-player')) {
+ this.element.html(this.options.template);
+ this.element = this.element.children('.symple-player:first');
+ }
+ if (!this.element.length)
+ throw 'Player element not found';*/
+
+ // changed
+ //this.screen = this.element.find('.symple-player-screen');
+ this.screen = this.element;
+ if (!this.screen.length)
+ throw 'Player screen element not found';
+
+ // Depreciated: Screen is always 100% unless speified otherwise via CSS
+ //if (this.options.screenWidth)
+ // this.screen.width(this.options.screenWidth);
+ //if (this.options.screenHeight)
+ // this.screen.height(this.options.screenHeight);
+
+ // changed
+ /*this.message = this.element.find('.symple-player-message')
+ if (!this.message.length)
+ throw 'Player message element not found';*/
+
+ // Try to choose the best engine if none was given
+ if (typeof this.options.engine == 'undefined') {
+ var engine = Symple.Media.preferredCompatibleEngine(this.options.format);
+ if (engine)
+ this.options.engine = engine.id;
+ }
+
+ this.bindEvents();
+ this.playing = false;
+
+ Symple.log(this.options.template)
+
+ //this.setState('stopped');
+ //var self = this;
+ //$(window).resize(function() {
+ // self.refresh();
+ //});
+ },
+
+ setup: function() {
+ var id = this.options.engine;
+
+ // Ensure the engine is configured
+ if (!id)
+ throw "Streaming engine not configured. Please set 'options.engine'";
+
+ // Ensure the engine exists
+ if (!Symple.Media.hasEngine(id))
+ throw "Streaming engine not available: " + id;
+ if (typeof Symple.Player.Engine[id] == 'undefined')
+ throw "Streaming engine not found: " + id;
+
+ // Ensure the engine is supported
+ if (!Symple.Media.supportsEngine(id))
+ throw "Streaming engine not supported: " + id;
+
+ // Instantiate the engine
+ this.engine = new Symple.Player.Engine[id](this);
+ this.engine.setup();
+
+ this.element.addClass('engine-' + id.toLowerCase())
+ },
+
+ //
+ // Player Controls
+ //
+ play: function(params) {
+ Symple.log('Symple Player: Play: ', params)
+ try {
+ if (!this.engine)
+ this.setup();
+
+ if (this.state != 'playing' //&&
+ // The player may be set to loading state by the
+ // outside application before play is called.
+ //this.state != 'loading'
+ ) {
+ this.setState('loading');
+ this.engine.play(params); // engine updates state to playing
+ }
+ } catch (e) {
+ this.setState('error');
+ this.displayMessage('error', e)
+ throw e;
+ }
+ },
+
+ stop: function() {
+ Symple.log('Symple Player: Stop')
+ if (this.state != 'stopped') {
+ if (this.engine)
+ this.engine.stop(); // engine updates state to stopped
+ }
+ },
+
+ muteLocal: function() {
+ this.engine.muteLocal();
+ },
+
+ toggleVideo: function() {
+ this.engine.toggleVideo();
+ },
+
+ toggleAudio: function() {
+ this.engine.toggleAudio();
+ },
+
+ fullscreenVideo: function() {
+ this.engine.fullscreenVideo();
+ },
+
+ destroy: function() {
+ if (this.engine)
+ this.engine.destroy();
+ this.element.remove();
+ },
+
+ setState: function(state, message) {
+ Symple.log('Symple Player: Set state:', this.state, '=>', state, message)
+ if (this.state == state)
+ return;
+
+ this.state = state;
+ this.displayStatus(null);
+ this.playing = state == 'playing';
+ if (message)
+ this.displayMessage(state == 'error' ? 'error' : 'info', message);
+ else
+ this.displayMessage(null);
+ this.element.removeClass('state-stopped state-loading state-playing state-paused state-error');
+ this.element.addClass('state-' + state);
+ //this.refresh();
+ this.options.onStateChange(this, state, message);
+ },
+
+ //
+ // Helpers
+ //
+ displayStatus: function(data) {
+ this.element.find('.symple-player-status').html(data ? data : '');
+ },
+
+ // Display an overlayed player message
+ // error, warning, info
+ displayMessage: function(type, message) {
+ Symple.log('Symple Player: Display message:', type, message)
+ // change
+ /*if (message) {
+ this.message.html('<p class="' + type + '-message">' + message + '</p>').show();
+ }
+ else {
+ this.message.html('').hide();
+ }*/
+ },
+
+ bindEvents: function() {
+ var self = this;
+ this.element.find('.symple-player-controls a').unbind().bind('click tap', function() {
+ self.sendCommand(this.rel, $(this));
+ return false;
+ })
+ },
+
+ sendCommand: function(cmd, e) {
+ if (!this.options.onCommand ||
+ !this.options.onCommand(this, cmd, e)) {
+
+ // If there is no command callback function or the callback returns
+ // false then we process these default behaviours.
+ switch(cmd) {
+ case 'play':
+ this.play();
+ break;
+ case 'stop':
+ this.stop();
+ break;
+ case 'muteLocal':
+ this.muteLocal();
+ break;
+ case 'toggleVideo':
+ this.toggleVideo();
+ break;
+ case 'toggleAudio':
+ this.toggleAudio();
+ break;
+ case 'fullscreen':
+ this.toggleFullScreen();
+ break;
+ }
+ }
+ },
+
+ getButton: function(cmd) {
+ return this.element.find('.symple-player-controls [rel="' + cmd + '"]');
+ },
+
+ // TODO: Toggle actual player element
+ toggleFullScreen: function() {
+ if (Symple.runVendorMethod(document, "FullScreen") || Symple.runVendorMethod(document, "IsFullScreen")) {
+ Symple.runVendorMethod(document, "CancelFullScreen");
+ }
+ else {
+ Symple.runVendorMethod(this.element[0], "RequestFullScreen");
+ }
+ }
+})
+
+
+// -----------------------------------------------------------------------------
+// Player Engine Interface
+//
+Symple.Player.Engine = Symple.Class.extend({
+ init: function(player) {
+ this.player = player;
+ this.fps = 0;
+ this.seq = 0;
+ },
+
+ support: function() { return true; },
+ setup: function() {},
+ destroy: function() {},
+ play: function(params) {
+ this.params = params || {};
+ if (!this.params.url && typeof(params.address) == 'object')
+ this.params.url = this.buildURL();
+ },
+ stop: function() {},
+ pause: function(flag) {},
+ mute: function(flag) {},
+ muteLocal: function() {},
+ toggleAudio: function() {},
+ toggleVideo: function() {},
+ toggleFullscreen: function() {},
+ //refresh: function() {},
+
+ setState: function(state, message) {
+ this.player.setState(state, message);
+ },
+
+ setError: function(error) {
+ Symple.log('Symple Player Engine: Error:', error);
+ this.setState('error', error);
+ },
+
+ onRemoteCandidate: function(candidate) {
+ Symple.log('Symple Player Engine: Remote candidates not supported.');
+ },
+
+ updateFPS: function() {
+ if (typeof this.prevTime == 'undefined')
+ this.prevTime = new Date().getTime();
+ if (this.seq > 0) {
+ var now = new Date().getTime();
+ this.delta = this.prevTime ? now - this.prevTime : 0;
+ this.fps = (1000.0 / this.delta).toFixed(3);
+ this.prevTime = now;
+ }
+ this.seq++;
+ },
+
+ displayFPS: function() {
+ this.updateFPS()
+ this.player.displayStatus(this.delta + " ms (" + this.fps + " fps)");
+ },
+
+ buildURL: function() {
+ if (!this.params)
+ throw 'Streaming parameters not set'
+ if (!this.params.address)
+ this.params.address = this.player.options.address;
+ return Symple.Media.buildURL(this.params);
+ }
+});
+
+
+
+
+ /*
+ refresh: function() {
+ if (this.engine)
+ this.engine.refresh();
+ },
+
+ refresh: function() {
+ var css = { position: 'relative' };
+ if (this.options.screenWidth == '100%' ||
+ this.options.screenHeight == '100%') {
+ var size = this.rescaleVideo(this.screen.outerWidth(), this.screen.outerHeight(),
+ this.element.outerWidth(), this.element.outerHeight());
+ css.width = size[0];
+ css.height = size[1];
+ css.left = this.element.outerWidth() / 2 - css.width / 2;
+ css.top = this.element.outerHeight() / 2 - css.height / 2;
+ css.left = css.left ? css.left : 0;
+ css.top = css.top ? css.top : 0;
+ if (this.engine)
+ this.engine.resize(css.width, css.height);
+ }
+ else {
+ css.width = this.options.screenWidth;
+ css.height = this.options.screenHeight;
+ css.left = this.element.outerWidth() / 2 - this.options.screenWidth / 2;
+ css.top = this.element.outerHeight() / 2 - this.options.screenHeight / 2;
+ css.left = css.left ? css.left : 0;
+ css.top = css.top ? css.top : 0;
+ }
+ Symple.log('Symple Player: Setting Size: ', css);
+
+ this.screen.css(css);
+
+ //var e = this.element.find('#player-screen');
+ //Symple.log('refresh: scaled:', size)
+ Symple.log('refresh: screenWidth:', this.options.screenWidth)
+ Symple.log('refresh: width:', this.screen.width())
+ Symple.log('refresh: screenHeight:', this.options.screenHeight)
+ Symple.log('refresh: height:', this.screen.height())
+ Symple.log('refresh: css:', css)
+ },
+
+ getBestEngineForFormat: function(format) {
+ var ua = navigator.userAgent;
+ var isMobile = Symple.isMobileDevice();
+ var engine = null;
+
+ // TODO: Use this function with care as it is not complete.
+ // TODO: Register engines which we can iterate to check support.
+ // Please feel free to update this function with your test results!
+
+ //
+ // MJPEG
+ //
+ if (format == "MJPEG") {
+
+
+ // Most versions of Safari has great MJPEG support.
+ // BUG: The MJPEG socket is not closed until the page is refreshed.
+ if (ua.match(/(Safari|iPhone|iPod|iPad)/)) {
+
+ // iOS 6 breaks native MJPEG support.
+ if (Symple.iOSVersion() > 6)
+ engine = 'MJPEGBase64MXHR';
+ else
+ engine = 'MJPEG';
+ }
+
+ // Firefox to the rescue! Nag user's to install firefox if MJPEG
+ // streaming is unavailable.
+ else if(ua.match(/(Mozilla)/))
+ engine = 'MJPEG';
+
+ // Android's WebKit has disabled multipart HTTP requests for some
+ // reason: http://code.google.com/p/android/issues/detail?id=301
+ else if(ua.match(/(Android)/))
+ engine = 'MJPEGBase64MXHR';
+
+ // BlackBerry doesn't understand multipart/x-mixed-replace ... duh
+ else if(ua.match(/(BlackBerry)/))
+ engine = 'PseudoMJPEG';
+
+ // Opera does not support mjpeg MJPEG, but their home grown image
+ // processing library is super fast so pseudo streaming is nearly
+ // as fast as other native MJPEG implementations!
+ else if(ua.match(/(Opera)/))
+ engine = isMobile ? 'MJPEGBase64MXHR' : 'Flash'; //PseudoMJPEG
+
+ // Internet Explorer... nuff said
+ else if(ua.match(/(MSIE)/))
+ engine = isMobile ? 'PseudoMJPEG' : 'Flash';
+
+ // Display a nag screen to install a real browser if we are in
+ // pseudo streaming mode.
+ if (engine == 'PseudoMJPEG') { //!forcePseudo &&
+ this.displayMessage('warning',
+ 'Your browser does not support native streaming so playback preformance will be severely limited. ' +
+ 'For the best streaming experience please <a href="http://www.mozilla.org/en-US/firefox/">download Firefox</a> .');
+ }
+ }
+
+
+ //
+ // FLV
+ //
+ else if (format == "FLV") {
+ if (Symple.isMobileDevice())
+ throw 'FLV not supported on mobile devices.'
+ engine = 'Flash';
+ }
+
+ else
+ throw 'Unknown media format: ' + format
+
+ return engine;
+ if (!document.fullscreenElement && // alternative standard method
+ !document.mozFullScreenElement && !document.webkitFullscreenElement) { // current working methods
+ if (document.documentElement.requestFullscreen) {
+ document.documentElement.requestFullscreen();
+ } else if (document.documentElement.mozRequestFullScreen) {
+ document.documentElement.mozRequestFullScreen();
+ } else if (document.documentElement.webkitRequestFullscreen) {
+ document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+ }
+ }
+ else {
+ if (document.cancelFullScreen) {
+ document.cancelFullScreen();
+ } else if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen();
+ } else if (document.webkitCancelFullScreen) {
+ document.webkitCancelFullScreen();
+ }
+ }
+ */