From b60df56157ee1fd0bd4938799bac05a62fda91a1 Mon Sep 17 00:00:00 2001 From: lookshe Date: Sat, 14 Mar 2015 20:45:20 +0100 Subject: initial commit from working version --- client-website/js/symple.player.js | 610 +++++++++++++++++++++++++++++++++++++ 1 file changed, 610 insertions(+) create mode 100644 client-website/js/symple.player.js (limited to 'client-website/js/symple.player.js') 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: '\ +
\ +
\ +
\ +
\ +
\ +
\ + Play\ + Stop\ + Fullscreen\ +
\ +
' + + }, 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('

' + message + '

').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 download Firefox .'); + } + } + + + // + // 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(); + } + } + */ -- cgit v1.2.3