610 lines
21 KiB
JavaScript
610 lines
21 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
*/
|