Browse Source

Applied jsbeautify target to all Javascript files making them use same syntax style and tab indentation everywhere.

pull/37/head
Simon Eisenmann 12 years ago
parent
commit
a0733b6555
  1. 338
      static/js/app.js
  2. 3
      static/js/base.js
  3. 922
      static/js/controllers/chatroomcontroller.js
  4. 39
      static/js/controllers/controllers.js
  5. 1477
      static/js/controllers/mediastreamcontroller.js
  6. 91
      static/js/controllers/roomchangecontroller.js
  7. 34
      static/js/controllers/statusmessagecontroller.js
  8. 6
      static/js/directives/audiolevel.js
  9. 593
      static/js/directives/audiovideo.js
  10. 218
      static/js/directives/buddylist.js
  11. 889
      static/js/directives/chat.js
  12. 79
      static/js/directives/directives.js
  13. 12
      static/js/directives/fileinfo.js
  14. 36
      static/js/directives/onenter.js
  15. 36
      static/js/directives/onescape.js
  16. 86
      static/js/directives/roombar.js
  17. 34
      static/js/directives/screenshare.js
  18. 417
      static/js/directives/settings.js
  19. 6
      static/js/directives/socialshare.js
  20. 158
      static/js/directives/usability.js
  21. 157
      static/js/filters/buddyimagesrc.js
  22. 26
      static/js/filters/displayconference.js
  23. 82
      static/js/filters/displayname.js
  24. 37
      static/js/filters/filters.js
  25. 331
      static/js/main.js
  26. 542
      static/js/mediastream/api.js
  27. 388
      static/js/mediastream/connector.js
  28. 586
      static/js/mediastream/peercall.js
  29. 426
      static/js/mediastream/peerconference.js
  30. 474
      static/js/mediastream/peerconnection.js
  31. 8
      static/js/mediastream/peerscreenshare.js
  32. 2
      static/js/mediastream/peerxfer.js
  33. 78
      static/js/mediastream/tokens.js
  34. 351
      static/js/mediastream/usermedia.js
  35. 269
      static/js/mediastream/utils.js
  36. 1319
      static/js/mediastream/webrtc.js
  37. 209
      static/js/modules/angular-humanize.js
  38. 26
      static/js/services/alertify.js
  39. 34
      static/js/services/appdata.js
  40. 177
      static/js/services/buddydata.js
  41. 902
      static/js/services/buddylist.js
  42. 120
      static/js/services/desktopnotify.js
  43. 46
      static/js/services/enrichmessage.js
  44. 137
      static/js/services/fastscroll.js
  45. 29
      static/js/services/filedata.js
  46. 12
      static/js/services/filedownload.js
  47. 40
      static/js/services/filetransfer.js
  48. 54
      static/js/services/fileupload.js
  49. 169
      static/js/services/mediasources.js
  50. 616
      static/js/services/mediastream.js
  51. 346
      static/js/services/playsound.js
  52. 92
      static/js/services/randomgen.js
  53. 34
      static/js/services/safeapply.js
  54. 18
      static/js/services/safedisplayname.js
  55. 14
      static/js/services/safemessage.js
  56. 162
      static/js/services/services.js
  57. 2
      static/js/services/toastr.js
  58. 154
      static/js/services/translation.js
  59. 573
      static/js/services/videolayout.js
  60. 90
      static/js/services/videowaiter.js

338
static/js/app.js

@ -19,177 +19,177 @@ @@ -19,177 +19,177 @@
*
*/
define([
'require',
'jquery',
'underscore',
'angular',
'modernizr',
'moment',
'services/services',
'directives/directives',
'filters/filters',
'controllers/controllers',
'translation/languages',
'ui-bootstrap',
'angular-sanitize',
'angular-animate',
'angular-humanize',
'angular-route',
'mobile-events',
'dialogs'
'require',
'jquery',
'underscore',
'angular',
'modernizr',
'moment',
'services/services',
'directives/directives',
'filters/filters',
'controllers/controllers',
'translation/languages',
'ui-bootstrap',
'angular-sanitize',
'angular-animate',
'angular-humanize',
'angular-route',
'mobile-events',
'dialogs'
], function(require, $, _, angular, modernizr, moment, services, directives, filters, controllers, languages) {
// Simple and fast split based URL query parser based on location.search. We require this before the
// angular App is bootstrap to control initialization parameters like translation based on URL parameters.
var urlQuery = (function() {
return (function(a) {
if (a === "") {
return {};
}
var b = {};
for (var i = 0; i < a.length; ++i) {
var p = a[i].split('=');
if (p.length != 2) {
continue;
}
b[p[0]] = window.decodeURIComponent(p[1].replace(/\+/g, " "));
}
return b;
})(window.location.search.substr(1).split("&"));
}());
var initialize = function(ms) {
var modules = ['ui.bootstrap', 'ngSanitize', 'ngAnimate', 'ngHumanize', 'ngRoute', 'dialogs'];
if (ms && ms.length) {
_.each(ms, function(module) {
modules.push(module);
});
}
var app = angular.module('app', modules);
services.initialize(app);
directives.initialize(app);
filters.initialize(app);
controllers.initialize(app);
app.config(["$compileProvider", "$locationProvider", "$routeProvider", function($compileProvider, $locationProvider, $routeProvider) {
// Allow angular to use filesystem: hrefs which would else be prefixed with unsafe:.
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|filesystem|blob):/);
$compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|filesystem|blob):|data:image\//);
// Setup routing
$routeProvider.when("/:room", {});
// Use HTML5 routing.
$locationProvider.html5Mode(true);
}]);
app.run(["$rootScope", "mediaStream", "translation", function($rootScope, mediaStream, translation) {
translation.inject($rootScope);
console.log("Initializing ...");
mediaStream.initialize($rootScope, translation);
}]);
app.constant("availableLanguages", languages);
angular.element(document).ready(function() {
var globalContext = JSON.parse($("#globalcontext").text());
app.constant("globalContext", globalContext);
// Configure language.
var lang = (function() {
var lang = "en";
var wanted = [];
var html = document.getElementsByTagName("html")[0];
// Get from storage.
if (modernizr.localstorage) {
var lsl = localStorage.getItem("mediastream-language");
if (lsl && lsl !== "undefined") {
wanted.push(lsl);
}
}
// Get from query.
var qsl = urlQuery.lang;
if (qsl) {
wanted.push(qsl);
}
// Expand browser languages with combined fallback.
_.each(globalContext.Languages, function(l) {
wanted.push(l);
if (l.indexOf("-") != -1) {
wanted.push(l.split("-")[0]);
}
});
// Loop through browser languages and use first one we got.
for (var i=0; i<wanted.length; i++) {
if (languages.hasOwnProperty(wanted[i])) {
lang = wanted[i];
break;
}
}
html.setAttribute("lang", lang);
return lang;
}());
// Prepare bootstrap function with injected locale data.
var domain = "messages";
var catalog = domain + "-" + lang;
var bootstrap = function(translationData) {
if (!translationData) {
// Fallback catalog in case translation could not be loaded.
lang = "en";
translationData = {};
translationData.locale_data = {};
translationData.domain = domain;
translationData.locale_data[domain] = {
"": {
"domain": domain,
"lang": lang,
"plural_forms" : "nplurals=2; plural=(n != 1);"
}
};
}
// Set date language too.
moment.lang([lang, "en"]);
// Inject translation data globally.
app.constant("translationData", translationData);
// Bootstrap AngularJS app.
console.log("Bootstrapping ...");
angular.bootstrap(document, ['app']);
};
if (lang !== "en") {
// Load translation file.
//console.log("Loading translation data: " + lang);
$.ajax({
dataType: "json",
url: require.toUrl('translation/'+catalog+'.json'),
success: function(data) {
//console.log("Loaded translation data.");
bootstrap(data);
},
error: function(err, textStatus, errorThrown) {
console.warn("Failed to load translation data " + catalog + ": "+ errorThrown);
bootstrap(null);
}
});
} else {
// No need to load english as this is built in.
bootstrap();
}
});
return app;
};
return {
initialize: initialize
};
// Simple and fast split based URL query parser based on location.search. We require this before the
// angular App is bootstrap to control initialization parameters like translation based on URL parameters.
var urlQuery = (function() {
return (function(a) {
if (a === "") {
return {};
}
var b = {};
for (var i = 0; i < a.length; ++i) {
var p = a[i].split('=');
if (p.length != 2) {
continue;
}
b[p[0]] = window.decodeURIComponent(p[1].replace(/\+/g, " "));
}
return b;
})(window.location.search.substr(1).split("&"));
}());
var initialize = function(ms) {
var modules = ['ui.bootstrap', 'ngSanitize', 'ngAnimate', 'ngHumanize', 'ngRoute', 'dialogs'];
if (ms && ms.length) {
_.each(ms, function(module) {
modules.push(module);
});
}
var app = angular.module('app', modules);
services.initialize(app);
directives.initialize(app);
filters.initialize(app);
controllers.initialize(app);
app.config(["$compileProvider", "$locationProvider", "$routeProvider", function($compileProvider, $locationProvider, $routeProvider) {
// Allow angular to use filesystem: hrefs which would else be prefixed with unsafe:.
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|filesystem|blob):/);
$compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|filesystem|blob):|data:image\//);
// Setup routing
$routeProvider.when("/:room", {});
// Use HTML5 routing.
$locationProvider.html5Mode(true);
}]);
app.run(["$rootScope", "mediaStream", "translation", function($rootScope, mediaStream, translation) {
translation.inject($rootScope);
console.log("Initializing ...");
mediaStream.initialize($rootScope, translation);
}]);
app.constant("availableLanguages", languages);
angular.element(document).ready(function() {
var globalContext = JSON.parse($("#globalcontext").text());
app.constant("globalContext", globalContext);
// Configure language.
var lang = (function() {
var lang = "en";
var wanted = [];
var html = document.getElementsByTagName("html")[0];
// Get from storage.
if (modernizr.localstorage) {
var lsl = localStorage.getItem("mediastream-language");
if (lsl && lsl !== "undefined") {
wanted.push(lsl);
}
}
// Get from query.
var qsl = urlQuery.lang;
if (qsl) {
wanted.push(qsl);
}
// Expand browser languages with combined fallback.
_.each(globalContext.Languages, function(l) {
wanted.push(l);
if (l.indexOf("-") != -1) {
wanted.push(l.split("-")[0]);
}
});
// Loop through browser languages and use first one we got.
for (var i = 0; i < wanted.length; i++) {
if (languages.hasOwnProperty(wanted[i])) {
lang = wanted[i];
break;
}
}
html.setAttribute("lang", lang);
return lang;
}());
// Prepare bootstrap function with injected locale data.
var domain = "messages";
var catalog = domain + "-" + lang;
var bootstrap = function(translationData) {
if (!translationData) {
// Fallback catalog in case translation could not be loaded.
lang = "en";
translationData = {};
translationData.locale_data = {};
translationData.domain = domain;
translationData.locale_data[domain] = {
"": {
"domain": domain,
"lang": lang,
"plural_forms": "nplurals=2; plural=(n != 1);"
}
};
}
// Set date language too.
moment.lang([lang, "en"]);
// Inject translation data globally.
app.constant("translationData", translationData);
// Bootstrap AngularJS app.
console.log("Bootstrapping ...");
angular.bootstrap(document, ['app']);
};
if (lang !== "en") {
// Load translation file.
//console.log("Loading translation data: " + lang);
$.ajax({
dataType: "json",
url: require.toUrl('translation/' + catalog + '.json'),
success: function(data) {
//console.log("Loaded translation data.");
bootstrap(data);
},
error: function(err, textStatus, errorThrown) {
console.warn("Failed to load translation data " + catalog + ": " + errorThrown);
bootstrap(null);
}
});
} else {
// No need to load english as this is built in.
bootstrap();
}
});
return app;
};
return {
initialize: initialize
};
});

3
static/js/base.js

@ -32,5 +32,4 @@ define([ @@ -32,5 +32,4 @@ define([
'rAF',
'humanize',
'sha',
'sjcl'
], function(){});
'sjcl'], function() {});

922
static/js/controllers/chatroomcontroller.js

@ -20,460 +20,472 @@ @@ -20,460 +20,472 @@
*/
define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, moment, templateFileInfo) {
// ChatroomController
return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) {
$scope.outputElement = $(".output", $element);
$scope.inputElement = $(".input", $element);
$scope.bodyElement = $(".chatbody", $element);
var lastSender = null;
var lastDate = null;
var lastMessageContainer = null;
var senderExpired = null;
var isTyping = false;
var isTypingExpired = null;
var peerTypingExpired = null;
var p2p = false;
var scrollAfterInput = false;
// Mark seen on several events.
$scope.bodyElement.on("mouseover mouseenter touchstart", _.debounce(function(event) {
$scope.$parent.seen();
$scope.$apply();
}, 100));
var displayName = safeDisplayName;
var buddyImageSrc = $filter("buddyImageSrc");
var fileInfo = $compile(templateFileInfo);
var knowMessage = {
r: {},
pending: [],
register: function(element, mid, status, received) {
if (mid) {
if (knowMessage.r.hasOwnProperty(mid)) {
console.warn("Duplicate chat message registration.", mid, element, status);
return;
}
var e = knowMessage.r[mid] = {
element: element,
status: status
}
if (e.status) {
element.addClass(e.status);
}
if (received) {
knowMessage.pending.push(mid);
$scope.$emit("submitreceived", mid);
}
}
},
update: function(mid, status) {
var e = knowMessage.r[mid];
if (e) {
if (e.status !== status && status) {
if (e.status) {
e.element.removeClass(e.status);
}
e.status = status;
e.element.addClass(e.status);
if (status === "received" || status === "read") {
// last one - cleanup
delete knowMessage.r[mid];
}
}
}
},
seen: function() {
var pending = knowMessage.pending;
if (pending.length) {
knowMessage.pending = [];
$scope.$emit("submitseen", pending);
_.each(pending, function(mid) {
knowMessage.update(mid, "read");
});
}
}
};
// Make sure that chat links are openend in a new window.
$element.on("click", function(event) {
var elem = $(event.target);
if (elem.is("a")) {
var url = elem.attr("href");
if (url && !elem.attr("download")) {
if (url.match(/mailto:/gi) === null) {
event.preventDefault();
$window.open(elem.attr("href"), "_blank");
}
}
};
});
$scope.$watch("input", function(newvalue) {
$scope.$parent.seen();
$window.clearTimeout(isTypingExpired);
if (!newvalue) {
return;
}
if (!isTyping) {
isTyping = true;
$scope.$emit("typing", {who: "local", status: "start"});
}
isTypingExpired = $window.setTimeout(function() {
isTyping = false;
$scope.$emit("typing", {who: "local", status: "stop"});
}, 4000);
});
$scope.reset = function() {
$scope.input = "";
isTyping = false;
$window.clearTimeout(isTypingExpired);
};
$scope.focus = function() {
$scope.inputElement.focus();
};
$scope.submit = function() {
var input = $scope.input;
if (input) {
scrollAfterInput = true;
$scope.$emit("submit", $scope.input);
$scope.reset();
$scope.focus();
}
};
$scope.canScroll = function() {
var o = $scope.outputElement.get(0);
if ((o.clientHeight - 20) < o.scrollHeight) {
if (!scrollAfterInput && (o.clientHeight + 20) < (o.scrollHeight - o.scrollTop)) {
// Manually scrolled -> do nothing.
} else {
scrollAfterInput = false;
// Return scroll function.
return function() {
o.scrollTop = o.scrollHeight;
};
}
}
return false;
}
$scope.display = function(s, nodes, extra_css, title, picture) {
var container;
var element;
var scroll = this.canScroll();
lastMessageContainer = null;
if (!extra_css) {
extra_css = "";
}
if (s || title || picture) {
container = $('<div class="message ' + extra_css + '"></div>');
if (title) {
container.prepend(title);
}
if (picture) {
container.prepend(picture);
}
lastMessageContainer = $("<ul>").appendTo(container);
if ($.trim(s)) {
element = $("<li>").html(s).appendTo(lastMessageContainer);
}
}
if (nodes) {
if (container) {
// Insert at the end of previously created container.
container.append(nodes);
} else {
$scope.outputElement.append(nodes);
}
if (container && lastMessageContainer) {
lastMessageContainer = $("<ul>").appendTo(container);
}
}
if (container) {
$scope.outputElement.append(container);
}
if (scroll) {
scroll();
}
return element;
};
$scope.$on("display", function(event, s, nodes) {
$scope.display(s, nodes);
});
$scope.append = function(s, nodes) {
if (!lastMessageContainer) {
return;
}
var scroll = this.canScroll();
var li = $("<li>");
li.html(s)
lastMessageContainer.append(li)
if (nodes) {
var parent = lastMessageContainer.parent();
parent.append(nodes);
lastMessageContainer = $("<ul>");
parent.append(lastMessageContainer);
}
if (scroll) {
scroll();
}
return li;
};
$scope.showtime = function(d, format, compare) {
var m;
if (d) {
m = moment(d);
} else {
m = moment();
}
if (!format) {
format = "LLL";
}
var datestring = m.format(format);
if (compare && datestring === compare) {
// Do nothing if compare matches.
return datestring;
}
$scope.display(null, $("<i>" + datestring + "</i>"));
if (!d) {
lastSender = null;
}
return datestring;
};
$scope.showdate = function(d) {
lastDate = $scope.showtime(d, "LL", lastDate);
};
$scope.showmessage = function(from, timestamp, message, nodes) {
var userid = $scope.$parent.$parent.id;
// Prepare message to display.
var s = [];
if (message) {
s.push(message);
$scope.$emit("incoming", message, from, userid);
}
var is_new_message = lastSender !== from;
var is_self = from === userid;
var extra_css = "";
var title = null;
var picture = null;
var showTitleAndPicture = function() {
if ($scope.isgroupchat) {
title = $("<strong>");
title.html(displayName(from, true));
extra_css += "with_name ";
var imgSrc = buddyImageSrc(from);
picture = $('<div class="buddyPicture"><i class="fa fa-user fa-3x"/><img/></div>');
if (imgSrc) {
picture.find("img").attr("src", imgSrc);
}
}
};
if (is_new_message) {
lastSender = from;
$scope.showdate(timestamp);
showTitleAndPicture()
}
var strMessage = s.join(" ");
if (!is_new_message) {
var element = this.append(strMessage, nodes);
if (element) {
return element;
}
showTitleAndPicture();
}
if (is_self) {
extra_css += "is_self";
} else {
extra_css += "is_remote";
}
if (timestamp) {
var ts = $('<div class="timestamp"/>');
ts.text(moment(timestamp).format("H:mm"));
if (nodes) {
nodes = nodes.add(ts);
} else {
nodes = ts;
}
}
return $scope.display(strMessage, nodes, extra_css, title, picture);
};
$scope.$on("seen", function() {
knowMessage.seen();
});
$scope.$on("p2p", function(event, state) {
//console.log("changed p2p state", state, p2p);
var msg;
if (state) {
msg = translation._("Peer to peer chat active.");
} else {
msg = translation._("Peer to peer chat is now off.");
}
$scope.display(null, $("<i class='p2p'><span class='icon-exchange'></span> " + msg + "</i>"));
});
$scope.$on("focus", function() {
$scope.focus();
});
$scope.$on("received", function(event, from, data) {
var userid = $scope.$parent.$parent.id;
var mid = data.Mid || null;
switch (data.Type) {
case "LeftOrJoined":
$scope.showtime(new Date());
if (data.LeftOrJoined === "left") {
$scope.display(null, $("<i>"+ displayName(from) + translation._(" is now offline.") + "</i>"));
} else {
$scope.display(null, $("<i>"+ displayName(from) + translation._(" is now online.") + "</i>"));
}
break;
case "Log":
$scope.showtime(new Date());
$scope.display(null, data.Log);
break;
default:
// Definitions.
var message = null;
var nodes = null;
var fromself = from === userid;
var noop = false;
var element = null;
var timestamp = data.Time;
if (!timestamp) {
timestamp = new Date();
}
// Process internal status messages.
if (data.Status) {
if (!mid && data.Status.Mid) {
mid = data.Status.Mid; // Inner Mid means internal chat status.
}
// Typing notification.
if (data.Status.Typing && !fromself) {
$window.clearTimeout(peerTypingExpired);
$scope.$emit("typing", {who: "peer", status: data.Status.Typing});
if (data.Status.Typing === "stop") {
peerTypingExpired = $window.setTimeout(function() {
$scope.$emit("typing", {who: "peer", status: "no"});
}, 20000);
}
}
// Mid updates.
if (mid && data.Status.State) {
knowMessage.update(mid, data.Status.State);
}
// Mid batch updates.
if (data.Status.SeenMids) {
_.each(data.Status.SeenMids, function(mid) {
knowMessage.update(mid, "received");
});
}
// File offers.
if (data.Status.FileInfo) {
var subscope = $scope.$new();
subscope.info = data.Status.FileInfo;
subscope.from = from;
fileInfo(subscope, function(clonedElement, scope) {
var text = fromself ? translation._("You share file:") : translation._("Incoming file:");
element = $scope.showmessage(from, timestamp, text, clonedElement);
});
noop = true;
}
// Ignore unknown status messages.
if (message === null && nodes === null) {
noop = true;
}
}
// Do nothing when where is nothing.
if (!data.Message && message === null && nodes === null) {
noop = true;
}
if (!noop) {
// Default handling is to use full message with security in place.
if (message === null && nodes === null && data.Message && typeof data.Message == "string") {
message = safeMessage(data.Message);
}
// Show the beast.
element = $scope.showmessage(from, timestamp, message, nodes);
}
if (element && mid && !$scope.isgroupchat) {
knowMessage.register(element, mid, fromself ? "sending" : "unread", !fromself);
}
break;
}
// Reset last sender to allow a new time stamp after a while.
$window.clearTimeout(senderExpired);
senderExpired = $window.setTimeout(function() {
lastSender = null;
}, 61000);
});
}];
// ChatroomController
return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) {
$scope.outputElement = $(".output", $element);
$scope.inputElement = $(".input", $element);
$scope.bodyElement = $(".chatbody", $element);
var lastSender = null;
var lastDate = null;
var lastMessageContainer = null;
var senderExpired = null;
var isTyping = false;
var isTypingExpired = null;
var peerTypingExpired = null;
var p2p = false;
var scrollAfterInput = false;
// Mark seen on several events.
$scope.bodyElement.on("mouseover mouseenter touchstart", _.debounce(function(event) {
$scope.$parent.seen();
$scope.$apply();
}, 100));
var displayName = safeDisplayName;
var buddyImageSrc = $filter("buddyImageSrc");
var fileInfo = $compile(templateFileInfo);
var knowMessage = {
r: {},
pending: [],
register: function(element, mid, status, received) {
if (mid) {
if (knowMessage.r.hasOwnProperty(mid)) {
console.warn("Duplicate chat message registration.", mid, element, status);
return;
}
var e = knowMessage.r[mid] = {
element: element,
status: status
}
if (e.status) {
element.addClass(e.status);
}
if (received) {
knowMessage.pending.push(mid);
$scope.$emit("submitreceived", mid);
}
}
},
update: function(mid, status) {
var e = knowMessage.r[mid];
if (e) {
if (e.status !== status && status) {
if (e.status) {
e.element.removeClass(e.status);
}
e.status = status;
e.element.addClass(e.status);
if (status === "received" || status === "read") {
// last one - cleanup
delete knowMessage.r[mid];
}
}
}
},
seen: function() {
var pending = knowMessage.pending;
if (pending.length) {
knowMessage.pending = [];
$scope.$emit("submitseen", pending);
_.each(pending, function(mid) {
knowMessage.update(mid, "read");
});
}
}
};
// Make sure that chat links are openend in a new window.
$element.on("click", function(event) {
var elem = $(event.target);
if (elem.is("a")) {
var url = elem.attr("href");
if (url && !elem.attr("download")) {
if (url.match(/mailto:/gi) === null) {
event.preventDefault();
$window.open(elem.attr("href"), "_blank");
}
}
};
});
$scope.$watch("input", function(newvalue) {
$scope.$parent.seen();
$window.clearTimeout(isTypingExpired);
if (!newvalue) {
return;
}
if (!isTyping) {
isTyping = true;
$scope.$emit("typing", {
who: "local",
status: "start"
});
}
isTypingExpired = $window.setTimeout(function() {
isTyping = false;
$scope.$emit("typing", {
who: "local",
status: "stop"
});
}, 4000);
});
$scope.reset = function() {
$scope.input = "";
isTyping = false;
$window.clearTimeout(isTypingExpired);
};
$scope.focus = function() {
$scope.inputElement.focus();
};
$scope.submit = function() {
var input = $scope.input;
if (input) {
scrollAfterInput = true;
$scope.$emit("submit", $scope.input);
$scope.reset();
$scope.focus();
}
};
$scope.canScroll = function() {
var o = $scope.outputElement.get(0);
if ((o.clientHeight - 20) < o.scrollHeight) {
if (!scrollAfterInput && (o.clientHeight + 20) < (o.scrollHeight - o.scrollTop)) {
// Manually scrolled -> do nothing.
} else {
scrollAfterInput = false;
// Return scroll function.
return function() {
o.scrollTop = o.scrollHeight;
};
}
}
return false;
}
$scope.display = function(s, nodes, extra_css, title, picture) {
var container;
var element;
var scroll = this.canScroll();
lastMessageContainer = null;
if (!extra_css) {
extra_css = "";
}
if (s || title || picture) {
container = $('<div class="message ' + extra_css + '"></div>');
if (title) {
container.prepend(title);
}
if (picture) {
container.prepend(picture);
}
lastMessageContainer = $("<ul>").appendTo(container);
if ($.trim(s)) {
element = $("<li>").html(s).appendTo(lastMessageContainer);
}
}
if (nodes) {
if (container) {
// Insert at the end of previously created container.
container.append(nodes);
} else {
$scope.outputElement.append(nodes);
}
if (container && lastMessageContainer) {
lastMessageContainer = $("<ul>").appendTo(container);
}
}
if (container) {
$scope.outputElement.append(container);
}
if (scroll) {
scroll();
}
return element;
};
$scope.$on("display", function(event, s, nodes) {
$scope.display(s, nodes);
});
$scope.append = function(s, nodes) {
if (!lastMessageContainer) {
return;
}
var scroll = this.canScroll();
var li = $("<li>");
li.html(s)
lastMessageContainer.append(li)
if (nodes) {
var parent = lastMessageContainer.parent();
parent.append(nodes);
lastMessageContainer = $("<ul>");
parent.append(lastMessageContainer);
}
if (scroll) {
scroll();
}
return li;
};
$scope.showtime = function(d, format, compare) {
var m;
if (d) {
m = moment(d);
} else {
m = moment();
}
if (!format) {
format = "LLL";
}
var datestring = m.format(format);
if (compare && datestring === compare) {
// Do nothing if compare matches.
return datestring;
}
$scope.display(null, $("<i>" + datestring + "</i>"));
if (!d) {
lastSender = null;
}
return datestring;
};
$scope.showdate = function(d) {
lastDate = $scope.showtime(d, "LL", lastDate);
};
$scope.showmessage = function(from, timestamp, message, nodes) {
var userid = $scope.$parent.$parent.id;
// Prepare message to display.
var s = [];
if (message) {
s.push(message);
$scope.$emit("incoming", message, from, userid);
}
var is_new_message = lastSender !== from;
var is_self = from === userid;
var extra_css = "";
var title = null;
var picture = null;
var showTitleAndPicture = function() {
if ($scope.isgroupchat) {
title = $("<strong>");
title.html(displayName(from, true));
extra_css += "with_name ";
var imgSrc = buddyImageSrc(from);
picture = $('<div class="buddyPicture"><i class="fa fa-user fa-3x"/><img/></div>');
if (imgSrc) {
picture.find("img").attr("src", imgSrc);
}
}
};
if (is_new_message) {
lastSender = from;
$scope.showdate(timestamp);
showTitleAndPicture()
}
var strMessage = s.join(" ");
if (!is_new_message) {
var element = this.append(strMessage, nodes);
if (element) {
return element;
}
showTitleAndPicture();
}
if (is_self) {
extra_css += "is_self";
} else {
extra_css += "is_remote";
}
if (timestamp) {
var ts = $('<div class="timestamp"/>');
ts.text(moment(timestamp).format("H:mm"));
if (nodes) {
nodes = nodes.add(ts);
} else {
nodes = ts;
}
}
return $scope.display(strMessage, nodes, extra_css, title, picture);
};
$scope.$on("seen", function() {
knowMessage.seen();
});
$scope.$on("p2p", function(event, state) {
//console.log("changed p2p state", state, p2p);
var msg;
if (state) {
msg = translation._("Peer to peer chat active.");
} else {
msg = translation._("Peer to peer chat is now off.");
}
$scope.display(null, $("<i class='p2p'><span class='icon-exchange'></span> " + msg + "</i>"));
});
$scope.$on("focus", function() {
$scope.focus();
});
$scope.$on("received", function(event, from, data) {
var userid = $scope.$parent.$parent.id;
var mid = data.Mid || null;
switch (data.Type) {
case "LeftOrJoined":
$scope.showtime(new Date());
if (data.LeftOrJoined === "left") {
$scope.display(null, $("<i>" + displayName(from) + translation._(" is now offline.") + "</i>"));
} else {
$scope.display(null, $("<i>" + displayName(from) + translation._(" is now online.") + "</i>"));
}
break;
case "Log":
$scope.showtime(new Date());
$scope.display(null, data.Log);
break;
default:
// Definitions.
var message = null;
var nodes = null;
var fromself = from === userid;
var noop = false;
var element = null;
var timestamp = data.Time;
if (!timestamp) {
timestamp = new Date();
}
// Process internal status messages.
if (data.Status) {
if (!mid && data.Status.Mid) {
mid = data.Status.Mid; // Inner Mid means internal chat status.
}
// Typing notification.
if (data.Status.Typing && !fromself) {
$window.clearTimeout(peerTypingExpired);
$scope.$emit("typing", {
who: "peer",
status: data.Status.Typing
});
if (data.Status.Typing === "stop") {
peerTypingExpired = $window.setTimeout(function() {
$scope.$emit("typing", {
who: "peer",
status: "no"
});
}, 20000);
}
}
// Mid updates.
if (mid && data.Status.State) {
knowMessage.update(mid, data.Status.State);
}
// Mid batch updates.
if (data.Status.SeenMids) {
_.each(data.Status.SeenMids, function(mid) {
knowMessage.update(mid, "received");
});
}
// File offers.
if (data.Status.FileInfo) {
var subscope = $scope.$new();
subscope.info = data.Status.FileInfo;
subscope.from = from;
fileInfo(subscope, function(clonedElement, scope) {
var text = fromself ? translation._("You share file:") : translation._("Incoming file:");
element = $scope.showmessage(from, timestamp, text, clonedElement);
});
noop = true;
}
// Ignore unknown status messages.
if (message === null && nodes === null) {
noop = true;
}
}
// Do nothing when where is nothing.
if (!data.Message && message === null && nodes === null) {
noop = true;
}
if (!noop) {
// Default handling is to use full message with security in place.
if (message === null && nodes === null && data.Message && typeof data.Message == "string") {
message = safeMessage(data.Message);
}
// Show the beast.
element = $scope.showmessage(from, timestamp, message, nodes);
}
if (element && mid && !$scope.isgroupchat) {
knowMessage.register(element, mid, fromself ? "sending" : "unread", !fromself);
}
break;
}
// Reset last sender to allow a new time stamp after a while.
$window.clearTimeout(senderExpired);
senderExpired = $window.setTimeout(function() {
lastSender = null;
}, 61000);
});
}];
});

39
static/js/controllers/controllers.js

@ -19,29 +19,28 @@ @@ -19,29 +19,28 @@
*
*/
define([
'underscore',
'underscore',
'controllers/mediastreamcontroller',
'controllers/statusmessagecontroller',
'controllers/chatroomcontroller',
'controllers/roomchangecontroller'
], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController) {
'controllers/mediastreamcontroller',
'controllers/statusmessagecontroller',
'controllers/chatroomcontroller',
'controllers/roomchangecontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController) {
var controllers = {
MediastreamController: MediastreamController,
StatusmessageController: StatusmessageController,
ChatroomController: ChatroomController,
RoomchangeController: RoomchangeController
};
var controllers = {
MediastreamController: MediastreamController,
StatusmessageController: StatusmessageController,
ChatroomController: ChatroomController,
RoomchangeController: RoomchangeController
};
var initialize = function (angModule) {
_.each(controllers, function(controller, name) {
angModule.controller(name, controller);
})
}
var initialize = function(angModule) {
_.each(controllers, function(controller, name) {
angModule.controller(name, controller);
})
}
return {
initialize: initialize
};
return {
initialize: initialize
};
});

1477
static/js/controllers/mediastreamcontroller.js

File diff suppressed because it is too large Load Diff

91
static/js/controllers/roomchangecontroller.js

@ -20,58 +20,59 @@ @@ -20,58 +20,59 @@
*/
define([], function() {
// RoomchangeController
return ["$scope", "$element", "$window", "$location", "mediaStream", "$http", "$timeout", function($scope, $element, $window, $location, mediaStream, $http, $timeout) {
// RoomchangeController
return ["$scope", "$element", "$window", "$location", "mediaStream", "$http", "$timeout", function($scope, $element, $window, $location, mediaStream, $http, $timeout) {
//console.log("Room change controller", $element, $scope.roomdata);
//console.log("Room change controller", $element, $scope.roomdata);
var url = mediaStream.url.api("rooms");
var url = mediaStream.url.api("rooms");
var ctrl = this;
ctrl.enabled = true;
var ctrl = this;
ctrl.enabled = true;
ctrl.getRoom = function(cb) {
$http({
method: "POST",
url: url,
data: $.param({
}),
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}).
success(function(data, status) {
cb(data);
}).
error(function() {
console.error("Failed to retrieve room link.");
cb({});
});
};
ctrl.getRoom = function(cb) {
$http({
method: "POST",
url: url,
data: $.param({}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).
success(function(data, status) {
cb(data);
}).
error(function() {
console.error("Failed to retrieve room link.");
cb({});
});
};
$scope.changeRoomToId = function(id) {
var roomid = $window.encodeURIComponent(id);
$location.path("/"+roomid);
return roomid;
};
$scope.changeRoomToId = function(id) {
var roomid = $window.encodeURIComponent(id);
$location.path("/" + roomid);
return roomid;
};
$scope.$on("$destroy", function() {
//console.log("Room change controller destroyed");
ctrl.enabled = false;
});
$scope.$on("$destroy", function() {
//console.log("Room change controller destroyed");
ctrl.enabled = false;
});
var roomDataLinkInput = $element.find(".roomdata-link-input");
if (roomDataLinkInput.length) {
$scope.roomdata = {};
$timeout(function() {
if (ctrl.enabled) {
ctrl.getRoom(function(roomdata) {
console.info("Retrieved room data", roomdata);
$scope.roomdata = roomdata;
roomdata.link = $scope.roomlink = mediaStream.url.room(roomdata.name);
});
}
}, 100);
}
var roomDataLinkInput = $element.find(".roomdata-link-input");
if (roomDataLinkInput.length) {
$scope.roomdata = {};
$timeout(function() {
if (ctrl.enabled) {
ctrl.getRoom(function(roomdata) {
console.info("Retrieved room data", roomdata);
$scope.roomdata = roomdata;
roomdata.link = $scope.roomlink = mediaStream.url.room(roomdata.name);
});
}
}, 100);
}
}];
}];
});

34
static/js/controllers/statusmessagecontroller.js

@ -20,25 +20,25 @@ @@ -20,25 +20,25 @@
*/
define([], function() {
// StatusmessageController
// StatusmessageController
return ["$scope", "mediaStream", function($scope, mediaStream) {
$scope.doHangup = function() {
mediaStream.webrtc.doHangup();
}
$scope.doAbort = function() {
mediaStream.webrtc.doHangup("abort", $scope.dialing);
}
$scope.doReconnect = function() {
mediaStream.connector.reconnect();
}
$scope.doAccept = function() {
mediaStream.webrtc.doAccept();
}
$scope.doReject = function() {
mediaStream.webrtc.doHangup('reject');
}
$scope.doHangup = function() {
mediaStream.webrtc.doHangup();
}
$scope.doAbort = function() {
mediaStream.webrtc.doHangup("abort", $scope.dialing);
}
$scope.doReconnect = function() {
mediaStream.connector.reconnect();
}
$scope.doAccept = function() {
mediaStream.webrtc.doAccept();
}
$scope.doReject = function() {
mediaStream.webrtc.doHangup('reject');
}
}];
}];
});

6
static/js/directives/audiolevel.js

@ -51,7 +51,7 @@ define(['jquery', 'underscore', 'rAF'], function($, _) { @@ -51,7 +51,7 @@ define(['jquery', 'underscore', 'rAF'], function($, _) {
if (webrtc.usermedia.audioLevel) {
width = Math.round(100 * webrtc.usermedia.audioLevel);
// Hide low volumes.
if (width<threshhold) {
if (width < threshhold) {
width = 0;
}
}
@ -71,7 +71,7 @@ define(['jquery', 'underscore', 'rAF'], function($, _) { @@ -71,7 +71,7 @@ define(['jquery', 'underscore', 'rAF'], function($, _) {
if (level < threshhold) {
level = 0;
} else {
level = level*activityMuliplier;
level = level * activityMuliplier;
}
this.audioActivityHistory.push(level);
if (this.audioActivityHistory.length > activityHistorySize) {
@ -110,7 +110,7 @@ define(['jquery', 'underscore', 'rAF'], function($, _) { @@ -110,7 +110,7 @@ define(['jquery', 'underscore', 'rAF'], function($, _) {
if (!talkingStatus[peercall.id]) {
send = true;
}
talkingStatusNew[peercall.id]=talking;
talkingStatusNew[peercall.id] = talking;
} else if (!talking && talkingStatus[peercall.id]) {
send = true;
}

593
static/js/directives/audiovideo.js

@ -20,300 +20,303 @@ @@ -20,300 +20,303 @@
*/
define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'injectCSS', 'webrtc.adapter', 'rAF'], function($, _, template, templatePeer, BigScreen) {
return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout) {
var requestAnimationFrame = $window.requestAnimationFrame;
var peerTemplate = $compile(templatePeer);
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
var peers = {};
var events = $({});
$scope.container = $element.get(0);
$scope.layoutparent = $element.parent();
$scope.remoteVideos = $element.find(".remoteVideos").get(0);
$scope.localVideo = $element.find(".localVideo").get(0);
$scope.miniVideo = $element.find(".miniVideo").get(0);
$scope.mini = $element.find(".miniContainer").get(0);
$scope.hasUsermedia = false;
$scope.isActive = false;
$scope.rendererName = $scope.defaultRendererName = "onepeople";
//console.log("audiovideo", localVideo, miniVideo);
$scope.addRemoteStream = function(stream, currentcall) {
//console.log("Add remote stream to scope", pc.id, stream);
var subscope = $scope.$new(true);
var peerid = subscope.peerid = currentcall.id;
buddyData.push(peerid);
subscope.withvideo = false;
subscope.onlyaudio = false;
subscope.talking = false;
subscope.applyTalking = function(talking) {
subscope.talking = !!talking;
safeApply(subscope);
};
subscope.$on("active", function() {
console.log("Stream scope is now active", peerid);
events.triggerHandler("active."+peerid, [subscope, currentcall, stream]);
});
console.log("Created stream scope", peerid);
peerTemplate(subscope, function(clonedElement, scope) {
$($scope.remoteVideos).append(clonedElement);
clonedElement.data("peerid", scope.peerid);
scope.element = clonedElement;
var video = clonedElement.find("video").get(0);
$window.attachMediaStream(video, stream);
// Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly.
videoWaiter.wait(video, stream, function(withvideo) {
peers[peerid] = scope;
if (withvideo) {
scope.$apply(function($scope) {
$scope.withvideo = true;
});
} else {
console.info("Incoming stream has no video tracks.");
scope.$apply(function($scope) {
$scope.onlyaudio = true;
});
}
scope.$emit("active", currentcall);
$scope.redraw();
}, function() {
peers[peerid] = scope;
console.warn("We did not receive video data for remote stream", currentcall, stream, video);
scope.$emit("active", currentcall);
$scope.redraw();
});
scope.doChat = function() {
$scope.$emit("startchat", currentcall.id, {autofocus: true, restore: true});
};
});
};
$scope.removeRemoteStream = function(stream, currentcall) {
var subscope = peers[currentcall.id];
if (subscope) {
buddyData.pop(currentcall.id);
delete peers[currentcall.id];
//console.log("remove scope", subscope);
if (subscope.element) {
subscope.element.remove();
}
subscope.$destroy();
$scope.redraw();
}
};
// Talking updates receiver.
mediaStream.api.e.on("received.talking", function(event, id, from, talking) {
var scope = peers[from];
//console.log("received.talking", talking, scope);
if (scope) {
scope.applyTalking(talking);
} else {
console.log("Received talking state without scope -> adding event.", from, talking);
events.one("active."+from, function(event, scope) {
console.log("Applying previously received talking state", from, talking);
scope.applyTalking(talking);
});
}
});
$scope.$on("active", function(currentcall) {
//console.log("active 2");
if (!$scope.isActive) {
$scope.isActive = true;
$scope.remoteVideos.style.opacity = 1;
$element.addClass("active");
//console.log("active 3");
_.delay(function() {
$scope.localVideo.style.opacity = 0;
$scope.localVideo.src = "";
}, 500);
_.delay(function() {
//console.log("active 4", $scope.mini);
$($scope.mini).addClass("visible");
}, 1000);
}
});
$scope.toggleFullscreen = function() {
//console.log("Toggle full screen", BigScreen.enabled, $scope.isActive, $scope.hasUsermedia);
if (BigScreen.enabled && ($scope.isActive || $scope.hasUsermedia)) {
$scope.layoutparent.toggleClass("fullscreen");
BigScreen.toggle($scope.layoutparent.get(0));
}
};
mediaStream.webrtc.e.on("usermedia", function(event, usermedia) {
//console.log("XXXXXXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia);
$scope.hasUsermedia = true;
usermedia.attachMediaStream($scope.localVideo);
var count = 0;
var waitForLocalVideo = function() {
if (!$scope.hasUsermedia) {
return;
}
if ($scope.localVideo.videoWidth > 0) {
$scope.localVideo.style.opacity = 1;
$scope.redraw();
} else {
count++;
if (count < 100) {
setTimeout(waitForLocalVideo, 100);
} else {
console.warn("Timeout while waiting for local video.")
}
}
};
waitForLocalVideo();
});
mediaStream.webrtc.e.on("done", function() {
$scope.hasUsermedia = false;
$scope.isActive = false;
if (BigScreen.enabled) {
BigScreen.exit();
}
_.delay(function() {
if ($scope.isActive) {
return;
}
$scope.localVideo.src = '';
$scope.miniVideo.src = '';
$($scope.remoteVideos).empty();
}, 1500);
$($scope.mini).removeClass("visible");
$scope.localVideo.style.opacity = 0;
$scope.remoteVideos.style.opacity = 0;
$element.removeClass('active');
_.each(peers, function(scope, k) {
scope.$destroy();
delete peers[k];
});
$scope.rendererName = $scope.defaultRendererName;
});
mediaStream.webrtc.e.on("streamadded", function(event, stream, currentcall) {
console.log("Remote stream added.", stream, currentcall);
if (_.isEmpty(peers)) {
//console.log("First stream");
$window.reattachMediaStream($scope.miniVideo, $scope.localVideo);
}
$scope.addRemoteStream(stream, currentcall);
});
mediaStream.webrtc.e.on("streamremoved", function(event, stream, currentcall) {
console.log("Remote stream removed.", stream, currentcall);
$scope.removeRemoteStream(stream, currentcall);
});
return {
peers: peers
};
}];
var compile = function(tElement, tAttr) {
return function(scope, iElement, iAttrs, controller) {
//console.log("compile", arguments)
iElement.on("doubletap dblclick", _.debounce(scope.toggleFullscreen, 100, true));
var rendererName = null;
var getRendererName = function() {
// Return name of current renderer.
if (rendererName !== null) {
return rendererName;
} else {
return scope.rendererName;
}
};
scope.setRenderer = function(name) {
scope.rendererName = name;
};
var needsRedraw = false;
scope.redraw = function() {
needsRedraw = true;
};
var redraw = function() {
var size = {
width: scope.layoutparent.width(),
height: scope.layoutparent.height()
}
var again = videoLayout.update(getRendererName(), size, scope, controller);
if (again) {
// Layout needs a redraw.
needsRedraw = true;
}
};
// Make sure we draw on resize.
$($window).on("resize", scope.redraw);
scope.$on("mainresize", function(event, main) {
if (main) {
// Force smally renderer when we have a main view.
rendererName = "smally"
} else if (rendererName) {
rendererName = null;
}
_.defer(scope.redraw);
});
scope.redraw();
// Make sure we draw when the renderer was changed.
scope.$watch("rendererName", function() {
_.defer(scope.redraw);
});
// Update function run in rendering thread.
var update = function() {
if (needsRedraw) {
needsRedraw =false;
redraw();
}
requestAnimationFrame(update);
}
_.defer(update);
}
};
return {
restrict: 'E',
replace: true,
scope: true,
template: template,
controller: controller,
compile: compile
}
}];
return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout) {
var requestAnimationFrame = $window.requestAnimationFrame;
var peerTemplate = $compile(templatePeer);
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
var peers = {};
var events = $({});
$scope.container = $element.get(0);
$scope.layoutparent = $element.parent();
$scope.remoteVideos = $element.find(".remoteVideos").get(0);
$scope.localVideo = $element.find(".localVideo").get(0);
$scope.miniVideo = $element.find(".miniVideo").get(0);
$scope.mini = $element.find(".miniContainer").get(0);
$scope.hasUsermedia = false;
$scope.isActive = false;
$scope.rendererName = $scope.defaultRendererName = "onepeople";
//console.log("audiovideo", localVideo, miniVideo);
$scope.addRemoteStream = function(stream, currentcall) {
//console.log("Add remote stream to scope", pc.id, stream);
var subscope = $scope.$new(true);
var peerid = subscope.peerid = currentcall.id;
buddyData.push(peerid);
subscope.withvideo = false;
subscope.onlyaudio = false;
subscope.talking = false;
subscope.applyTalking = function(talking) {
subscope.talking = !! talking;
safeApply(subscope);
};
subscope.$on("active", function() {
console.log("Stream scope is now active", peerid);
events.triggerHandler("active." + peerid, [subscope, currentcall, stream]);
});
console.log("Created stream scope", peerid);
peerTemplate(subscope, function(clonedElement, scope) {
$($scope.remoteVideos).append(clonedElement);
clonedElement.data("peerid", scope.peerid);
scope.element = clonedElement;
var video = clonedElement.find("video").get(0);
$window.attachMediaStream(video, stream);
// Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly.
videoWaiter.wait(video, stream, function(withvideo) {
peers[peerid] = scope;
if (withvideo) {
scope.$apply(function($scope) {
$scope.withvideo = true;
});
} else {
console.info("Incoming stream has no video tracks.");
scope.$apply(function($scope) {
$scope.onlyaudio = true;
});
}
scope.$emit("active", currentcall);
$scope.redraw();
}, function() {
peers[peerid] = scope;
console.warn("We did not receive video data for remote stream", currentcall, stream, video);
scope.$emit("active", currentcall);
$scope.redraw();
});
scope.doChat = function() {
$scope.$emit("startchat", currentcall.id, {
autofocus: true,
restore: true
});
};
});
};
$scope.removeRemoteStream = function(stream, currentcall) {
var subscope = peers[currentcall.id];
if (subscope) {
buddyData.pop(currentcall.id);
delete peers[currentcall.id];
//console.log("remove scope", subscope);
if (subscope.element) {
subscope.element.remove();
}
subscope.$destroy();
$scope.redraw();
}
};
// Talking updates receiver.
mediaStream.api.e.on("received.talking", function(event, id, from, talking) {
var scope = peers[from];
//console.log("received.talking", talking, scope);
if (scope) {
scope.applyTalking(talking);
} else {
console.log("Received talking state without scope -> adding event.", from, talking);
events.one("active." + from, function(event, scope) {
console.log("Applying previously received talking state", from, talking);
scope.applyTalking(talking);
});
}
});
$scope.$on("active", function(currentcall) {
//console.log("active 2");
if (!$scope.isActive) {
$scope.isActive = true;
$scope.remoteVideos.style.opacity = 1;
$element.addClass("active");
//console.log("active 3");
_.delay(function() {
$scope.localVideo.style.opacity = 0;
$scope.localVideo.src = "";
}, 500);
_.delay(function() {
//console.log("active 4", $scope.mini);
$($scope.mini).addClass("visible");
}, 1000);
}
});
$scope.toggleFullscreen = function() {
//console.log("Toggle full screen", BigScreen.enabled, $scope.isActive, $scope.hasUsermedia);
if (BigScreen.enabled && ($scope.isActive || $scope.hasUsermedia)) {
$scope.layoutparent.toggleClass("fullscreen");
BigScreen.toggle($scope.layoutparent.get(0));
}
};
mediaStream.webrtc.e.on("usermedia", function(event, usermedia) {
//console.log("XXXXXXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia);
$scope.hasUsermedia = true;
usermedia.attachMediaStream($scope.localVideo);
var count = 0;
var waitForLocalVideo = function() {
if (!$scope.hasUsermedia) {
return;
}
if ($scope.localVideo.videoWidth > 0) {
$scope.localVideo.style.opacity = 1;
$scope.redraw();
} else {
count++;
if (count < 100) {
setTimeout(waitForLocalVideo, 100);
} else {
console.warn("Timeout while waiting for local video.")
}
}
};
waitForLocalVideo();
});
mediaStream.webrtc.e.on("done", function() {
$scope.hasUsermedia = false;
$scope.isActive = false;
if (BigScreen.enabled) {
BigScreen.exit();
}
_.delay(function() {
if ($scope.isActive) {
return;
}
$scope.localVideo.src = '';
$scope.miniVideo.src = '';
$($scope.remoteVideos).empty();
}, 1500);
$($scope.mini).removeClass("visible");
$scope.localVideo.style.opacity = 0;
$scope.remoteVideos.style.opacity = 0;
$element.removeClass('active');
_.each(peers, function(scope, k) {
scope.$destroy();
delete peers[k];
});
$scope.rendererName = $scope.defaultRendererName;
});
mediaStream.webrtc.e.on("streamadded", function(event, stream, currentcall) {
console.log("Remote stream added.", stream, currentcall);
if (_.isEmpty(peers)) {
//console.log("First stream");
$window.reattachMediaStream($scope.miniVideo, $scope.localVideo);
}
$scope.addRemoteStream(stream, currentcall);
});
mediaStream.webrtc.e.on("streamremoved", function(event, stream, currentcall) {
console.log("Remote stream removed.", stream, currentcall);
$scope.removeRemoteStream(stream, currentcall);
});
return {
peers: peers
};
}];
var compile = function(tElement, tAttr) {
return function(scope, iElement, iAttrs, controller) {
//console.log("compile", arguments)
iElement.on("doubletap dblclick", _.debounce(scope.toggleFullscreen, 100, true));
var rendererName = null;
var getRendererName = function() {
// Return name of current renderer.
if (rendererName !== null) {
return rendererName;
} else {
return scope.rendererName;
}
};
scope.setRenderer = function(name) {
scope.rendererName = name;
};
var needsRedraw = false;
scope.redraw = function() {
needsRedraw = true;
};
var redraw = function() {
var size = {
width: scope.layoutparent.width(),
height: scope.layoutparent.height()
}
var again = videoLayout.update(getRendererName(), size, scope, controller);
if (again) {
// Layout needs a redraw.
needsRedraw = true;
}
};
// Make sure we draw on resize.
$($window).on("resize", scope.redraw);
scope.$on("mainresize", function(event, main) {
if (main) {
// Force smally renderer when we have a main view.
rendererName = "smally"
} else if (rendererName) {
rendererName = null;
}
_.defer(scope.redraw);
});
scope.redraw();
// Make sure we draw when the renderer was changed.
scope.$watch("rendererName", function() {
_.defer(scope.redraw);
});
// Update function run in rendering thread.
var update = function() {
if (needsRedraw) {
needsRedraw = false;
redraw();
}
requestAnimationFrame(update);
}
_.defer(update);
}
};
return {
restrict: 'E',
replace: true,
scope: true,
template: template,
controller: controller,
compile: compile
}
}];
});

218
static/js/directives/buddylist.js

@ -20,111 +20,117 @@ @@ -20,111 +20,117 @@
*/
define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
// buddyList
return ["$compile", "buddyList", "mediaStream", function($compile, buddyList, mediaStream) {
//console.log("buddyList directive");
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.layout.buddylist = false;
$scope.enabled = false;
$scope.doCall = function(id) {
mediaStream.webrtc.doCall(id);
};
$scope.doChat = function(id) {
//console.log("doChat", id);
$scope.$emit("startchat", id, {autofocus: true, restore: true});
};
$scope.doAudioConference = function(id) {
$scope.updateAutoAccept(id);
mediaStream.api.sendChat(id, null, {type: "conference", id: mediaStream.connector.roomid})
};
$scope.setRoomStatus = function(status) {
if (status !== $scope.enabled) {
$scope.enabled = status;
$scope.$emit("roomStatus", status);
}
if (status && !$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = true
}
};
//XXX(longsleep): Debug leftover ?? Remove this.
window.doAudioConference = $scope.doAudioConference;
var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {});
var onJoined = _.bind(buddylist.onJoined, buddylist);
var onLeft = _.bind(buddylist.onLeft, buddylist);
var onStatus = _.bind(buddylist.onStatus, buddylist);
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
if (dataType === "Left") {
onLeft(data);
} else {
onJoined(data);
}
});
mediaStream.api.e.on("received.users", function(event, data) {
$scope.setRoomStatus(true);
var selfId = $scope.id;
_.each(data, function(p) {
if (p.Id !== selfId) {
onJoined(p);
}
});
$scope.$apply();
});
mediaStream.api.e.on("received.status", function(event, data) {
onStatus(data);
});
mediaStream.connector.e.on("closed error", function() {
$scope.setRoomStatus(false);
buddylist.onClosed();
});
// Request user list whenever the connection comes ready.
mediaStream.connector.ready(function() {
mediaStream.api.requestUsers();
});
}];
var link = function(scope, iElement, iAttrs, controller) {
// Add events to buddy list parent container to show/hide.
var parent = iElement.parent();
parent.on("mouseenter mouseleave", function(event) {
if (event.type === "mouseenter") {
scope.layout.buddylist = true;
} else {
if (scope.layout.buddylistAutoHide) {
scope.layout.buddylist = false;
}
}
scope.$apply();
});
};
return {
restrict: 'E',
replace: true,
scope: true,
template: template,
controller: controller,
link: link
}
}];
// buddyList
return ["$compile", "buddyList", "mediaStream", function($compile, buddyList, mediaStream) {
//console.log("buddyList directive");
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.layout.buddylist = false;
$scope.enabled = false;
$scope.doCall = function(id) {
mediaStream.webrtc.doCall(id);
};
$scope.doChat = function(id) {
//console.log("doChat", id);
$scope.$emit("startchat", id, {
autofocus: true,
restore: true
});
};
$scope.doAudioConference = function(id) {
$scope.updateAutoAccept(id);
mediaStream.api.sendChat(id, null, {
type: "conference",
id: mediaStream.connector.roomid
})
};
$scope.setRoomStatus = function(status) {
if (status !== $scope.enabled) {
$scope.enabled = status;
$scope.$emit("roomStatus", status);
}
if (status && !$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = true
}
};
//XXX(longsleep): Debug leftover ?? Remove this.
window.doAudioConference = $scope.doAudioConference;
var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {});
var onJoined = _.bind(buddylist.onJoined, buddylist);
var onLeft = _.bind(buddylist.onLeft, buddylist);
var onStatus = _.bind(buddylist.onStatus, buddylist);
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
if (dataType === "Left") {
onLeft(data);
} else {
onJoined(data);
}
});
mediaStream.api.e.on("received.users", function(event, data) {
$scope.setRoomStatus(true);
var selfId = $scope.id;
_.each(data, function(p) {
if (p.Id !== selfId) {
onJoined(p);
}
});
$scope.$apply();
});
mediaStream.api.e.on("received.status", function(event, data) {
onStatus(data);
});
mediaStream.connector.e.on("closed error", function() {
$scope.setRoomStatus(false);
buddylist.onClosed();
});
// Request user list whenever the connection comes ready.
mediaStream.connector.ready(function() {
mediaStream.api.requestUsers();
});
}];
var link = function(scope, iElement, iAttrs, controller) {
// Add events to buddy list parent container to show/hide.
var parent = iElement.parent();
parent.on("mouseenter mouseleave", function(event) {
if (event.type === "mouseenter") {
scope.layout.buddylist = true;
} else {
if (scope.layout.buddylistAutoHide) {
scope.layout.buddylist = false;
}
}
scope.$apply();
});
};
return {
restrict: 'E',
replace: true,
scope: true,
template: template,
controller: controller,
link: link
}
}];
});

889
static/js/directives/chat.js

@ -20,425 +20,474 @@ @@ -20,425 +20,474 @@
*/
define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function(_, templateChat, templateChatroom) {
return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "$timeout", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, $timeout) {
var displayName = safeDisplayName;
var group_chat_id = "";
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.layout.chat = false;
$scope.layout.chatMaximized = false;
var ctrl = this;
var rooms = ctrl.rooms = {};
ctrl.visibleRooms = [];
ctrl.group = group_chat_id;
ctrl.get = function(id) {
return ctrl.rooms[id];
}
$scope.currentRoom = null;
$scope.currentRoomActive = false;
$scope.getVisibleRooms = function() {
var res = [];
for (var i=0; i<ctrl.visibleRooms.length; i++) {
var r = rooms[ctrl.visibleRooms[i]];
if (!r || r.id === ctrl.group) {
continue;
}
res.push(r);
}
return res;
};
$scope.getGroupRoom = function() {
return rooms[ctrl.group];
};
mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p) {
//console.log("received", data, id, from);
var roomid = id;
if (roomid === mediaStream.api.id) {
roomid = from;
} else {
if (roomid !== ctrl.group && from !== mediaStream.api.id) {
console.log("Received chat message for invalid room", roomid, id, from);
return;
}
}
var with_message = !!data.Message;
var room = rooms[roomid];
if (!room) {
if (!with_message) {
return;
}
// No room with this id, get one with the from id
$scope.$emit("startchat", from, {restore: with_message});
room = rooms[from];
}
if (with_message && from !== mediaStream.api.id) {
room.newmessage = true;
room.peerIsTyping = "no";
room.p2p(!!p2p);
}
room.$broadcast("received", from, data);
safeApply(room);
});
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
var room = rooms[data.Id];
if (room) {
switch (dataType) {
case "Left":
if (data.Status !== "soft") {
room.enabled = false;
room.$broadcast("received", data.Id, {Type: "LeftOrJoined", "LeftOrJoined": "left"});
safeApply(room);
}
break;
case "Joined":
if (!room.enabled) {
room.enabled = true;
_.delay(function() {
room.$broadcast("received", data.Id, {Type: "LeftOrJoined", "LeftOrJoined": "joined"});
safeApply(room);
}, 1000);
}
break;
default:
break;
}
}
});
$scope.$parent.$on("startchat", function(event, id, options) {
//console.log("startchat requested", event, id);
if (id === group_chat_id) {
$scope.showGroupRoom(null, options);
} else {
$scope.showRoom(id, {title: translation._("Chat with")}, options);
}
});
}];
var compile = function(tElement, tAttrs) {
var chat = $compile(templateChatroom);
return function(scope, iElement, iAttrs, controller) {
var pane = iElement.find(".chatpane");
scope.showGroupRoom = function(settings, options) {
var stngs = $.extend({title: translation._("Room chat")}, settings);
return scope.showRoom(controller.group, stngs, options);
};
scope.showRoom = function(id, settings, opts) {
var options = $.extend({}, opts);
var subscope = controller.rooms[id];
var index = controller.visibleRooms.length;
if (!subscope) {
console.log("Create new chatroom", [id]);
controller.visibleRooms.push(id);
subscope = controller.rooms[id] = scope.$new();
translation.inject(subscope);
subscope.id = id;
subscope.isgroupchat = id === controller.group ? true : false;
subscope.index = index;
subscope.settings = settings;
subscope.visible = false;
subscope.newmessage = false;
subscope.enabled = true;
subscope.peerIsTyping = "no";
subscope.firstmessage = true;
subscope.p2pstate = false;
subscope.active = false;
subscope.pending = 0;
if (!subscope.isgroupchat) {
buddyData.push(id);
}
subscope.hide = function() {
scope.hideRoom(id);
};
//TODO(longsleep): This is currently never called. Find a suitable way to clean up old chats.
subscope.kill = function() {
if (!subscope.isgroupchat) {
buddyData.pop(id);
}
scope.killRoom(id);
};
subscope.seen = function() {
subscope.pending = 0;
scope.$emit("chatseen", subscope.id);
if (subscope.newmessage) {
subscope.newmessage = false;
subscope.$broadcast("seen");
}
};
subscope.deactivate =function() {
scope.deactivateRoom();
};
subscope.toggleMax = function() {
scope.toggleMax();
};
subscope.sendChat = function(to, message, status, mid, noloop) {
//console.log("send chat", to, scope.peer);
var peercall = mediaStream.webrtc.findTargetCall(to);
if (message && !mid) {
mid = randomGen.random({hex: true});
};
if (peercall && peercall.peerconnection.datachannelReady) {
subscope.p2p(true);
// Send out stuff through data channel.
_.delay(function() {
mediaStream.api.apply("sendChat", {
send: function(type, data) {
// We also send to self, to display our own stuff.
if (!noloop) {
mediaStream.api.received({Type: data.Type, Data: data, From: mediaStream.api.id, To: peercall.id});
}
return peercall.peerconnection.send(data);
}
})(to, message, status, mid);
}, 100);
} else {
subscope.p2p(false);
_.delay(function() {
mediaStream.api.send2("sendChat", function(type, data) {
if (!noloop) {
//console.log("looped to self", type, data);
mediaStream.api.received({Type: data.Type, Data: data, From: mediaStream.api.id, To: to});
}
})(to, message, status, mid);
}, 100);
}
return mid;
};
subscope.p2p = function(state) {
if (state !== subscope.p2pstate) {
subscope.p2pstate = state;
subscope.$broadcast("p2p", state);
}
};
//console.log("Creating new chat room", controller, subscope, index);
subscope.$on("submit", function(event, input) {
subscope.seen();
var mid = subscope.sendChat(event.targetScope.id, input);
event.targetScope.$broadcast("received", null, {Type: "Message", Status: {State: "sent", "Mid": mid}});
});
subscope.$on("submitseen", function(event, pending) {
//console.log("submitseen", pending);
subscope.sendChat(event.targetScope.id, null, {SeenMids: pending}, null, true);
});
subscope.$on("submitreceived", function(event, mid) {
subscope.sendChat(event.targetScope.id, null, {State: "delivered", Mid: mid}, null, true);
});
subscope.$on("typing", function(event, params) {
if (params.who === "local") {
var room = event.targetScope.id;
subscope.seen();
//console.log("typing event", params.status);
if (!subscope.isgroupchat) {
// Transmit typing events to private chats.
subscope.sendChat(event.targetScope.id, null, {Typing: params.status});
}
} else {
subscope.peerIsTyping = params.status;
//console.log("peer typing event", params.status, subscope.peerIsTyping);
}
safeApply(subscope);
});
subscope.$on("incoming", function(event, message, from, userid) {
if (from !== userid) {
subscope.pending++;
scope.$emit("chatincoming", subscope.id);
}
if (subscope.firstmessage || !desktopNotify.windowHasFocus) {
var room = event.targetScope.id;
// Make sure we are not in group chat or the message is from ourselves
// before we beep and shout.
if (!subscope.isgroupchat && from !== userid) {
playSound.play("message1");
desktopNotify.notify(translation._("Message from ")+displayName(from), message);
}
subscope.firstmessage = false;
}
});
chat(subscope, function(clonedElement, $scope) {
pane.append(clonedElement);
$scope.element=clonedElement;
$scope.visible = true;
if (options.autofocus) {
_.defer(function() {
$scope.$broadcast("focus");
});
}
// Support drag and drop file uploads in Chat.
var namespace = "file_"+scope.id;
var binder = fileUpload.bindDrop(namespace, clonedElement, _.bind(function(files) {
console.log("File dragged", files);
_.each(files, _.bind(function(f) {
var info = $.extend({id: f.id}, f.info);
console.log("Advertising file", f, info);
$scope.sendChat(subscope.id, "File", {FileInfo: info});
}, this));
}, this));
binder.namespace = function() {
// Inject own id into namespace.
return namespace+"_"+scope.myid;
};
});
} else {
if (options.restore) {
if (!subscope.visible) {
controller.visibleRooms.push(id);
subscope.index = index;
subscope.visible = true;
}
}
if (options.autofocus && subscope.visible) {
subscope.$broadcast("focus");
}
}
if (!options.noactivate) {
scope.activateRoom(subscope.id, true);
}
if (options.restore && !options.noenable) {
if (!scope.layout.chat) {
scope.layout.chat = true;
}
}
safeApply(subscope);
return subscope;
};
scope.hideRoom = function(id) {
var subscope = controller.rooms[id];
if (!subscope) {
console.log("hideRoom called for unknown room", id);
return;
}
var element = subscope.element;
var index = subscope.index;
controller.visibleRooms.splice(index, 1);
subscope.visible=false;
subscope.firstmessage=true;
// Refresh index of the rest of the rooms.
_.each(controller.visibleRooms, function(id, idx) {
var s = controller.rooms[id];
//console.log("updated idx", idx, s.index);
s.index = idx;
});
if (scope.currentRoom === subscope) {
scope.currentRoom = null;
scope.currentRoomActive = false;
}
if (!controller.visibleRooms.length) {
scope.showGroupRoom(null, {restore: true, noenable: true, noactivate: true});
// If last visible room was removed, hide chat.
scope.layout.chat = false;
}
};
scope.killRoom = function(id) {
scope.hideRoom(id);
var subscope = controller.rooms[id];
if (!subscope) {
return;
}
delete controller.rooms[id];
$timeout(function() {
subscope.$destroy();
}, 0);
};
scope.toggleMax = function() {
scope.layout.chatMaximized = !scope.layout.chatMaximized;
};
scope.activateRoom = function(id, active) {
var subscope = controller.rooms[id];
if (!subscope) {
return;
}
var visible = !!scope.layout.chat;
var flip = false;
//console.log("toggleActive", active, id, scope.currentRoom, scope.currentRoom == subscope, subscope.active);
if (scope.currentRoom == subscope) {
subscope.active = active;
scope.currentRoomActive = true;
if (visible) {
flip = true;
}
} else {
if (scope.currentRoom) {
scope.currentRoom.active = false;
//scope.currentRoom.hide();
if (visible) {
flip = true;
}
}
if (active) {
scope.currentRoom = subscope;
scope.currentRoomActive = true;
}
subscope.active = active;
}
if (flip) {
pane.toggleClass("flip");
}
};
scope.deactivateRoom = function() {
scope.currentRoomActive = false;
};
scope.$watch("layout.chat", function(chat) {
if (!chat) {
pane.removeClass("flip");
}
scope.layout.chatMaximized = false;
});
scope.$on("room", function(event, room) {
var subscope = scope.showGroupRoom(null, {restore: true, noenable: true, noactivate: true});
if (room) {
var msg = $("<span>").text(translation._("You are now in room %s ...", room));
subscope.$broadcast("display", null, $("<i>").append(msg));
}
});
};
};
return {
restrict: 'E',
replace: true,
scope: true,
template: templateChat,
controller: controller,
compile: compile
}
}];
return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "$timeout", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, $timeout) {
var displayName = safeDisplayName;
var group_chat_id = "";
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.layout.chat = false;
$scope.layout.chatMaximized = false;
var ctrl = this;
var rooms = ctrl.rooms = {};
ctrl.visibleRooms = [];
ctrl.group = group_chat_id;
ctrl.get = function(id) {
return ctrl.rooms[id];
}
$scope.currentRoom = null;
$scope.currentRoomActive = false;
$scope.getVisibleRooms = function() {
var res = [];
for (var i = 0; i < ctrl.visibleRooms.length; i++) {
var r = rooms[ctrl.visibleRooms[i]];
if (!r || r.id === ctrl.group) {
continue;
}
res.push(r);
}
return res;
};
$scope.getGroupRoom = function() {
return rooms[ctrl.group];
};
mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p) {
//console.log("received", data, id, from);
var roomid = id;
if (roomid === mediaStream.api.id) {
roomid = from;
} else {
if (roomid !== ctrl.group && from !== mediaStream.api.id) {
console.log("Received chat message for invalid room", roomid, id, from);
return;
}
}
var with_message = !! data.Message;
var room = rooms[roomid];
if (!room) {
if (!with_message) {
return;
}
// No room with this id, get one with the from id
$scope.$emit("startchat", from, {
restore: with_message
});
room = rooms[from];
}
if (with_message && from !== mediaStream.api.id) {
room.newmessage = true;
room.peerIsTyping = "no";
room.p2p( !! p2p);
}
room.$broadcast("received", from, data);
safeApply(room);
});
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
var room = rooms[data.Id];
if (room) {
switch (dataType) {
case "Left":
if (data.Status !== "soft") {
room.enabled = false;
room.$broadcast("received", data.Id, {
Type: "LeftOrJoined",
"LeftOrJoined": "left"
});
safeApply(room);
}
break;
case "Joined":
if (!room.enabled) {
room.enabled = true;
_.delay(function() {
room.$broadcast("received", data.Id, {
Type: "LeftOrJoined",
"LeftOrJoined": "joined"
});
safeApply(room);
}, 1000);
}
break;
default:
break;
}
}
});
$scope.$parent.$on("startchat", function(event, id, options) {
//console.log("startchat requested", event, id);
if (id === group_chat_id) {
$scope.showGroupRoom(null, options);
} else {
$scope.showRoom(id, {
title: translation._("Chat with")
}, options);
}
});
}];
var compile = function(tElement, tAttrs) {
var chat = $compile(templateChatroom);
return function(scope, iElement, iAttrs, controller) {
var pane = iElement.find(".chatpane");
scope.showGroupRoom = function(settings, options) {
var stngs = $.extend({
title: translation._("Room chat")
}, settings);
return scope.showRoom(controller.group, stngs, options);
};
scope.showRoom = function(id, settings, opts) {
var options = $.extend({}, opts);
var subscope = controller.rooms[id];
var index = controller.visibleRooms.length;
if (!subscope) {
console.log("Create new chatroom", [id]);
controller.visibleRooms.push(id);
subscope = controller.rooms[id] = scope.$new();
translation.inject(subscope);
subscope.id = id;
subscope.isgroupchat = id === controller.group ? true : false;
subscope.index = index;
subscope.settings = settings;
subscope.visible = false;
subscope.newmessage = false;
subscope.enabled = true;
subscope.peerIsTyping = "no";
subscope.firstmessage = true;
subscope.p2pstate = false;
subscope.active = false;
subscope.pending = 0;
if (!subscope.isgroupchat) {
buddyData.push(id);
}
subscope.hide = function() {
scope.hideRoom(id);
};
//TODO(longsleep): This is currently never called. Find a suitable way to clean up old chats.
subscope.kill = function() {
if (!subscope.isgroupchat) {
buddyData.pop(id);
}
scope.killRoom(id);
};
subscope.seen = function() {
subscope.pending = 0;
scope.$emit("chatseen", subscope.id);
if (subscope.newmessage) {
subscope.newmessage = false;
subscope.$broadcast("seen");
}
};
subscope.deactivate = function() {
scope.deactivateRoom();
};
subscope.toggleMax = function() {
scope.toggleMax();
};
subscope.sendChat = function(to, message, status, mid, noloop) {
//console.log("send chat", to, scope.peer);
var peercall = mediaStream.webrtc.findTargetCall(to);
if (message && !mid) {
mid = randomGen.random({
hex: true
});
};
if (peercall && peercall.peerconnection.datachannelReady) {
subscope.p2p(true);
// Send out stuff through data channel.
_.delay(function() {
mediaStream.api.apply("sendChat", {
send: function(type, data) {
// We also send to self, to display our own stuff.
if (!noloop) {
mediaStream.api.received({
Type: data.Type,
Data: data,
From: mediaStream.api.id,
To: peercall.id
});
}
return peercall.peerconnection.send(data);
}
})(to, message, status, mid);
}, 100);
} else {
subscope.p2p(false);
_.delay(function() {
mediaStream.api.send2("sendChat", function(type, data) {
if (!noloop) {
//console.log("looped to self", type, data);
mediaStream.api.received({
Type: data.Type,
Data: data,
From: mediaStream.api.id,
To: to
});
}
})(to, message, status, mid);
}, 100);
}
return mid;
};
subscope.p2p = function(state) {
if (state !== subscope.p2pstate) {
subscope.p2pstate = state;
subscope.$broadcast("p2p", state);
}
};
//console.log("Creating new chat room", controller, subscope, index);
subscope.$on("submit", function(event, input) {
subscope.seen();
var mid = subscope.sendChat(event.targetScope.id, input);
event.targetScope.$broadcast("received", null, {
Type: "Message",
Status: {
State: "sent",
"Mid": mid
}
});
});
subscope.$on("submitseen", function(event, pending) {
//console.log("submitseen", pending);
subscope.sendChat(event.targetScope.id, null, {
SeenMids: pending
}, null, true);
});
subscope.$on("submitreceived", function(event, mid) {
subscope.sendChat(event.targetScope.id, null, {
State: "delivered",
Mid: mid
}, null, true);
});
subscope.$on("typing", function(event, params) {
if (params.who === "local") {
var room = event.targetScope.id;
subscope.seen();
//console.log("typing event", params.status);
if (!subscope.isgroupchat) {
// Transmit typing events to private chats.
subscope.sendChat(event.targetScope.id, null, {
Typing: params.status
});
}
} else {
subscope.peerIsTyping = params.status;
//console.log("peer typing event", params.status, subscope.peerIsTyping);
}
safeApply(subscope);
});
subscope.$on("incoming", function(event, message, from, userid) {
if (from !== userid) {
subscope.pending++;
scope.$emit("chatincoming", subscope.id);
}
if (subscope.firstmessage || !desktopNotify.windowHasFocus) {
var room = event.targetScope.id;
// Make sure we are not in group chat or the message is from ourselves
// before we beep and shout.
if (!subscope.isgroupchat && from !== userid) {
playSound.play("message1");
desktopNotify.notify(translation._("Message from ") + displayName(from), message);
}
subscope.firstmessage = false;
}
});
chat(subscope, function(clonedElement, $scope) {
pane.append(clonedElement);
$scope.element = clonedElement;
$scope.visible = true;
if (options.autofocus) {
_.defer(function() {
$scope.$broadcast("focus");
});
}
// Support drag and drop file uploads in Chat.
var namespace = "file_" + scope.id;
var binder = fileUpload.bindDrop(namespace, clonedElement, _.bind(function(files) {
console.log("File dragged", files);
_.each(files, _.bind(function(f) {
var info = $.extend({
id: f.id
}, f.info);
console.log("Advertising file", f, info);
$scope.sendChat(subscope.id, "File", {
FileInfo: info
});
}, this));
}, this));
binder.namespace = function() {
// Inject own id into namespace.
return namespace + "_" + scope.myid;
};
});
} else {
if (options.restore) {
if (!subscope.visible) {
controller.visibleRooms.push(id);
subscope.index = index;
subscope.visible = true;
}
}
if (options.autofocus && subscope.visible) {
subscope.$broadcast("focus");
}
}
if (!options.noactivate) {
scope.activateRoom(subscope.id, true);
}
if (options.restore && !options.noenable) {
if (!scope.layout.chat) {
scope.layout.chat = true;
}
}
safeApply(subscope);
return subscope;
};
scope.hideRoom = function(id) {
var subscope = controller.rooms[id];
if (!subscope) {
console.log("hideRoom called for unknown room", id);
return;
}
var element = subscope.element;
var index = subscope.index;
controller.visibleRooms.splice(index, 1);
subscope.visible = false;
subscope.firstmessage = true;
// Refresh index of the rest of the rooms.
_.each(controller.visibleRooms, function(id, idx) {
var s = controller.rooms[id];
//console.log("updated idx", idx, s.index);
s.index = idx;
});
if (scope.currentRoom === subscope) {
scope.currentRoom = null;
scope.currentRoomActive = false;
}
if (!controller.visibleRooms.length) {
scope.showGroupRoom(null, {
restore: true,
noenable: true,
noactivate: true
});
// If last visible room was removed, hide chat.
scope.layout.chat = false;
}
};
scope.killRoom = function(id) {
scope.hideRoom(id);
var subscope = controller.rooms[id];
if (!subscope) {
return;
}
delete controller.rooms[id];
$timeout(function() {
subscope.$destroy();
}, 0);
};
scope.toggleMax = function() {
scope.layout.chatMaximized = !scope.layout.chatMaximized;
};
scope.activateRoom = function(id, active) {
var subscope = controller.rooms[id];
if (!subscope) {
return;
}
var visible = !! scope.layout.chat;
var flip = false;
//console.log("toggleActive", active, id, scope.currentRoom, scope.currentRoom == subscope, subscope.active);
if (scope.currentRoom == subscope) {
subscope.active = active;
scope.currentRoomActive = true;
if (visible) {
flip = true;
}
} else {
if (scope.currentRoom) {
scope.currentRoom.active = false;
//scope.currentRoom.hide();
if (visible) {
flip = true;
}
}
if (active) {
scope.currentRoom = subscope;
scope.currentRoomActive = true;
}
subscope.active = active;
}
if (flip) {
pane.toggleClass("flip");
}
};
scope.deactivateRoom = function() {
scope.currentRoomActive = false;
};
scope.$watch("layout.chat", function(chat) {
if (!chat) {
pane.removeClass("flip");
}
scope.layout.chatMaximized = false;
});
scope.$on("room", function(event, room) {
var subscope = scope.showGroupRoom(null, {
restore: true,
noenable: true,
noactivate: true
});
if (room) {
var msg = $("<span>").text(translation._("You are now in room %s ...", room));
subscope.$broadcast("display", null, $("<i>").append(msg));
}
});
};
};
return {
restrict: 'E',
replace: true,
scope: true,
template: templateChat,
controller: controller,
compile: compile
}
}];
});

79
static/js/directives/directives.js

@ -19,49 +19,48 @@ @@ -19,49 +19,48 @@
*
*/
define([
'underscore',
'underscore',
'directives/onenter',
'directives/onescape',
'directives/statusmessage',
'directives/buddylist',
'directives/settings',
'directives/chat',
'directives/audiovideo',
'directives/usability',
'directives/audiolevel',
'directives/fileinfo',
'directives/screenshare',
'directives/roombar',
'directives/socialshare',
'directives/page'
], function(_, onEnter, onEscape, statusMessage, buddyList, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page) {
'directives/onenter',
'directives/onescape',
'directives/statusmessage',
'directives/buddylist',
'directives/settings',
'directives/chat',
'directives/audiovideo',
'directives/usability',
'directives/audiolevel',
'directives/fileinfo',
'directives/screenshare',
'directives/roombar',
'directives/socialshare',
'directives/page'], function(_, onEnter, onEscape, statusMessage, buddyList, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page) {
var directives = {
onEnter: onEnter,
onEscape: onEscape,
statusMessage: statusMessage,
buddyList: buddyList,
settings: settings,
chat: chat,
audioVideo: audioVideo,
usability: usability,
audioLevel: audioLevel,
fileInfo: fileInfo,
screenshare: screenshare,
roomBar: roomBar,
socialShare: socialShare,
page: page
};
var directives = {
onEnter: onEnter,
onEscape: onEscape,
statusMessage: statusMessage,
buddyList: buddyList,
settings: settings,
chat: chat,
audioVideo: audioVideo,
usability: usability,
audioLevel: audioLevel,
fileInfo: fileInfo,
screenshare: screenshare,
roomBar: roomBar,
socialShare: socialShare,
page: page
};
var initialize = function (angModule) {
_.each(directives, function(directive, name) {
angModule.directive(name, directive);
})
};
var initialize = function(angModule) {
_.each(directives, function(directive, name) {
angModule.directive(name, directive);
})
};
return {
initialize: initialize
};
return {
initialize: initialize
};
});

12
static/js/directives/fileinfo.js

@ -46,10 +46,10 @@ define(['jquery', 'underscore'], function($, _) { @@ -46,10 +46,10 @@ define(['jquery', 'underscore'], function($, _) {
var progressBar = progressBars[0];
var progressBarDownload = progressBars[1];
$scope.$watch("progress", function() {
progressBar.style.width=($scope.progress)+"%";
progressBar.style.width = ($scope.progress) + "%";
});
$scope.$watch("progressDownload", function() {
progressBarDownload.style.width=($scope.progressDownload)+"%";
progressBarDownload.style.width = ($scope.progressDownload) + "%";
});
$scope.$watch("error", function() {
@ -85,7 +85,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -85,7 +85,7 @@ define(['jquery', 'underscore'], function($, _) {
}
// Compute left bytes for eta.
var bytesLeft = $scope.info.size - ($scope.info.size * ($scope.progress/100));
var bytesLeft = $scope.info.size - ($scope.info.size * ($scope.progress / 100));
$scope.$apply(function(scope) {
scope.bytesPerSecond = _.reduce(bytesPerSecond, function(memo, num) {
@ -177,8 +177,8 @@ define(['jquery', 'underscore'], function($, _) { @@ -177,8 +177,8 @@ define(['jquery', 'underscore'], function($, _) {
if ($scope.cancelled) {
return;
}
$scope.progress = Math.ceil((written / ($scope.info.chunks-1)) * 100);
if (written >= $scope.info.chunks-1) {
$scope.progress = Math.ceil((written / ($scope.info.chunks - 1)) * 100);
if (written >= $scope.info.chunks - 1) {
$scope.progress = 100;
}
});
@ -189,7 +189,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -189,7 +189,7 @@ define(['jquery', 'underscore'], function($, _) {
return;
}
bytesIn.push(bytesLength);
$scope.progressDownload = Math.ceil((chunk / ($scope.info.chunks-1)) * 100);
$scope.progressDownload = Math.ceil((chunk / ($scope.info.chunks - 1)) * 100);
chunk++;
});

36
static/js/directives/onenter.js

@ -20,22 +20,22 @@ @@ -20,22 +20,22 @@
*/
define([], function() {
// onEnter
return [function() {
return {
restrict: "A",
link: function(scope, element, attrs) {
var c = attrs.onEnter;
element.bind("keydown keypress", function(event) {
if (event.which === 13 && !event.shiftKey && !event.ctrlKey) {
// On enter whithout shift or ctrl.
event.preventDefault();
scope.$eval(c);
scope.$apply();
}
});
}
}
}];
// onEnter
return [function() {
return {
restrict: "A",
link: function(scope, element, attrs) {
var c = attrs.onEnter;
element.bind("keydown keypress", function(event) {
if (event.which === 13 && !event.shiftKey && !event.ctrlKey) {
// On enter whithout shift or ctrl.
event.preventDefault();
scope.$eval(c);
scope.$apply();
}
});
}
}
}];
});
});

36
static/js/directives/onescape.js

@ -20,22 +20,22 @@ @@ -20,22 +20,22 @@
*/
define([], function() {
// onEscape
return [function() {
return {
restrict: "A",
link: function(scope, element, attrs) {
var c = attrs.onEscape;
element.bind("keydown keypress", function(event) {
if (event.which === 27) {
// On escape.
event.preventDefault();
scope.$eval(c);
scope.$apply();
}
});
}
}
}];
// onEscape
return [function() {
return {
restrict: "A",
link: function(scope, element, attrs) {
var c = attrs.onEscape;
element.bind("keydown keypress", function(event) {
if (event.which === 27) {
// On escape.
event.preventDefault();
scope.$eval(c);
scope.$apply();
}
});
}
}
}];
});
});

86
static/js/directives/roombar.js

@ -20,58 +20,58 @@ @@ -20,58 +20,58 @@
*/
define(['underscore', 'text!partials/roombar.html'], function(_, template) {
// roomBar
return ["$window", "$rootScope", "$location", function($window, $rootScope, $location) {
// roomBar
return ["$window", "$rootScope", "$location", function($window, $rootScope, $location) {
var link = function($scope) {
var link = function($scope) {
//console.log("roomBar directive link", arguments);
$scope.newroomid = $rootScope.roomid;
$scope.hideRoomBar = true;
//console.log("roomBar directive link", arguments);
$scope.newroomid = $rootScope.roomid;
$scope.hideRoomBar = true;
$scope.save = function() {
var roomid = $scope.changeRoomToId($scope.newroomid);
if (roomid !== $rootScope.roomid) {
$scope.roombarform.$setPristine();
}
$scope.hideRoomBar = true;
};
$scope.save = function() {
var roomid = $scope.changeRoomToId($scope.newroomid);
if (roomid !== $rootScope.roomid) {
$scope.roombarform.$setPristine();
}
$scope.hideRoomBar = true;
};
$scope.hitEnter = function(evt){
if(angular.equals(evt.keyCode, 13)) {
$scope.save();
}
};
$scope.hitEnter = function(evt) {
if (angular.equals(evt.keyCode, 13)) {
$scope.save();
}
};
$scope.exit = function() {
$scope.newroomid = "";
$scope.save();
};
$scope.exit = function() {
$scope.newroomid = "";
$scope.save();
};
$rootScope.$watch("roomid", function(newroomid, roomid) {
if (!newroomid) {
newroomid = "";
}
$scope.newroomid = newroomid;
});
$rootScope.$watch("roomid", function(newroomid, roomid) {
if (!newroomid) {
newroomid = "";
}
$scope.newroomid = newroomid;
});
$scope.$watch("newroomid", function(newroomid) {
if (newroomid === $rootScope.roomid) {
$scope.roombarform.$setPristine();
}
});
$scope.$watch("newroomid", function(newroomid) {
if (newroomid === $rootScope.roomid) {
$scope.roombarform.$setPristine();
}
});
};
};
return {
restrict: 'E',
replace: true,
scope: true,
template: template,
controller: "RoomchangeController",
link: link
}
return {
restrict: 'E',
replace: true,
scope: true,
template: template,
controller: "RoomchangeController",
link: link
}
}];
}];
});

34
static/js/directives/screenshare.js

@ -51,7 +51,7 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -51,7 +51,7 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
var msg;
try {
msg = JSON.parse(data);
} catch(e) {
} catch (e) {
// Invalid JSON.
console.warn("Invalid JSON received from screen share channel.", data);
peerscreenshare.close();
@ -59,13 +59,13 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -59,13 +59,13 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
}
switch (msg.m) {
case "bye":
// Close this screen share.
peerscreenshare.close();
break;
default:
console.log("Unknown screen share control request", msg.m, msg);
break;
case "bye":
// Close this screen share.
peerscreenshare.close();
break;
default:
console.log("Unknown screen share control request", msg.m, msg);
break;
}
} else {
@ -195,15 +195,15 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -195,15 +195,15 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
scope.usermedia = usermedia;
// Create token to register with us and send token out to all peers.
// Peers when connect to us with the token and we answer.
var token = "screenshare_"+scope.id+"_"+(screenCount++);
var token = "screenshare_" + scope.id + "_" + (screenCount++);
// Updater function to bring in new calls.
var updater = function(event, state, currentcall) {
switch (state) {
case "completed":
case "connected":
connector(token, currentcall);
break;
case "completed":
case "connected":
connector(token, currentcall);
break;
}
};
@ -229,7 +229,9 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -229,7 +229,9 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
updated = null;
// Send by to all connected peers.
_.each(screenshares, function(peerscreenshare) {
peerscreenshare.send({m: "bye"});
peerscreenshare.send({
m: "bye"
});
$timeout(function() {
peerscreenshare.close();
}, 0);
@ -283,7 +285,7 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -283,7 +285,7 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
$scope.$watch("layout.screenshare", function(newval, oldval) {
if (newval && !oldval) {
$scope.doScreenshare();
} else if(!newval && oldval) {
} else if (!newval && oldval) {
$scope.stopScreenshare();
}
});
@ -309,4 +311,4 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -309,4 +311,4 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
}];
});
});

417
static/js/directives/settings.js

@ -22,213 +22,214 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -22,213 +22,214 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
return ["$compile", "mediaStream", function($compile, mediaStream) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation) {
$scope.layout.settings = false;
$scope.showAdvancedSettings = true;
$scope.showTakePicture = false;
$scope.showTakePictureReady = true;
$scope.rememberSettings = true;
$scope.desktopNotify = desktopNotify;
$scope.mediaSources = mediaSources;
$scope.availableLanguages = [
{
code: "",
name: translation._("Use browser language")
}
];
$scope.withUsers = mediaStream.config.UsersEnabled;
$scope.withUsersRegistration = mediaStream.config.UsersAllowRegistration;
$scope.withUsersMode = mediaStream.config.UsersMode;
_.each(availableLanguages, function(name, code) {
$scope.availableLanguages.push({
code: code,
name: name
});
});
var localStream = null;
// Make sure to save settings when they are open and the page is reloaded.
$(window).on("unload", function() {
if ($scope.layout.settings) {
$scope.saveSettings();
}
});
$scope.saveSettings = function() {
var user = $scope.user;
$scope.update(user);
$scope.layout.settings = false;
if ($scope.rememberSettings) {
localStorage.setItem("mediastream-user", JSON.stringify(user));
localStorage.setItem("mediastream-language", user.settings.language || "");
} else {
localStorage.removeItem("mediastream-user");
localStorage.removeItem("mediastream-language");
localStorage.removeItem("mediastream-access-code");
}
};
$scope.cancelSettings = function() {
$scope.reset();
$scope.layout.settings = false;
};
$scope.requestDesktopNotifyPermission = function() {
$scope.desktopNotify.requestPermission(function() {
safeApply($scope);
});
};
$scope.takePicture = function(element, stop) {
if (stop) {
$scope.showTakePicture = false;
if (localStream) {
localStream.stop();
localStream = null;
}
} else {
var video = $(element).parent().find("video").get(0);
if (!$scope.showTakePicture) {
$scope.showTakePictureReady = false;
var videoConstraints = true;
if ($scope.user.settings.cameraId) {
videoConstraints = {
optional: [{sourceId: $scope.user.settings.cameraId}]
}
}
getUserMedia({video: videoConstraints}, function(stream) {
if ($scope.showTakePictureReady) {
stream.stop()
return;
}
$scope.showTakePicture = true;
localStream = stream;
$scope.showTakePictureReady = true;
attachMediaStream(video, stream);
safeApply($scope);
}, function(error) {
console.error('Failed to get access to local media. Error code was ' + error.code);
$scope.showTakePictureReady = true;
safeApply($scope);
});
return;
} else {
var canvas = $(element).parent().find("canvas").get(0);
var videoWidth = video.videoWidth;
var videoHeight = video.videoHeight;
var aspectRatio = videoWidth/videoHeight;
if (!aspectRatio) {
// NOTE(longsleep): In Firefox the video size becomes available at sound point later - crap!
console.warn("Unable to compute aspectRatio", aspectRatio);
aspectRatio = 1.3333333333333333;
}
var x = (46*aspectRatio-46)/-2
canvas.getContext("2d").drawImage(video, x, 0, 46*aspectRatio, 46);
$scope.user.buddyPicture = canvas.toDataURL("image/jpeg");
console.info("Image size", $scope.user.buddyPicture.length);
localStream.stop();
localStream = null;
$scope.showTakePictureReady = true;
$scope.showTakePicture = false;
safeApply($scope);
}
}
};
$scope.registerUserid = function(btn) {
var successHandler = function(data) {
console.info("Created new userid:", data.userid);
// If the server provided us a nonce, we can do everthing on our own.
mediaStream.users.store(data);
$scope.loadedUserlogin = true;
safeApply($scope);
// Directly authenticate ourselves with the provided nonce.
mediaStream.api.requestAuthentication(data.userid, data.nonce);
delete data.nonce;
};
console.log("No userid - creating one ...");
mediaStream.users.register(btn.form, function(data) {
if (data.nonce) {
successHandler(data);
} else {
// No nonce received. So this means something we cannot do on our own.
// Make are GET request and retrieve nonce that way and let the
// browser/server do the rest.
mediaStream.users.authorize(data, successHandler, function(data, status) {
console.error("Failed to get nonce after create", status, data);
});
}
}, function(data, status) {
console.error("Failed to create userid", status, data);
});
};
$scope.forgetUserid = function() {
mediaStream.users.forget();
mediaStream.connector.forgetAndReconnect();
};
$scope.checkDefaultMediaSources = function() {
if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) {
$scope.master.settings.microphoneId=null;
}
if ($scope.master.settings.cameraId && !$scope.mediaSources.hasVideoId($scope.master.settings.cameraId)) {
$scope.master.settings.cameraId=null;
}
var audio = $scope.mediaSources.audio;
var video = $scope.mediaSources.video;
if (!$scope.master.settings.microphoneId && audio.length > 0) {
$scope.master.settings.microphoneId = audio[0].id;
}
if (!$scope.master.settings.cameraId && video.length > 0) {
$scope.master.settings.cameraId = $scope.mediaSources.video[0].id;
}
//console.log("master sources updates", $scope.master);
$scope.refreshWebrtcSettings();
};
$scope.mediaSources.refresh(function() {
safeApply($scope, $scope.checkDefaultMediaSources);
});
$scope.$watch("layout.settings", function(showSettings, oldValue) {
if (showSettings) {
$scope.desktopNotify.refresh();
$scope.mediaSources.refresh(function(audio, video) {
safeApply($scope, function(scope) {
if ($scope.user.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.user.settings.microphoneId)) {
$scope.user.settings.microphoneId=null;
}
if ($scope.user.settings.cameraId && !$scope.mediaSources.hasVideoId($scope.user.settings.cameraId)) {
$scope.user.settings.cameraId=null;
}
if (!$scope.user.settings.microphoneId && audio.length > 0) {
$scope.user.settings.microphoneId = audio[0].id;
}
if (!$scope.user.settings.cameraId && video.length > 0) {
$scope.user.settings.cameraId = video[0].id;
}
});
});
} else if (!showSettings && oldValue) {
$scope.saveSettings();
}
});
}];
var link = function($scope, $element) {
};
return {
scope: true,
restrict: 'E',
replace: true,
template: template,
controller: controller,
link: link
}
}];
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation) {
$scope.layout.settings = false;
$scope.showAdvancedSettings = true;
$scope.showTakePicture = false;
$scope.showTakePictureReady = true;
$scope.rememberSettings = true;
$scope.desktopNotify = desktopNotify;
$scope.mediaSources = mediaSources;
$scope.availableLanguages = [{
code: "",
name: translation._("Use browser language")
}];
$scope.withUsers = mediaStream.config.UsersEnabled;
$scope.withUsersRegistration = mediaStream.config.UsersAllowRegistration;
$scope.withUsersMode = mediaStream.config.UsersMode;
_.each(availableLanguages, function(name, code) {
$scope.availableLanguages.push({
code: code,
name: name
});
});
var localStream = null;
// Make sure to save settings when they are open and the page is reloaded.
$(window).on("unload", function() {
if ($scope.layout.settings) {
$scope.saveSettings();
}
});
$scope.saveSettings = function() {
var user = $scope.user;
$scope.update(user);
$scope.layout.settings = false;
if ($scope.rememberSettings) {
localStorage.setItem("mediastream-user", JSON.stringify(user));
localStorage.setItem("mediastream-language", user.settings.language || "");
} else {
localStorage.removeItem("mediastream-user");
localStorage.removeItem("mediastream-language");
localStorage.removeItem("mediastream-access-code");
}
};
$scope.cancelSettings = function() {
$scope.reset();
$scope.layout.settings = false;
};
$scope.requestDesktopNotifyPermission = function() {
$scope.desktopNotify.requestPermission(function() {
safeApply($scope);
});
};
$scope.takePicture = function(element, stop) {
if (stop) {
$scope.showTakePicture = false;
if (localStream) {
localStream.stop();
localStream = null;
}
} else {
var video = $(element).parent().find("video").get(0);
if (!$scope.showTakePicture) {
$scope.showTakePictureReady = false;
var videoConstraints = true;
if ($scope.user.settings.cameraId) {
videoConstraints = {
optional: [{
sourceId: $scope.user.settings.cameraId
}]
}
}
getUserMedia({
video: videoConstraints
}, function(stream) {
if ($scope.showTakePictureReady) {
stream.stop()
return;
}
$scope.showTakePicture = true;
localStream = stream;
$scope.showTakePictureReady = true;
attachMediaStream(video, stream);
safeApply($scope);
}, function(error) {
console.error('Failed to get access to local media. Error code was ' + error.code);
$scope.showTakePictureReady = true;
safeApply($scope);
});
return;
} else {
var canvas = $(element).parent().find("canvas").get(0);
var videoWidth = video.videoWidth;
var videoHeight = video.videoHeight;
var aspectRatio = videoWidth / videoHeight;
if (!aspectRatio) {
// NOTE(longsleep): In Firefox the video size becomes available at sound point later - crap!
console.warn("Unable to compute aspectRatio", aspectRatio);
aspectRatio = 1.3333333333333333;
}
var x = (46 * aspectRatio - 46) / -2
canvas.getContext("2d").drawImage(video, x, 0, 46 * aspectRatio, 46);
$scope.user.buddyPicture = canvas.toDataURL("image/jpeg");
console.info("Image size", $scope.user.buddyPicture.length);
localStream.stop();
localStream = null;
$scope.showTakePictureReady = true;
$scope.showTakePicture = false;
safeApply($scope);
}
}
};
$scope.registerUserid = function(btn) {
var successHandler = function(data) {
console.info("Created new userid:", data.userid);
// If the server provided us a nonce, we can do everthing on our own.
mediaStream.users.store(data);
$scope.loadedUserlogin = true;
safeApply($scope);
// Directly authenticate ourselves with the provided nonce.
mediaStream.api.requestAuthentication(data.userid, data.nonce);
delete data.nonce;
};
console.log("No userid - creating one ...");
mediaStream.users.register(btn.form, function(data) {
if (data.nonce) {
successHandler(data);
} else {
// No nonce received. So this means something we cannot do on our own.
// Make are GET request and retrieve nonce that way and let the
// browser/server do the rest.
mediaStream.users.authorize(data, successHandler, function(data, status) {
console.error("Failed to get nonce after create", status, data);
});
}
}, function(data, status) {
console.error("Failed to create userid", status, data);
});
};
$scope.forgetUserid = function() {
mediaStream.users.forget();
mediaStream.connector.forgetAndReconnect();
};
$scope.checkDefaultMediaSources = function() {
if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) {
$scope.master.settings.microphoneId = null;
}
if ($scope.master.settings.cameraId && !$scope.mediaSources.hasVideoId($scope.master.settings.cameraId)) {
$scope.master.settings.cameraId = null;
}
var audio = $scope.mediaSources.audio;
var video = $scope.mediaSources.video;
if (!$scope.master.settings.microphoneId && audio.length > 0) {
$scope.master.settings.microphoneId = audio[0].id;
}
if (!$scope.master.settings.cameraId && video.length > 0) {
$scope.master.settings.cameraId = $scope.mediaSources.video[0].id;
}
//console.log("master sources updates", $scope.master);
$scope.refreshWebrtcSettings();
};
$scope.mediaSources.refresh(function() {
safeApply($scope, $scope.checkDefaultMediaSources);
});
$scope.$watch("layout.settings", function(showSettings, oldValue) {
if (showSettings) {
$scope.desktopNotify.refresh();
$scope.mediaSources.refresh(function(audio, video) {
safeApply($scope, function(scope) {
if ($scope.user.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.user.settings.microphoneId)) {
$scope.user.settings.microphoneId = null;
}
if ($scope.user.settings.cameraId && !$scope.mediaSources.hasVideoId($scope.user.settings.cameraId)) {
$scope.user.settings.cameraId = null;
}
if (!$scope.user.settings.microphoneId && audio.length > 0) {
$scope.user.settings.microphoneId = audio[0].id;
}
if (!$scope.user.settings.cameraId && video.length > 0) {
$scope.user.settings.cameraId = video[0].id;
}
});
});
} else if (!showSettings && oldValue) {
$scope.saveSettings();
}
});
}];
var link = function($scope, $element) {};
return {
scope: true,
restrict: 'E',
replace: true,
template: template,
controller: controller,
link: link
}
}];
});

6
static/js/directives/socialshare.js

@ -50,11 +50,11 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) { @@ -50,11 +50,11 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
var nw = $(event.currentTarget).data("nw");
var url = makeUrl(nw, $scope.roomlink);
if (url) {
if (nw==="email") {
if (nw === "email") {
// Hack our way to disable unload popup for mail links.
$scope.manualReloadApp(url);
} else {
$window.open(url, "social_"+nw, "menubar=no,toolbar=no,resizable=yes,width=600,height=600,scrollbars=yes");
$window.open(url, "social_" + nw, "menubar=no,toolbar=no,resizable=yes,width=600,height=600,scrollbars=yes");
}
}
});
@ -63,4 +63,4 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) { @@ -63,4 +63,4 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
}];
});
});

158
static/js/directives/usability.js

@ -20,97 +20,97 @@ @@ -20,97 +20,97 @@
*/
define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, template) {
var MEDIA_CHECK="1" // First version of media check flag.
var MEDIA_CHECK = "1" // First version of media check flag.
return ["mediaStream", function(mediaStream) {
return ["mediaStream", function(mediaStream) {
var controller = ['$scope', "mediaStream", "safeApply", "$timeout", function($scope, mediaStream, safeApply, $timeout) {
var controller = ['$scope', "mediaStream", "safeApply", "$timeout", function($scope, mediaStream, safeApply, $timeout) {
var pending = true;
var complete = false;
var pending = true;
var complete = false;
var initalizer = null;
var initalizer = null;
var ctrl = this;
ctrl.setInfo = function(info) {
$scope.usabilityInfo = info;
};
ctrl.setInfo("waiting");
var ctrl = this;
ctrl.setInfo = function(info) {
$scope.usabilityInfo = info;
};
ctrl.setInfo("waiting");
$scope.continueConnect = function(status) {
safeApply($scope, function() {
pending = false;
if (status) {
localStorage.setItem("mediastream-mediacheck", MEDIA_CHECK)
$scope.connect()
ctrl.setInfo("initializing");
initializer = $timeout(function() {
ctrl.setInfo("ok");
$scope.$emit("welcome");
}, 1000);
complete = true;
} else {
ctrl.setInfo("denied");
}
// Check if we should show settings per default.
$scope.layout.settings = $scope.loadedUser ? false : true;
});
};
$scope.continueConnect = function(status) {
safeApply($scope, function() {
pending = false;
if (status) {
localStorage.setItem("mediastream-mediacheck", MEDIA_CHECK)
$scope.connect()
ctrl.setInfo("initializing");
initializer = $timeout(function() {
ctrl.setInfo("ok");
$scope.$emit("welcome");
}, 1000);
complete = true;
} else {
ctrl.setInfo("denied");
}
// Check if we should show settings per default.
$scope.layout.settings = $scope.loadedUser ? false : true;
});
};
$scope.testMediaAccess = function() {
//console.log("Test media access");
var passedBefore = localStorage.getItem("mediastream-mediacheck");
if (passedBefore !== MEDIA_CHECK && $scope.isChrome) {
// NOTE(longsleep): Checkin for media access makes only sense on
// Chrome for now, as its the only one which remembers this
// decision permanently for https.
mediaStream.webrtc.testMediaAccess($scope.continueConnect);
} else {
$scope.continueConnect(true);
}
};
$scope.testMediaAccess = function() {
//console.log("Test media access");
var passedBefore = localStorage.getItem("mediastream-mediacheck");
if (passedBefore !== MEDIA_CHECK && $scope.isChrome) {
// NOTE(longsleep): Checkin for media access makes only sense on
// Chrome for now, as its the only one which remembers this
// decision permanently for https.
mediaStream.webrtc.testMediaAccess($scope.continueConnect);
} else {
$scope.continueConnect(true);
}
};
$scope.retry = function() {
ctrl.setInfo("usermedia");
$scope.testMediaAccess();
};
$scope.retry = function() {
ctrl.setInfo("usermedia");
$scope.testMediaAccess();
};
// Toplevel watcher for connect function to become available.
$scope.$watch("connect", function() {
if ($scope.connect) {
console.log("Connecting ...");
ctrl.setInfo("checking");
$timeout(function() {
if (pending) {
safeApply($scope, function() {
ctrl.setInfo("usermedia");
});
}
}, 500);
$scope.testMediaAccess();
}
});
// Toplevel watcher for connect function to become available.
$scope.$watch("connect", function() {
if ($scope.connect) {
console.log("Connecting ...");
ctrl.setInfo("checking");
$timeout(function() {
if (pending) {
safeApply($scope, function() {
ctrl.setInfo("usermedia");
});
}
}, 500);
$scope.testMediaAccess();
}
});
$scope.$on("room", function(event, room) {
//console.log("roomStatus", room !== null ? true : false);
if (complete) {
if (initializer !== null) {
$timeout.cancel(initializer);
initializer = null;
}
ctrl.setInfo("ok");
}
});
$scope.$on("room", function(event, room) {
//console.log("roomStatus", room !== null ? true : false);
if (complete) {
if (initializer !== null) {
$timeout.cancel(initializer);
initializer = null;
}
ctrl.setInfo("ok");
}
});
}];
}];
return {
restrict: 'E',
replace: true,
template: template,
controller: controller
}
return {
restrict: 'E',
replace: true,
template: template,
controller: controller
}
}];
}];
});

157
static/js/filters/buddyimagesrc.js

@ -20,90 +20,93 @@ @@ -20,90 +20,93 @@
*/
define(["underscore"], function(_) {
// Simple function which converts data urls to blobs, both base64 or not.
var dataURLToBlob = (function() {
var is_base64 = ";base64,";
return function(dataURL) {
var parts, ct;
if (dataURL.indexOf(is_base64) === -1) {
// No base64.
parts = dataURL.split(",");
ct = parts[0].split(":")[1];
return new Blob([parts[1]], {type: ct});
}
parts = dataURL.split(is_base64);
ct = parts[0].split(":")[1];
var data = window.atob(parts[1]);
var length = data.length;
var buffer = new Uint8Array(length);
for (var i = 0; i < length; i++) {
buffer[i] = data.charCodeAt(i);
}
return new Blob([buffer], {type: ct});
};
}());
// Simple function which converts data urls to blobs, both base64 or not.
var dataURLToBlob = (function() {
var is_base64 = ";base64,";
return function(dataURL) {
var parts, ct;
if (dataURL.indexOf(is_base64) === -1) {
// No base64.
parts = dataURL.split(",");
ct = parts[0].split(":")[1];
return new Blob([parts[1]], {
type: ct
});
}
parts = dataURL.split(is_base64);
ct = parts[0].split(":")[1];
var data = window.atob(parts[1]);
var length = data.length;
var buffer = new Uint8Array(length);
for (var i = 0; i < length; i++) {
buffer[i] = data.charCodeAt(i);
}
return new Blob([buffer], {
type: ct
});
};
}());
// Create URLs for blobs.
var blobToObjectURL = function(blob) {
return URL.createObjectURL(blob);
};
// Create URLs for blobs.
var blobToObjectURL = function(blob) {
return URL.createObjectURL(blob);
};
var revokeBlobURL = function(url) {
return URL.revokeObjectURL(url);
};
var revokeBlobURL = function(url) {
return URL.revokeObjectURL(url);
};
// buddyImageSrc
return ["buddyData", "appData", function(buddyData, appData) {
// buddyImageSrc
return ["buddyData", "appData", function(buddyData, appData) {
// Cache created blob urls.
var urls = {};
var revokeURL = function(id, url) {
delete urls[id];
revokeBlobURL(url);
};
// Cache created blob urls.
var urls = {};
var revokeURL = function(id, url) {
delete urls[id];
revokeBlobURL(url);
};
// Cleanup helper.
window.setInterval(function() {
_.each(urls, function(url, id) {
if (!buddyData.get(id)) {
revokeURL(id, url);
}
});
}, 5000);
// Cleanup helper.
window.setInterval(function() {
_.each(urls, function(url, id) {
if (!buddyData.get(id)) {
revokeURL(id, url);
}
});
}, 5000);
return function(id) {
return function(id) {
var scope = buddyData.lookup(id);
if (scope) {
var status = scope.status;
if (status) {
if (status.buddyPictureLocalUrl) {
return status.buddyPictureLocalUrl;
}
else if (status.buddyPicture) {
var url = urls[id];
if (url) {
revokeURL(id, url);
}
// New data -> new url.
var blob = dataURLToBlob(status.buddyPicture);
url = status.buddyPictureLocalUrl = urls[id] = blobToObjectURL(blob);
return url;
}
}
} else {
var data = appData.get();
if (data) {
if (id === data.id) {
if (data.master.buddyPicture) {
return data.master.buddyPicture;
}
}
}
}
return "";
};
var scope = buddyData.lookup(id);
if (scope) {
var status = scope.status;
if (status) {
if (status.buddyPictureLocalUrl) {
return status.buddyPictureLocalUrl;
} else if (status.buddyPicture) {
var url = urls[id];
if (url) {
revokeURL(id, url);
}
// New data -> new url.
var blob = dataURLToBlob(status.buddyPicture);
url = status.buddyPictureLocalUrl = urls[id] = blobToObjectURL(blob);
return url;
}
}
} else {
var data = appData.get();
if (data) {
if (id === data.id) {
if (data.master.buddyPicture) {
return data.master.buddyPicture;
}
}
}
}
return "";
};
}];
}];
});

26
static/js/filters/displayconference.js

@ -20,18 +20,18 @@ @@ -20,18 +20,18 @@
*/
define([], function() {
// displayConference
return ["safeDisplayName", "translation", function(safeDisplayName, translation) {
return function(peers) {
if (!peers || peers.length === 0) {
return "";
}
if (peers.length === 1) {
return " " + translation._("and %s", safeDisplayName(peers[0]));
} else {
return " " + translation._("and %d others", peers.length);
}
};
}];
// displayConference
return ["safeDisplayName", "translation", function(safeDisplayName, translation) {
return function(peers) {
if (!peers || peers.length === 0) {
return "";
}
if (peers.length === 1) {
return " " + translation._("and %s", safeDisplayName(peers[0]));
} else {
return " " + translation._("and %d others", peers.length);
}
};
}];
});

82
static/js/filters/displayname.js

@ -20,46 +20,46 @@ @@ -20,46 +20,46 @@
*/
define([], function() {
// displayName
return ["buddyData", "appData", "translation", function(buddyData, appData, translation) {
var group_chat_id = "";
var someones = {
count: 1
};
var user_text = translation._("User");
var someone_text = translation._("Someone");
var me_text = translation._("Me");
return function(id, me_ok) {
if (id === group_chat_id) {
return "";
}
var scope = buddyData.lookup(id);
if (scope) {
if (scope.displayName) {
return scope.displayName;
}
return user_text + " " + scope.buddyIndex;
} else {
var data = appData.get();
if (data) {
if (id === data.id) {
if (me_ok) {
return me_text;
}
if (data.master.displayName) {
return data.master.displayName;
}
return me_text;
}
}
var someone = someones[id];
if (!someone) {
someone = someone_text + " " + someones.count++;
someones[id] = someone;
}
return someone;
}
};
}];
// displayName
return ["buddyData", "appData", "translation", function(buddyData, appData, translation) {
var group_chat_id = "";
var someones = {
count: 1
};
var user_text = translation._("User");
var someone_text = translation._("Someone");
var me_text = translation._("Me");
return function(id, me_ok) {
if (id === group_chat_id) {
return "";
}
var scope = buddyData.lookup(id);
if (scope) {
if (scope.displayName) {
return scope.displayName;
}
return user_text + " " + scope.buddyIndex;
} else {
var data = appData.get();
if (data) {
if (id === data.id) {
if (me_ok) {
return me_text;
}
if (data.master.displayName) {
return data.master.displayName;
}
return me_text;
}
}
var someone = someones[id];
if (!someone) {
someone = someone_text + " " + someones.count++;
someones[id] = someone;
}
return someone;
}
};
}];
});

37
static/js/filters/filters.js

@ -19,27 +19,26 @@ @@ -19,27 +19,26 @@
*
*/
define([
'underscore',
'underscore',
'filters/displayname',
'filters/buddyimagesrc',
'filters/displayconference'
], function(_, displayName, buddyImageSrc, displayConference) {
'filters/displayname',
'filters/buddyimagesrc',
'filters/displayconference'], function(_, displayName, buddyImageSrc, displayConference) {
var filters = {
displayName: displayName,
buddyImageSrc: buddyImageSrc,
displayConference: displayConference
};
var filters = {
displayName: displayName,
buddyImageSrc: buddyImageSrc,
displayConference: displayConference
};
var initialize = function (angModule) {
_.each(filters, function(filter, name) {
angModule.filter(name, filter);
})
}
var initialize = function(angModule) {
_.each(filters, function(filter, name) {
angModule.filter(name, filter);
})
}
return {
initialize: initialize
};
return {
initialize: initialize
};
});
});

331
static/js/main.js

@ -19,182 +19,181 @@ @@ -19,182 +19,181 @@
*
*/
require.config({
waitSeconds: 300,
paths: {
// Major libraries
"text": "libs/require/text",
"jquery": 'libs/jquery/jquery.min',
"underscore": 'libs/lodash.min', // alternative to underscore
"modernizr": 'libs/modernizr',
'webrtc.adapter': 'libs/webrtc.adapter',
'angular': 'libs/angular/angular.min',
'ui-bootstrap': 'libs/angular/ui-bootstrap-tpls.min',
'ua-parser': 'libs/ua-parser',
'Howler': 'libs/howler.min',
'desktop-notify': 'libs/desktop-notify',
'bigscreen': 'libs/bigscreen.min',
'moment': 'libs/moment.min',
'angular-sanitize': 'libs/angular/angular-sanitize.min',
'angular-animate': 'libs/angular/angular-animate.min',
'angular-route': 'libs/angular/angular-route.min',
'angular-humanize': 'modules/angular-humanize',
'toastr': 'libs/toastr',
'visibly': 'libs/visibly',
'avltree': 'libs/avltree',
'injectCSS': 'libs/jquery/jquery.injectCSS',
'mobile-events': 'libs/jquery/jquery.mobile-events',
'jed': 'libs/jed',
'audiocontext': 'libs/audiocontext',
'rAF': 'libs/rAF',
'humanize': 'libs/humanize',
'sha': 'libs/sha',
'dialogs': 'libs/angular/dialogs.min',
'sjcl': 'libs/sjcl',
waitSeconds: 300,
paths: {
// Major libraries
"text": "libs/require/text",
"jquery": 'libs/jquery/jquery.min',
"underscore": 'libs/lodash.min', // alternative to underscore
"modernizr": 'libs/modernizr',
'webrtc.adapter': 'libs/webrtc.adapter',
'angular': 'libs/angular/angular.min',
'ui-bootstrap': 'libs/angular/ui-bootstrap-tpls.min',
'ua-parser': 'libs/ua-parser',
'Howler': 'libs/howler.min',
'desktop-notify': 'libs/desktop-notify',
'bigscreen': 'libs/bigscreen.min',
'moment': 'libs/moment.min',
'angular-sanitize': 'libs/angular/angular-sanitize.min',
'angular-animate': 'libs/angular/angular-animate.min',
'angular-route': 'libs/angular/angular-route.min',
'angular-humanize': 'modules/angular-humanize',
'toastr': 'libs/toastr',
'visibly': 'libs/visibly',
'avltree': 'libs/avltree',
'injectCSS': 'libs/jquery/jquery.injectCSS',
'mobile-events': 'libs/jquery/jquery.mobile-events',
'jed': 'libs/jed',
'audiocontext': 'libs/audiocontext',
'rAF': 'libs/rAF',
'humanize': 'libs/humanize',
'sha': 'libs/sha',
'dialogs': 'libs/angular/dialogs.min',
'sjcl': 'libs/sjcl',
'partials': '../partials',
'sounds': '../sounds',
'translation': '../translation'
},
shim: {
'modernizr': {
exports: 'Modernizr'
},
'underscore': {
exports: '_'
},
'angular': {
deps: ['jquery'],
exports: 'angular'
},
'ui-bootstrap': {
deps: ['angular']
},
'desktop-notify': {
exports: 'notify'
},
'bigscreen': {
exports: 'BigScreen'
},
'moment': {
exports: 'moment'
},
'angular-sanitize': {
deps: ['angular'],
exports: 'angular'
},
'angular-animate': {
deps: ['angular'],
exports: 'angular'
},
'angular-humanize': {
deps: ['angular', 'humanize'],
exports: 'angular'
},
'toastr': {
deps: ['jquery'],
exports: 'toastr'
},
'visibly': {
exports: 'visibly'
},
'avltree': {
exports: 'AvlTree'
},
'injectCSS': {
deps: ['jquery'],
exports: '$'
},
'mobile-events': {
deps: ['jquery'],
exports: '$'
},
'dialogs': {
deps: ['angular', 'angular-sanitize'],
exports: 'angular'
},
'sjcl': {
exports: 'sjcl'
}
}
'partials': '../partials',
'sounds': '../sounds',
'translation': '../translation'
},
shim: {
'modernizr': {
exports: 'Modernizr'
},
'underscore': {
exports: '_'
},
'angular': {
deps: ['jquery'],
exports: 'angular'
},
'ui-bootstrap': {
deps: ['angular']
},
'desktop-notify': {
exports: 'notify'
},
'bigscreen': {
exports: 'BigScreen'
},
'moment': {
exports: 'moment'
},
'angular-sanitize': {
deps: ['angular'],
exports: 'angular'
},
'angular-animate': {
deps: ['angular'],
exports: 'angular'
},
'angular-humanize': {
deps: ['angular', 'humanize'],
exports: 'angular'
},
'toastr': {
deps: ['jquery'],
exports: 'toastr'
},
'visibly': {
exports: 'visibly'
},
'avltree': {
exports: 'AvlTree'
},
'injectCSS': {
deps: ['jquery'],
exports: '$'
},
'mobile-events': {
deps: ['jquery'],
exports: '$'
},
'dialogs': {
deps: ['angular', 'angular-sanitize'],
exports: 'angular'
},
'sjcl': {
exports: 'sjcl'
}
}
});
(function() {
var debugDefault = window.location.href.match(/(\?|&)debug($|&|=)/);
// Overwrite console to not log stuff per default.
// Write debug(true) in console to enable or start with ?debug parameter.
window.consoleBackup = null;
window.debug = function(flag) {
if (!flag) {
if (window.consoleBackup === null) {
window.consoleBackup = window.console;
}
window.console = {
log: function() {},
info: function() {},
warn: function() {},
error: function() {},
debug: function() {},
trace: function() {}
}
} else {
if (window.consoleBackup) {
window.console = window.consoleBackup;
}
}
};
window.debug(debugDefault && true);
var debugDefault = window.location.href.match(/(\?|&)debug($|&|=)/);
// Overwrite console to not log stuff per default.
// Write debug(true) in console to enable or start with ?debug parameter.
window.consoleBackup = null;
window.debug = function(flag) {
if (!flag) {
if (window.consoleBackup === null) {
window.consoleBackup = window.console;
}
window.console = {
log: function() {},
info: function() {},
warn: function() {},
error: function() {},
debug: function() {},
trace: function() {}
}
} else {
if (window.consoleBackup) {
window.console = window.consoleBackup;
}
}
};
window.debug(debugDefault && true);
}());
require.onError = (function() {
var retrying = false;
return function(err) {
if (retrying) {
console.error("Error while loading "+err.requireType, err.requireModules);
return;
}
if (err.requireType === "timeout" || err.requireType === "scripterror") {
alert('Failed to load application. Confirm to retry.');
retrying = true;
document.location.reload(true);
} else {
throw err;
}
};
var retrying = false;
return function(err) {
if (retrying) {
console.error("Error while loading " + err.requireType, err.requireModules);
return;
}
if (err.requireType === "timeout" || err.requireType === "scripterror") {
alert('Failed to load application. Confirm to retry.');
retrying = true;
document.location.reload(true);
} else {
throw err;
}
};
}());
define([
'jquery',
'underscore',
'angular',
'require',
'base'
], function ($, _, angular, require) {
'jquery',
'underscore',
'angular',
'require',
'base'], function($, _, angular, require) {
// Dynamic app loader with plugin support.
var load = ['app'];
_.each(document.getElementsByTagName('script'), function(script) {
var dataPlugin = script.getAttribute('data-plugin');
if (dataPlugin) {
load.push(dataPlugin);
}
});
require(load, function(App) {
var args = Array.prototype.slice.call(arguments, 1);
// Add Angular modules from plugins.
var modules = [];
_.each(args, function(plugin) {
if (plugin && plugin.module) {
plugin.module(modules);
}
});
// Init Angular app.
var app = App.initialize(modules);
// Init plugins.
_.each(args, function(plugin) {
if (plugin && plugin.initialize) {
plugin.initialize(app);
}
});
});
// Dynamic app loader with plugin support.
var load = ['app'];
_.each(document.getElementsByTagName('script'), function(script) {
var dataPlugin = script.getAttribute('data-plugin');
if (dataPlugin) {
load.push(dataPlugin);
}
});
require(load, function(App) {
var args = Array.prototype.slice.call(arguments, 1);
// Add Angular modules from plugins.
var modules = [];
_.each(args, function(plugin) {
if (plugin && plugin.module) {
plugin.module(modules);
}
});
// Init Angular app.
var app = App.initialize(modules);
// Init plugins.
_.each(args, function(plugin) {
if (plugin && plugin.initialize) {
plugin.initialize(app);
}
});
});
});

542
static/js/mediastream/api.js

@ -20,305 +20,307 @@ @@ -20,305 +20,307 @@
*/
define(['jquery', 'underscore'], function($, _) {
var alive_check_timeout = 5000;
var alive_check_timeout_2 = 10000;
var Api = function(connector) {
this.id = null;
this.sid = null;
this.session = {};
this.connector = connector;
this.e = $({});
connector.e.on("received", _.bind(function(event, data) {
this.received(data);
}, this));
// Trigger alive heartbeat when nothing is received for a while.
this.heartbeat = window.setInterval(_.bind(function() {
var last_receive = this.last_receive;
if (this.connector.connected) {
if (last_receive !== null) {
//console.log("api heartbeat", this.last_receive);
var now = new Date().getTime();
if (this.last_receive_overdue) {
if (now > last_receive+alive_check_timeout_2) {
console.log("Reconnecting because alive timeout was reached.");
this.last_receive_overdue = false;
this.last_receive = null;
this.connector.reconnect();
}
} else {
if (now > last_receive+alive_check_timeout) {
//console.log("overdue 1");
this.last_receive_overdue = true;
this.sendAlive(now);
}
}
}
} else {
this.last_receive = null;
this.last_receive_overdue = false;
}
}, this), 1000);
this.last_receive = null;
this.last_receive_overdue = false;
};
Api.prototype.send = function(type, data, noqueue) {
var payload = {Type: type};
payload[type] = data;
//console.log("<<<<<<<<<<<<", JSON.stringify(payload));
this.connector.send(payload, noqueue);
};
Api.prototype.send2 = function(name, cb) {
var obj = {
send: _.bind(function(type, data) {
if (cb) {
cb(type, data);
}
this.send(type, data);
}, this)
}
return this.apply(name, obj);
};
// Helper hack function to send API requests to other destinations.
// Simply provide an alternative send function on the obj Object.
Api.prototype.apply = function(name, obj) {
var f = this[name];
return _.bind(f, obj);
};
Api.prototype.received = function(d) {
// Store received timestamp.
var now = new Date().getTime();
this.last_receive = now;
this.last_receive_overdue = false;
var data = d.Data;
var dataType = data.Type;
switch (dataType) {
case "Self":
console.log("Self received", data);
if (data.Token) {
this.connector.token = data.Token;
}
this.id = data.Id;
this.sid = data.Sid;
this.e.triggerHandler("received.self", [data]);
break;
case "Offer":
console.log("Offer received", data.To, data.Offer);
this.e.triggerHandler("received.offer", [data.To, data.Offer, data.Type, d.To, d.From]);
break;
case "Candidate":
//console.log("Candidate received", data.To, data.Candidate);
this.e.triggerHandler("received.candidate", [data.To, data.Candidate, data.Type, d.To, d.From]);
break;
case "Answer":
console.log("Answer received", data.To, data.Answer);
this.e.triggerHandler("received.answer", [data.To, data.Answer, data.Type, d.To, d.From]);
break;
case "Users":
console.log("Connected users: " + data.Users.length);
this.e.triggerHandler("received.users", [data.Users]);
break;
case "Bye":
console.log("Bye received", data.To, data.Bye);
this.e.triggerHandler("received.bye", [data.To, data.Bye, data.Type, d.To, d.From]);
break;
case "Joined":
case "Left":
//console.log("User action received", dataType, data);
this.e.triggerHandler("received.userleftorjoined", [dataType, data]);
break;
case "Status":
//console.log("User status received", dataType, data);
this.e.triggerHandler("received.status", [data]);
break;
case "Chat":
//console.log("chat received", dataType, data);
this.e.triggerHandler("received.chat", [data.To, d.From, data.Chat, d.p2p]);
break;
case "Conference":
this.e.triggerHandler("received.conference", [data.Id, data.Conference, data.Type, d.To, d.From]);
break;
case "Talking":
this.e.triggerHandler("received.talking", [d.To, d.From, data.Talking]);
break;
case "Screenshare":
this.e.triggerHandler("received.screenshare", [d.To, d.From, data.Screenshare, d.p2p]);
break;
case "Alive":
// Do nothing.
//console.log("Alive response received.");
break;
default:
console.log("Unhandled type received:", dataType, data);
break;
}
};
Api.prototype.sendSelf = function() {
var data = {
Type: "Self",
Self: {}
}
return this.send("Self", data, true);
};
Api.prototype.sendOffer = function(to, payload) {
var data = {
To: to,
Type: "Offer",
Offer: payload
}
return this.send("Offer", data);
};
Api.prototype.sendCandidate = function(to, payload) {
var data = {
To: to,
Type: "Candidate",
Candidate: payload
}
return this.send("Candidate", data);
}
Api.prototype.sendAnswer = function(to, payload) {
var data = {
To: to,
Type: "Answer",
Answer: payload
}
return this.send("Answer", data);
}
Api.prototype.requestUsers = function() {
var data = {
Type: "Users",
Users: {}
}
var alive_check_timeout = 5000;
var alive_check_timeout_2 = 10000;
var Api = function(connector) {
this.id = null;
this.sid = null;
this.session = {};
this.connector = connector;
this.e = $({});
connector.e.on("received", _.bind(function(event, data) {
this.received(data);
}, this));
// Trigger alive heartbeat when nothing is received for a while.
this.heartbeat = window.setInterval(_.bind(function() {
var last_receive = this.last_receive;
if (this.connector.connected) {
if (last_receive !== null) {
//console.log("api heartbeat", this.last_receive);
var now = new Date().getTime();
if (this.last_receive_overdue) {
if (now > last_receive + alive_check_timeout_2) {
console.log("Reconnecting because alive timeout was reached.");
this.last_receive_overdue = false;
this.last_receive = null;
this.connector.reconnect();
}
} else {
if (now > last_receive + alive_check_timeout) {
//console.log("overdue 1");
this.last_receive_overdue = true;
this.sendAlive(now);
}
}
}
} else {
this.last_receive = null;
this.last_receive_overdue = false;
}
}, this), 1000);
this.last_receive = null;
this.last_receive_overdue = false;
};
Api.prototype.send = function(type, data, noqueue) {
var payload = {
Type: type
};
payload[type] = data;
//console.log("<<<<<<<<<<<<", JSON.stringify(payload));
this.connector.send(payload, noqueue);
};
Api.prototype.send2 = function(name, cb) {
var obj = {
send: _.bind(function(type, data) {
if (cb) {
cb(type, data);
}
this.send(type, data);
}, this)
}
return this.apply(name, obj);
};
// Helper hack function to send API requests to other destinations.
// Simply provide an alternative send function on the obj Object.
Api.prototype.apply = function(name, obj) {
var f = this[name];
return _.bind(f, obj);
};
Api.prototype.received = function(d) {
// Store received timestamp.
var now = new Date().getTime();
this.last_receive = now;
this.last_receive_overdue = false;
var data = d.Data;
var dataType = data.Type;
switch (dataType) {
case "Self":
console.log("Self received", data);
if (data.Token) {
this.connector.token = data.Token;
}
this.id = data.Id;
this.sid = data.Sid;
this.e.triggerHandler("received.self", [data]);
break;
case "Offer":
console.log("Offer received", data.To, data.Offer);
this.e.triggerHandler("received.offer", [data.To, data.Offer, data.Type, d.To, d.From]);
break;
case "Candidate":
//console.log("Candidate received", data.To, data.Candidate);
this.e.triggerHandler("received.candidate", [data.To, data.Candidate, data.Type, d.To, d.From]);
break;
case "Answer":
console.log("Answer received", data.To, data.Answer);
this.e.triggerHandler("received.answer", [data.To, data.Answer, data.Type, d.To, d.From]);
break;
case "Users":
console.log("Connected users: " + data.Users.length);
this.e.triggerHandler("received.users", [data.Users]);
break;
case "Bye":
console.log("Bye received", data.To, data.Bye);
this.e.triggerHandler("received.bye", [data.To, data.Bye, data.Type, d.To, d.From]);
break;
case "Joined":
case "Left":
//console.log("User action received", dataType, data);
this.e.triggerHandler("received.userleftorjoined", [dataType, data]);
break;
case "Status":
//console.log("User status received", dataType, data);
this.e.triggerHandler("received.status", [data]);
break;
case "Chat":
//console.log("chat received", dataType, data);
this.e.triggerHandler("received.chat", [data.To, d.From, data.Chat, d.p2p]);
break;
case "Conference":
this.e.triggerHandler("received.conference", [data.Id, data.Conference, data.Type, d.To, d.From]);
break;
case "Talking":
this.e.triggerHandler("received.talking", [d.To, d.From, data.Talking]);
break;
case "Screenshare":
this.e.triggerHandler("received.screenshare", [d.To, d.From, data.Screenshare, d.p2p]);
break;
case "Alive":
// Do nothing.
//console.log("Alive response received.");
break;
default:
console.log("Unhandled type received:", dataType, data);
break;
}
};
Api.prototype.sendSelf = function() {
var data = {
Type: "Self",
Self: {}
}
return this.send("Self", data, true);
};
Api.prototype.sendOffer = function(to, payload) {
var data = {
To: to,
Type: "Offer",
Offer: payload
}
return this.send("Offer", data);
};
Api.prototype.sendCandidate = function(to, payload) {
var data = {
To: to,
Type: "Candidate",
Candidate: payload
}
return this.send("Candidate", data);
}
Api.prototype.sendAnswer = function(to, payload) {
var data = {
To: to,
Type: "Answer",
Answer: payload
}
return this.send("Answer", data);
}
Api.prototype.requestUsers = function() {
var data = {
Type: "Users",
Users: {}
}
return this.send("Users", data);
return this.send("Users", data);
};
};
Api.prototype.requestAuthentication = function(userid, nonce) {
Api.prototype.requestAuthentication = function(userid, nonce) {
var data = {
Type: "Authentication",
Authentication: {
Userid: userid,
Nonce: nonce
}
}
var data = {
Type: "Authentication",
Authentication: {
Userid: userid,
Nonce: nonce
}
}
return this.send("Authentication", data);
return this.send("Authentication", data);
};
};
Api.prototype.updateStatus = function(status) {
Api.prototype.updateStatus = function(status) {
var data = {
Type: "Status",
Status: status
}
var data = {
Type: "Status",
Status: status
}
return this.send("Status", data);
return this.send("Status", data);
};
};
Api.prototype.sendBye = function(to, reason) {
Api.prototype.sendBye = function(to, reason) {
var data = {
To: to,
Type: "Bye",
Bye: {
Reason: reason
}
}
var data = {
To: to,
Type: "Bye",
Bye: {
Reason: reason
}
}
return this.send("Bye", data);
return this.send("Bye", data);
};
};
Api.prototype.sendChat = function(to, message, status, mid) {
Api.prototype.sendChat = function(to, message, status, mid) {
var data = {
To: to,
Type: "Chat",
Chat: {
Mid: mid,
Message: message,
Status: status,
NoEcho: true // This client shows own messages internally.
}
}
var data = {
To: to,
Type: "Chat",
Chat: {
Mid: mid,
Message: message,
Status: status,
NoEcho: true // This client shows own messages internally.
}
}
return this.send("Chat", data);
return this.send("Chat", data);
};
};
Api.prototype.sendConference = function(id, ids) {
Api.prototype.sendConference = function(id, ids) {
var data = {
Id: id,
Type: "Conference",
Conference: ids
}
var data = {
Id: id,
Type: "Conference",
Conference: ids
}
return this.send("Conference", data);
return this.send("Conference", data);
};
};
Api.prototype.sendScreenshare = function(id, screen_id) {
Api.prototype.sendScreenshare = function(id, screen_id) {
var data = {
Id: id,
Type: "Screenshare",
Screenshare: {
id: screen_id
}
}
var data = {
Id: id,
Type: "Screenshare",
Screenshare: {
id: screen_id
}
}
return this.send("Screenshare", data);
return this.send("Screenshare", data);
};
};
Api.prototype.sendAlive = function(timestamp) {
Api.prototype.sendAlive = function(timestamp) {
var data = {
Type: "Alive",
Alive: timestamp
}
var data = {
Type: "Alive",
Alive: timestamp
}
return this.send("Alive", data);
};
return this.send("Alive", data);
};
return Api;
return Api;
});

388
static/js/mediastream/connector.js

@ -20,217 +20,221 @@ @@ -20,217 +20,221 @@
*/
define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
var timeout = 5000;
var timeout_max = 20000;
var timeout = 5000;
var timeout_max = 20000;
var Connector = function(version) {
var Connector = function(version) {
this.version = version;
this.e = $({});
this.error = false;
this.connected = false;
this.disabled = false;
this.connecting = null;
this.connecting_timeout = timeout;
this.token = null;
this.queue = [];
this.roomid = null;
this.version = version;
this.e = $({});
this.error = false;
this.connected = false;
this.disabled = false;
this.connecting = null;
this.connecting_timeout = timeout;
this.token = null;
this.queue = [];
this.roomid = null;
var ua = uaparser();
if (ua.os.name && /Spreed Desktop Caller/i.test(ua.ua)) {
this.userAgent = ua.ua.match(/Spreed Desktop Caller\/([\d.]+)/i)[1] + " (" + ua.os.name + ")";
} else if (ua.browser.name) {
this.userAgent = ua.browser.name + " " + ua.browser.major;
} else {
this.userAgent = ua.ua;
}
};
Connector.prototype.connect = function(url) {
//console.log("connect", this.disabled, url);
if (this.disabled) {
return;
}
this.error = false;
this.e.triggerHandler("connecting", [url]);
this.url = url;
if (this.token) {
url += ("?t=" + this.token);
//console.log("Reusing existing token", this.token);
}
var conn = this.conn = new WebSocket(url);
conn.onopen = _.bind(this.onopen, this);
conn.onerror = _.bind(this.onerror, this);
conn.onclose = _.bind(this.onclose, this);
conn.onmessage = _.bind(this.onmessage, this)
this.connecting = window.setTimeout(_.bind(function() {
console.warn("Connection timeout out after", this.connecting_timeout);
if (this.connecting_timeout < timeout_max) {
this.connecting_timeout += timeout;
}
this.e.triggerHandler("error");
this.reconnect();
}, this), this.connecting_timeout);
};
Connector.prototype.reconnect = function() {
if (!this.url) {
return;
}
this.close();
var url = this.url;
this.url = null;
setTimeout(_.bind(function() {
this.connect(url);
}, this), 200);
};
Connector.prototype.close = function() {
this.connected = false;
if (this.conn) {
var conn = this.conn;
this.conn = null;
if (!this.error) {
conn.close();
}
conn.onopen = conn.onerror = conn.onclose = conn.onmessage = null;
}
};
Connector.prototype.forgetAndReconnect = function() {
this.token = null;
if (this.conn && this.connected) {
this.conn.close();
}
var ua = uaparser();
if (ua.os.name && /Spreed Desktop Caller/i.test(ua.ua)) {
this.userAgent = ua.ua.match(/Spreed Desktop Caller\/([\d.]+)/i)[1] + " ("+ua.os.name+ ")";
} else if (ua.browser.name) {
this.userAgent = ua.browser.name + " " + ua.browser.major;
} else {
this.userAgent = ua.ua;
}
};
Connector.prototype.connect = function(url) {
//console.log("connect", this.disabled, url);
if (this.disabled) {
return;
}
this.error = false;
this.e.triggerHandler("connecting", [url]);
this.url = url;
if (this.token) {
url += ("?t="+this.token);
//console.log("Reusing existing token", this.token);
}
var conn = this.conn= new WebSocket(url);
conn.onopen = _.bind(this.onopen, this);
conn.onerror = _.bind(this.onerror, this);
conn.onclose = _.bind(this.onclose, this);
conn.onmessage = _.bind(this.onmessage, this)
this.connecting = window.setTimeout(_.bind(function() {
console.warn("Connection timeout out after", this.connecting_timeout);
if (this.connecting_timeout < timeout_max) {
this.connecting_timeout += timeout;
}
this.e.triggerHandler("error");
this.reconnect();
}, this), this.connecting_timeout);
};
Connector.prototype.reconnect = function() {
if (!this.url) {
return;
}
this.close();
var url = this.url;
this.url = null;
};
Connector.prototype.room = function(roomid, cb) {
var was_connected = this.connected;
if (was_connected) {
if (this.roomid === roomid) {
return;
}
this.e.triggerHandler("closed", [{
soft: true
}]);
}
this.roomid = roomid;
roomid = this.roomid ? this.roomid : "";
if (cb) {
cb();
}
this.send({
Type: "Hello",
Hello: {
Version: this.version,
Ua: this.userAgent,
Id: roomid
}
});
if (was_connected) {
this.e.triggerHandler("open", [{
soft: true
}]);
}
};
Connector.prototype.onopen = function(event) {
window.clearTimeout(this.connecting);
this.connecting_timeout = timeout;
setTimeout(_.bind(function() {
this.connect(url);
}, this), 200);
//console.log("onopen", event);
console.info("Connector on connection open.");
this.room(this.roomid, _.bind(function() {
this.connected = true;
}, this));
this.e.triggerHandler("open", [event]);
};
Connector.prototype.close = function() {
this.connected = false;
if (this.conn) {
var conn = this.conn;
this.conn = null;
if (!this.error) {
conn.close();
}
conn.onopen = conn.onerror = conn.onclose = conn.onmessage = null;
}
// Send out stuff which was previously queued.
var data;
while (this.queue.length > 0 && this.connected) {
data = this.queue.shift();
this.send(data);
}
};
};
Connector.prototype.forgetAndReconnect = function() {
Connector.prototype.onerror = function(event) {
this.token = null;
if (this.conn && this.connected) {
this.conn.close();
}
window.clearTimeout(this.connecting);
this.connecting_timeout = timeout;
};
Connector.prototype.room = function(roomid, cb) {
//console.log("onerror", event);
console.warn("Connector on connection error.");
this.error = true;
this.close();
this.e.triggerHandler("error", [event]);
var was_connected = this.connected;
};
if (was_connected) {
if (this.roomid === roomid) {
return;
}
this.e.triggerHandler("closed", [{soft: true}]);
}
Connector.prototype.onclose = function(event) {
this.roomid = roomid;
roomid = this.roomid ? this.roomid : "";
window.clearTimeout(this.connecting);
this.connecting_timeout = timeout;
if (cb) {
cb();
}
//console.log("onclose", event);
console.info("Connector on connection close.", event);
this.close();
if (!this.error) {
this.e.triggerHandler("close", [event]);
}
this.e.triggerHandler("closed", [event]);
this.send({
Type: "Hello",
Hello: {
Version: this.version,
Ua: this.userAgent,
Id: roomid
}
});
};
if (was_connected) {
this.e.triggerHandler("open", [{soft: true}]);
}
Connector.prototype.onmessage = function(event) {
};
Connector.prototype.onopen = function(event) {
//console.log("onmessage", event);
var msg = JSON.parse(event.data);
this.e.triggerHandler("received", [msg]);
window.clearTimeout(this.connecting);
this.connecting_timeout = timeout;
};
//console.log("onopen", event);
console.info("Connector on connection open.");
this.room(this.roomid, _.bind(function() {
this.connected = true;
}, this));
this.e.triggerHandler("open", [event]);
Connector.prototype.send = function(data, noqueue) {
// Send out stuff which was previously queued.
var data;
while (this.queue.length > 0 && this.connected) {
data = this.queue.shift();
this.send(data);
}
if (!this.connected) {
if (!noqueue) {
this.queue.push(data);
console.warn("Queuing sending data because of not connected.", data);
return;
}
}
this.conn.send(JSON.stringify(data));
};
Connector.prototype.ready = function(func) {
/* Call a function whenever the Connection is ready */
this.e.on("open", func);
if (this.connected) {
func();
}
};
};
Connector.prototype.onerror = function(event) {
window.clearTimeout(this.connecting);
this.connecting_timeout = timeout;
//console.log("onerror", event);
console.warn("Connector on connection error.");
this.error = true;
this.close();
this.e.triggerHandler("error", [event]);
};
Connector.prototype.onclose = function(event) {
window.clearTimeout(this.connecting);
this.connecting_timeout = timeout;
//console.log("onclose", event);
console.info("Connector on connection close.", event);
this.close();
if (!this.error) {
this.e.triggerHandler("close", [event]);
}
this.e.triggerHandler("closed", [event]);
};
Connector.prototype.onmessage = function(event) {
//console.log("onmessage", event);
var msg = JSON.parse(event.data);
this.e.triggerHandler("received", [msg]);
};
Connector.prototype.send = function(data, noqueue) {
if (!this.connected) {
if (!noqueue) {
this.queue.push(data);
console.warn("Queuing sending data because of not connected.", data);
return;
}
}
this.conn.send(JSON.stringify(data));
};
Connector.prototype.ready = function(func) {
/* Call a function whenever the Connection is ready */
this.e.on("open", func);
if (this.connected) {
func();
}
};
return Connector;
return Connector;
});

586
static/js/mediastream/peercall.js

@ -20,297 +20,303 @@ @@ -20,297 +20,303 @@
*/
define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection'], function($, _, utils, PeerConnection) {
var PeerCall = function(webrtc, id, from, to) {
var PeerCall = function(webrtc, id, from, to) {
this.webrtc = webrtc;
this.id = id;
this.from = from;
this.to = to;
this.webrtc = webrtc;
this.id = id;
this.from = from;
this.to = to;
this.e = $({}) // events
this.mediaConstraints = $.extend(true, {}, this.webrtc.settings.mediaConstraints);
this.pcConfig = $.extend(true, {}, this.webrtc.settings.pcConfig);
this.pcConstraints = $.extend(true, {}, this.webrtc.settings.pcConstraints);
this.sdpConstraints = $.extend(true, {}, this.webrtc.settings.sdpConstraints);
this.offerConstraints = $.extend(true, {}, this.webrtc.settings.offerConstraints);
this.peerconnection = null;
this.datachannels = {};
this.streams= {};
this.initiate = false;
this.closed = false;
};
PeerCall.prototype.setInitiate = function(initiate) {
this.initiate = !!initiate;
//console.log("Set initiate", this.initiate, this);
};
PeerCall.prototype.createPeerConnection = function(success_cb, error_cb) {
var peerconnection = this.peerconnection = new PeerConnection(this.webrtc, this);
if (success_cb && peerconnection.pc) {
success_cb(peerconnection);
}
if (error_cb && !peerconnection.pc) {
// TODO(longsleep): Check if this can happen?
error_cb(peerconnection);
}
return peerconnection;
};
PeerCall.prototype.createOffer = function(cb) {
var constraints = utils.mergeConstraints(this.offerConstraints, this.sdpConstraints);
console.log('Creating offer with constraints: \n' +
' \'' + JSON.stringify(constraints, null, '\t') + '\'.')
this.peerconnection.createOffer(_.bind(this.onCreateAnswerOffer, this, cb), _.bind(this.onErrorAnswerOffer, this), constraints);
};
PeerCall.prototype.createAnswer = function(cb) {
console.log("Creating answer.");
this.peerconnection.createAnswer(_.bind(this.onCreateAnswerOffer, this, cb), _.bind(this.onErrorAnswerOffer, this), this.peerconnection.sdpConstraints);
};
PeerCall.prototype.onCreateAnswerOffer = function(cb, sessionDescription) {
// Prefer Opus.
sessionDescription.sdp = utils.preferOpus(sessionDescription.sdp);
// Convert to object to allow custom property injection.
var sessionDescriptionObj = sessionDescription;
if (sessionDescriptionObj.toJSON) {
sessionDescriptionObj = JSON.parse(JSON.stringify(sessionDescriptionObj));
}
console.log("Created offer/answer", JSON.stringify(sessionDescriptionObj, null, "\t"));
// Allow external session description modifications.
this.e.triggerHandler("sessiondescription", [sessionDescriptionObj, this]);
// Always set local description.
this.peerconnection.setLocalDescription(sessionDescription, _.bind(function() {
console.log("Set local session description.", sessionDescription, this);
if (cb) {
cb(sessionDescriptionObj, this);
}
}, this), _.bind(function(err) {
console.error("Set local session description failed", err);
this.close();
this.e.triggerHandler("error", "failed_peerconnection_setup");
}, this));
};
PeerCall.prototype.onErrorAnswerOffer = function(event) {
console.error("Failed to create answer/offer", event);
};
PeerCall.prototype.setRemoteDescription = function(sessionDescription, cb) {
var peerconnection = this.peerconnection;
if (!peerconnection) {
console.log("Got a remote description but not connected -> ignored.");
return;
}
peerconnection.setRemoteDescription(sessionDescription, _.bind(function() {
console.log("Set remote session description.", sessionDescription, this);
if (cb) {
cb(sessionDescription, this);
}
// NOTE(longsleep): There are several szenarios where onaddstream is never fired, when
// the peer does not provide a certain stream type (eg. has no camera). See
// for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546. For this
// reason we always trigger onRemoteStream added for all streams which are available
// after the remote SDP was set successfully.
_.defer(_.bind(function() {
_.each(peerconnection.getRemoteStreams(), _.bind(function(stream) {
if (!this.streams.hasOwnProperty(stream)) {
console.log("Adding stream after remote SDP success.", stream);
this.onRemoteStreamAdded(stream);
}
}, this));
}, this));
}, this), _.bind(function(err) {
console.error("Set remote session description failed", err);
this.close();
this.e.triggerHandler("error", "failed_peerconnection_setup");
}, this));
};
PeerCall.prototype.onIceCandidate = function(event) {
if (event.candidate) {
//console.log("ice candidate", event.candidate);
var payload = {
type: 'candidate',
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
// Allow external payload modifications.
this.e.triggerHandler("icecandidate", [payload, this]);
// Send it.
// TODO(longsleep): This id needs to be different for PeerXfers.
// XXX(longsleep): This seems to be breaking conferences when this.to and not this.id.
this.webrtc.api.sendCandidate(this.to, payload);
//console.log("Sent candidate", event.candidate.sdpMid, event.candidate.sdpMLineIndex, event.candidate.candidate);
} else {
console.log('End of candidates.');
}
};
PeerCall.prototype.onIceConnectionStateChange = function(iceConnectionState) {
this.e.triggerHandler("connectionStateChange", [iceConnectionState, this]);
};
PeerCall.prototype.onRemoteStreamAdded = function(stream) {
this.streams[stream] = true;
this.e.triggerHandler("remoteStreamAdded", [stream, this]);
};
PeerCall.prototype.onRemoteStreamRemoved = function(stream) {
this.e.triggerHandler("remoteStreamRemoved", [stream, this]);
if (stream) {
delete this.streams[stream];
}
};
PeerCall.prototype.onNegotiationNeeded = function(peerconnection) {
var remoteDescription = peerconnection.pc.remoteDescription;
console.log("Negotiation needed.", remoteDescription);
if (remoteDescription && remoteDescription.type === "offer") {
// Need to answer.
// XXX(longsleep): In cases where we are in answer state but need to
// negotiate again, createAnswer will do nothing. We might need to call
// createOffer and send it out as answer? Is that valid / makes sense?
this.createAnswer(_.bind(function(sessionDescription) {
console.log("Sending new negotiation answer with sessionDescription", sessionDescription, this.id);
this.webrtc.api.sendAnswer(this.to, sessionDescription);
}, this));
} else {
// Send offer.
this.createOffer(_.bind(function(sessionDescription) {
console.log("Sending new negotiation offer with sessionDescription", sessionDescription, this.id);
this.webrtc.api.sendOffer(this.to, sessionDescription);
}, this));
}
};
PeerCall.prototype.addIceCandidate = function(candidate) {
if (this.closed) {
// Avoid errors when still receiving candidates but closed.
return;
}
this.peerconnection.addIceCandidate(candidate);
};
PeerCall.prototype.onDatachannel = function(datachannel) {
//console.log("onDatachannel", datachannel);
var label = datachannel.label;
if (this.datachannels.hasOwnProperty(label)) {
console.warn("Received duplicated datachannel label", label, datachannel, this.datachannels);
return;
}
// Remember it for correct cleanups.
this.datachannels[label] = datachannel;
this.e.triggerHandler("datachannel", ["new", datachannel, this]);
};
PeerCall.prototype.onDatachannelDefault = function(state, datachannel) {
if (state === "open") {
//console.log("Data ready", this);
_.defer(_.bind(function() {
this.e.triggerHandler("dataReady", [this]);
}, this));
}
this.e.triggerHandler("datachannel.default", [state, datachannel, this]);
};
PeerCall.prototype.onMessage = function(event) {
//console.log("Peer to peer channel message", event);
var data = event.data;
if (data instanceof Blob) {
console.warn("Blob data received - not implemented.", data);
} else if (data instanceof ArrayBuffer) {
console.warn("ArrayBuffer data received - not implemented.", data);
} else if (typeof data === "string") {
if (data.charCodeAt(0) === 2) {
// Ignore whatever shit is this (sent by Chrome 34 and FireFox). Feel free to
// change the comment it you know what this is.
return;
}
//console.log("Datachannel message", [event.data, event.data.length, event.data.charCodeAt(0)]);
var msg = JSON.parse(event.data);
this.webrtc.api.received({Type: msg.Type, Data: msg, To: this.webrtc.api.id, From: this.id, p2p: true}); //XXX(longsleep): use event for this?
} else {
console.warn("Unknow data type received -> igored", typeof data, [data]);
}
};
PeerCall.prototype.getDatachannel = function(label, init, create_cb) {
//console.log("getDatachannel", label);
var datachannel = this.datachannels[label];
if (!datachannel) {
console.log("Creating new datachannel", label, init);
datachannel = this.peerconnection.createDatachannel(label, init);
if (create_cb) {
create_cb(datachannel);
}
}
return datachannel;
};
PeerCall.prototype.close = function() {
if (this.closed) {
return;
}
this.closed = true;
_.each(this.datachannels, function(datachannel) {
datachannel.close();
});
this.datachannels = {};
this.streams = {};
if (this.peerconnection) {
this.peerconnection.close();
this.peerconnection = null;
}
console.log("Peercall close", this);
this.e.triggerHandler("closed", [this]);
};
return PeerCall;
this.e = $({}) // events
this.mediaConstraints = $.extend(true, {}, this.webrtc.settings.mediaConstraints);
this.pcConfig = $.extend(true, {}, this.webrtc.settings.pcConfig);
this.pcConstraints = $.extend(true, {}, this.webrtc.settings.pcConstraints);
this.sdpConstraints = $.extend(true, {}, this.webrtc.settings.sdpConstraints);
this.offerConstraints = $.extend(true, {}, this.webrtc.settings.offerConstraints);
this.peerconnection = null;
this.datachannels = {};
this.streams = {};
this.initiate = false;
this.closed = false;
};
PeerCall.prototype.setInitiate = function(initiate) {
this.initiate = !! initiate;
//console.log("Set initiate", this.initiate, this);
};
PeerCall.prototype.createPeerConnection = function(success_cb, error_cb) {
var peerconnection = this.peerconnection = new PeerConnection(this.webrtc, this);
if (success_cb && peerconnection.pc) {
success_cb(peerconnection);
}
if (error_cb && !peerconnection.pc) {
// TODO(longsleep): Check if this can happen?
error_cb(peerconnection);
}
return peerconnection;
};
PeerCall.prototype.createOffer = function(cb) {
var constraints = utils.mergeConstraints(this.offerConstraints, this.sdpConstraints);
console.log('Creating offer with constraints: \n' +
' \'' + JSON.stringify(constraints, null, '\t') + '\'.')
this.peerconnection.createOffer(_.bind(this.onCreateAnswerOffer, this, cb), _.bind(this.onErrorAnswerOffer, this), constraints);
};
PeerCall.prototype.createAnswer = function(cb) {
console.log("Creating answer.");
this.peerconnection.createAnswer(_.bind(this.onCreateAnswerOffer, this, cb), _.bind(this.onErrorAnswerOffer, this), this.peerconnection.sdpConstraints);
};
PeerCall.prototype.onCreateAnswerOffer = function(cb, sessionDescription) {
// Prefer Opus.
sessionDescription.sdp = utils.preferOpus(sessionDescription.sdp);
// Convert to object to allow custom property injection.
var sessionDescriptionObj = sessionDescription;
if (sessionDescriptionObj.toJSON) {
sessionDescriptionObj = JSON.parse(JSON.stringify(sessionDescriptionObj));
}
console.log("Created offer/answer", JSON.stringify(sessionDescriptionObj, null, "\t"));
// Allow external session description modifications.
this.e.triggerHandler("sessiondescription", [sessionDescriptionObj, this]);
// Always set local description.
this.peerconnection.setLocalDescription(sessionDescription, _.bind(function() {
console.log("Set local session description.", sessionDescription, this);
if (cb) {
cb(sessionDescriptionObj, this);
}
}, this), _.bind(function(err) {
console.error("Set local session description failed", err);
this.close();
this.e.triggerHandler("error", "failed_peerconnection_setup");
}, this));
};
PeerCall.prototype.onErrorAnswerOffer = function(event) {
console.error("Failed to create answer/offer", event);
};
PeerCall.prototype.setRemoteDescription = function(sessionDescription, cb) {
var peerconnection = this.peerconnection;
if (!peerconnection) {
console.log("Got a remote description but not connected -> ignored.");
return;
}
peerconnection.setRemoteDescription(sessionDescription, _.bind(function() {
console.log("Set remote session description.", sessionDescription, this);
if (cb) {
cb(sessionDescription, this);
}
// NOTE(longsleep): There are several szenarios where onaddstream is never fired, when
// the peer does not provide a certain stream type (eg. has no camera). See
// for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546. For this
// reason we always trigger onRemoteStream added for all streams which are available
// after the remote SDP was set successfully.
_.defer(_.bind(function() {
_.each(peerconnection.getRemoteStreams(), _.bind(function(stream) {
if (!this.streams.hasOwnProperty(stream)) {
console.log("Adding stream after remote SDP success.", stream);
this.onRemoteStreamAdded(stream);
}
}, this));
}, this));
}, this), _.bind(function(err) {
console.error("Set remote session description failed", err);
this.close();
this.e.triggerHandler("error", "failed_peerconnection_setup");
}, this));
};
PeerCall.prototype.onIceCandidate = function(event) {
if (event.candidate) {
//console.log("ice candidate", event.candidate);
var payload = {
type: 'candidate',
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
// Allow external payload modifications.
this.e.triggerHandler("icecandidate", [payload, this]);
// Send it.
// TODO(longsleep): This id needs to be different for PeerXfers.
// XXX(longsleep): This seems to be breaking conferences when this.to and not this.id.
this.webrtc.api.sendCandidate(this.to, payload);
//console.log("Sent candidate", event.candidate.sdpMid, event.candidate.sdpMLineIndex, event.candidate.candidate);
} else {
console.log('End of candidates.');
}
};
PeerCall.prototype.onIceConnectionStateChange = function(iceConnectionState) {
this.e.triggerHandler("connectionStateChange", [iceConnectionState, this]);
};
PeerCall.prototype.onRemoteStreamAdded = function(stream) {
this.streams[stream] = true;
this.e.triggerHandler("remoteStreamAdded", [stream, this]);
};
PeerCall.prototype.onRemoteStreamRemoved = function(stream) {
this.e.triggerHandler("remoteStreamRemoved", [stream, this]);
if (stream) {
delete this.streams[stream];
}
};
PeerCall.prototype.onNegotiationNeeded = function(peerconnection) {
var remoteDescription = peerconnection.pc.remoteDescription;
console.log("Negotiation needed.", remoteDescription);
if (remoteDescription && remoteDescription.type === "offer") {
// Need to answer.
// XXX(longsleep): In cases where we are in answer state but need to
// negotiate again, createAnswer will do nothing. We might need to call
// createOffer and send it out as answer? Is that valid / makes sense?
this.createAnswer(_.bind(function(sessionDescription) {
console.log("Sending new negotiation answer with sessionDescription", sessionDescription, this.id);
this.webrtc.api.sendAnswer(this.to, sessionDescription);
}, this));
} else {
// Send offer.
this.createOffer(_.bind(function(sessionDescription) {
console.log("Sending new negotiation offer with sessionDescription", sessionDescription, this.id);
this.webrtc.api.sendOffer(this.to, sessionDescription);
}, this));
}
};
PeerCall.prototype.addIceCandidate = function(candidate) {
if (this.closed) {
// Avoid errors when still receiving candidates but closed.
return;
}
this.peerconnection.addIceCandidate(candidate);
};
PeerCall.prototype.onDatachannel = function(datachannel) {
//console.log("onDatachannel", datachannel);
var label = datachannel.label;
if (this.datachannels.hasOwnProperty(label)) {
console.warn("Received duplicated datachannel label", label, datachannel, this.datachannels);
return;
}
// Remember it for correct cleanups.
this.datachannels[label] = datachannel;
this.e.triggerHandler("datachannel", ["new", datachannel, this]);
};
PeerCall.prototype.onDatachannelDefault = function(state, datachannel) {
if (state === "open") {
//console.log("Data ready", this);
_.defer(_.bind(function() {
this.e.triggerHandler("dataReady", [this]);
}, this));
}
this.e.triggerHandler("datachannel.default", [state, datachannel, this]);
};
PeerCall.prototype.onMessage = function(event) {
//console.log("Peer to peer channel message", event);
var data = event.data;
if (data instanceof Blob) {
console.warn("Blob data received - not implemented.", data);
} else if (data instanceof ArrayBuffer) {
console.warn("ArrayBuffer data received - not implemented.", data);
} else if (typeof data === "string") {
if (data.charCodeAt(0) === 2) {
// Ignore whatever shit is this (sent by Chrome 34 and FireFox). Feel free to
// change the comment it you know what this is.
return;
}
//console.log("Datachannel message", [event.data, event.data.length, event.data.charCodeAt(0)]);
var msg = JSON.parse(event.data);
this.webrtc.api.received({
Type: msg.Type,
Data: msg,
To: this.webrtc.api.id,
From: this.id,
p2p: true
}); //XXX(longsleep): use event for this?
} else {
console.warn("Unknow data type received -> igored", typeof data, [data]);
}
};
PeerCall.prototype.getDatachannel = function(label, init, create_cb) {
//console.log("getDatachannel", label);
var datachannel = this.datachannels[label];
if (!datachannel) {
console.log("Creating new datachannel", label, init);
datachannel = this.peerconnection.createDatachannel(label, init);
if (create_cb) {
create_cb(datachannel);
}
}
return datachannel;
};
PeerCall.prototype.close = function() {
if (this.closed) {
return;
}
this.closed = true;
_.each(this.datachannels, function(datachannel) {
datachannel.close();
});
this.datachannels = {};
this.streams = {};
if (this.peerconnection) {
this.peerconnection.close();
this.peerconnection = null;
}
console.log("Peercall close", this);
this.e.triggerHandler("closed", [this]);
};
return PeerCall;
});

426
static/js/mediastream/peerconference.js

@ -20,218 +20,218 @@ @@ -20,218 +20,218 @@
*/
define(['underscore', 'mediastream/peercall'], function(_, PeerCall) {
//NOTE(longsleep): This id should be changed to something undeterministic.
var conferences = 0;
var PeerConference = function(webrtc, currentcall, id) {
this.webrtc = webrtc;
this.currentcall = currentcall;
this.calls = {};
this.callsIn = {};
this.e = $({});
if (!id) {
this.id = webrtc.api.id + "_" + (++conferences);
} else {
this.id = id;
}
console.log("Created conference", this.id);
};
PeerConference.prototype.createCall = function(id, from, to) {
var currentcall = new PeerCall(this.webrtc, id, from, to);
currentcall.e.on("closed", _.bind(function() {
delete this.calls[id];
if (this.callsIn.hasOwnProperty(id)) {
delete this.callsIn[id];
}
console.log("Cleaned up conference call", id);
if (_.isEmpty(this.calls)) {
console.log("Conference is now empty -> cleaning up.");
this.e.triggerHandler("finished");
}
}, this));
currentcall.e.on("connectionStateChange", _.bind(function(event, iceConnectionState, currentcall) {
this.onConnectionStateChange(iceConnectionState, currentcall);
}, this));
currentcall.e.on("remoteStreamAdded", _.bind(function(event, stream, currentcall) {
this.webrtc.onRemoteStreamAdded(stream, currentcall);
}, this));
currentcall.e.on("remoteStreamRemoved", _.bind(function(event, stream, currentcall) {
this.webrtc.onRemoteStreamRemoved(stream, currentcall);
}, this));
return currentcall;
};
PeerConference.prototype.doCall = function(id, autocall) {
if (id === this.currentcall.id || this.calls.hasOwnProperty(id)) {
// Ignore calls which we already have.
//console.debug("Already got a call to this id (doCall)", id, this.calls, this.currentcall);
return;
}
var call = this.calls[id] = this.createCall(id, null, id);
call.setInitiate(true);
call.e.on("sessiondescription", _.bind(function(event, sessionDescription) {
console.log("Injected conference id into sessionDescription", this.id);
sessionDescription._conference = this.id;
}, this));
if (!autocall) {
this.webrtc.e.triggerHandler("connecting", [call]);
}
console.log("Creating PeerConnection", call);
call.createPeerConnection(_.bind(function(peerconnection) {
// Success call.
if (this.webrtc.usermedia) {
this.webrtc.usermedia.addToPeerConnection(peerconnection);
}
call.createOffer(_.bind(function(sessionDescription, extracall) {
console.log("Sending offer with sessionDescription", sessionDescription, extracall.id);
this.webrtc.api.sendOffer(extracall.id, sessionDescription);
}, this));
}, this), _.bind(function() {
// Error call.
console.error("Failed to create peer connection for conference call.");
}, this));
};
PeerConference.prototype.handOver = function() {
// Use a new call as currentcall and return this one.
var calls = _.keys(this.callsIn);
if (calls.length) {
var id = calls[0];
var currentcall = this.currentcall = this.calls[id];
delete this.calls[id];
delete this.callsIn[id];
console.log("Handed over conference to", id, currentcall);
if (_.isEmpty(this.calls)) {
console.log("Conference is now empty -> cleaning up.");
this.e.triggerHandler("finished");
}
return currentcall;
}
return null;
};
PeerConference.prototype.autoAnswer = function(from, rtcsdp) {
if (from === this.currentcall.id || this.calls.hasOwnProperty(from)) {
console.warn("Already got a call to this id (autoAnswer)", from, this.calls);
return;
}
var call = this.calls[from] = this.createCall(from, this.webrtc.api.id, from);
console.log("Creating PeerConnection", call);
call.createPeerConnection(_.bind(function(peerconnection) {
// Success call.
call.setRemoteDescription(rtcsdp, _.bind(function() {
if (this.webrtc.usermedia) {
this.webrtc.usermedia.addToPeerConnection(peerconnection);
}
call.createAnswer(_.bind(function(sessionDescription, extracall) {
console.log("Sending answer", sessionDescription, extracall.id);
this.webrtc.api.sendAnswer(extracall.id, sessionDescription);
}, this));
}, this));
}, this), _.bind(function() {
// Error call.
console.error("Failed to create peer connection for auto answer.");
}, this));
};
PeerConference.prototype.getCall = function(id) {
var call = this.calls[id];
if (!call) {
call = null;
}
return call;
};
PeerConference.prototype.close = function() {
this.currentcall = null;
var api = this.webrtc.api;
_.each(this.calls, function(c) {
c.close();
var id = c.id;
if (id) {
api.sendBye(id);
}
});
this.calls = {};
};
PeerConference.prototype.onConnectionStateChange = function(iceConnectionState, currentcall) {
console.log("Conference peer connection state changed", iceConnectionState, currentcall);
switch (iceConnectionState) {
case "completed":
case "connected":
if (!this.callsIn.hasOwnProperty(currentcall.id)) {
this.callsIn[currentcall.id] = true;
this.pushUpdate();
}
break;
case "failed":
console.warn("Conference peer connection state failed", currentcall);
break;
}
this.webrtc.onConnectionStateChange(iceConnectionState, currentcall);
};
PeerConference.prototype.pushUpdate = function() {
var calls = _.keys(this.callsIn);
if (calls) {
if (this.currentcall) {
calls.push(this.currentcall.id);
calls.push(this.webrtc.api.id);
}
}
console.log("Calls in conference: ", calls);
this.webrtc.api.sendConference(this.id, calls);
};
PeerConference.prototype.applyUpdate = function(ids) {
console.log("Applying conference update", this.id, ids);
var myid = this.webrtc.api.id;
_.each(ids, _.bind(function(id) {
var res = myid < id ? -1 : myid > id ? 1 : 0;
console.log("Considering conference peers to call", res, id);
if (res === -1) {
this.doCall(id, true);
}
}, this));
};
PeerConference.prototype.peerIds = function() {
return _.keys(this.calls);
};
return PeerConference;
//NOTE(longsleep): This id should be changed to something undeterministic.
var conferences = 0;
var PeerConference = function(webrtc, currentcall, id) {
this.webrtc = webrtc;
this.currentcall = currentcall;
this.calls = {};
this.callsIn = {};
this.e = $({});
if (!id) {
this.id = webrtc.api.id + "_" + (++conferences);
} else {
this.id = id;
}
console.log("Created conference", this.id);
};
PeerConference.prototype.createCall = function(id, from, to) {
var currentcall = new PeerCall(this.webrtc, id, from, to);
currentcall.e.on("closed", _.bind(function() {
delete this.calls[id];
if (this.callsIn.hasOwnProperty(id)) {
delete this.callsIn[id];
}
console.log("Cleaned up conference call", id);
if (_.isEmpty(this.calls)) {
console.log("Conference is now empty -> cleaning up.");
this.e.triggerHandler("finished");
}
}, this));
currentcall.e.on("connectionStateChange", _.bind(function(event, iceConnectionState, currentcall) {
this.onConnectionStateChange(iceConnectionState, currentcall);
}, this));
currentcall.e.on("remoteStreamAdded", _.bind(function(event, stream, currentcall) {
this.webrtc.onRemoteStreamAdded(stream, currentcall);
}, this));
currentcall.e.on("remoteStreamRemoved", _.bind(function(event, stream, currentcall) {
this.webrtc.onRemoteStreamRemoved(stream, currentcall);
}, this));
return currentcall;
};
PeerConference.prototype.doCall = function(id, autocall) {
if (id === this.currentcall.id || this.calls.hasOwnProperty(id)) {
// Ignore calls which we already have.
//console.debug("Already got a call to this id (doCall)", id, this.calls, this.currentcall);
return;
}
var call = this.calls[id] = this.createCall(id, null, id);
call.setInitiate(true);
call.e.on("sessiondescription", _.bind(function(event, sessionDescription) {
console.log("Injected conference id into sessionDescription", this.id);
sessionDescription._conference = this.id;
}, this));
if (!autocall) {
this.webrtc.e.triggerHandler("connecting", [call]);
}
console.log("Creating PeerConnection", call);
call.createPeerConnection(_.bind(function(peerconnection) {
// Success call.
if (this.webrtc.usermedia) {
this.webrtc.usermedia.addToPeerConnection(peerconnection);
}
call.createOffer(_.bind(function(sessionDescription, extracall) {
console.log("Sending offer with sessionDescription", sessionDescription, extracall.id);
this.webrtc.api.sendOffer(extracall.id, sessionDescription);
}, this));
}, this), _.bind(function() {
// Error call.
console.error("Failed to create peer connection for conference call.");
}, this));
};
PeerConference.prototype.handOver = function() {
// Use a new call as currentcall and return this one.
var calls = _.keys(this.callsIn);
if (calls.length) {
var id = calls[0];
var currentcall = this.currentcall = this.calls[id];
delete this.calls[id];
delete this.callsIn[id];
console.log("Handed over conference to", id, currentcall);
if (_.isEmpty(this.calls)) {
console.log("Conference is now empty -> cleaning up.");
this.e.triggerHandler("finished");
}
return currentcall;
}
return null;
};
PeerConference.prototype.autoAnswer = function(from, rtcsdp) {
if (from === this.currentcall.id || this.calls.hasOwnProperty(from)) {
console.warn("Already got a call to this id (autoAnswer)", from, this.calls);
return;
}
var call = this.calls[from] = this.createCall(from, this.webrtc.api.id, from);
console.log("Creating PeerConnection", call);
call.createPeerConnection(_.bind(function(peerconnection) {
// Success call.
call.setRemoteDescription(rtcsdp, _.bind(function() {
if (this.webrtc.usermedia) {
this.webrtc.usermedia.addToPeerConnection(peerconnection);
}
call.createAnswer(_.bind(function(sessionDescription, extracall) {
console.log("Sending answer", sessionDescription, extracall.id);
this.webrtc.api.sendAnswer(extracall.id, sessionDescription);
}, this));
}, this));
}, this), _.bind(function() {
// Error call.
console.error("Failed to create peer connection for auto answer.");
}, this));
};
PeerConference.prototype.getCall = function(id) {
var call = this.calls[id];
if (!call) {
call = null;
}
return call;
};
PeerConference.prototype.close = function() {
this.currentcall = null;
var api = this.webrtc.api;
_.each(this.calls, function(c) {
c.close();
var id = c.id;
if (id) {
api.sendBye(id);
}
});
this.calls = {};
};
PeerConference.prototype.onConnectionStateChange = function(iceConnectionState, currentcall) {
console.log("Conference peer connection state changed", iceConnectionState, currentcall);
switch (iceConnectionState) {
case "completed":
case "connected":
if (!this.callsIn.hasOwnProperty(currentcall.id)) {
this.callsIn[currentcall.id] = true;
this.pushUpdate();
}
break;
case "failed":
console.warn("Conference peer connection state failed", currentcall);
break;
}
this.webrtc.onConnectionStateChange(iceConnectionState, currentcall);
};
PeerConference.prototype.pushUpdate = function() {
var calls = _.keys(this.callsIn);
if (calls) {
if (this.currentcall) {
calls.push(this.currentcall.id);
calls.push(this.webrtc.api.id);
}
}
console.log("Calls in conference: ", calls);
this.webrtc.api.sendConference(this.id, calls);
};
PeerConference.prototype.applyUpdate = function(ids) {
console.log("Applying conference update", this.id, ids);
var myid = this.webrtc.api.id;
_.each(ids, _.bind(function(id) {
var res = myid < id ? -1 : myid > id ? 1 : 0;
console.log("Considering conference peers to call", res, id);
if (res === -1) {
this.doCall(id, true);
}
}, this));
};
PeerConference.prototype.peerIds = function() {
return _.keys(this.calls);
};
return PeerConference;
});

474
static/js/mediastream/peerconnection.js

@ -20,285 +20,291 @@ @@ -20,285 +20,291 @@
*/
define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
var count = 0;
var dataChannelDefaultLabel = "default";
var PeerConnection = function(webrtc, currentcall) {
this.webrtc = webrtc;
this.id = count++;
this.currentcall = null;
this.pc = null;
this.datachannel = null;
this.datachannelReady = false;
if (currentcall) {
this.createPeerConnection(currentcall);
}
}
PeerConnection.prototype.createPeerConnection = function(currentcall) {
// XXX(longsleep): This function is a mess.
var pc;
if (currentcall) {
this.currentcall = currentcall;
} else {
currentcall = this.currentcall;
}
try {
// Create an RTCPeerConnection via the polyfill (adapter.js)
console.log('Creating RTCPeerConnnection with:\n' +
' config: \'' + JSON.stringify(currentcall.pcConfig) + '\';\n' +
' constraints: \'' + JSON.stringify(currentcall.pcConstraints) + '\'.');
pc = this.pc = new RTCPeerConnection(currentcall.pcConfig, currentcall.pcConstraints);
} catch(e) {
console.error('Failed to create PeerConnection, exception: ' + e.message);
pc = this.pc = null;
}
if (pc) {
// Bind peer connection events.
pc.onicecandidate = _.bind(currentcall.onIceCandidate, currentcall);
pc.oniceconnectionstatechange = _.bind(this.onIceConnectionStateChange, this)
// NOTE(longsleep): There are several szenarios where onaddstream is never fired, when
// the peer does not provide a certain stream type (eg. has no camera). See
// for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546.
pc.onaddstream = _.bind(this.onRemoteStreamAdded, this);
pc.onremovestream = _.bind(this.onRemoteStreamRemoved, this);
pc.onnegotiationneeded = _.bind(this.onNegotiationNeeded, this);
pc.ondatachannel = _.bind(this.onDatachannel, this);
pc.onsignalingstatechange = function(event) {
// XXX(longsleep): Remove this or handle it in a real function.
// XXX(longsleep): Firefox 25 does send event as a string (like stable).
console.debug("Signaling state changed", pc.signalingState);
};
// NOTE(longsleep):
// Support old callback too (https://groups.google.com/forum/?fromgroups=#!topic/discuss-webrtc/glukq0OWwVM)
// Chrome < 27 and Firefox < 24 need this.
pc.onicechange = _.bind(function(iceConnectionState) {
//XXX(longsleep): Hack the compatibility to new style event.
console.warn("Old style onicechange event", arguments);
this.onIceConnectionStateChange({target: {iceConnectionState: iceConnectionState}});
}, this);
// Create default data channel when we are in initiate mode.
if (currentcall.initiate) {
if (webrtcDetectedBrowser !== "chrome" || !webrtcDetectedAndroid || (webrtcDetectedBrowser === "chrome" && webrtcDetectedVersion >= 33)) {
// NOTE(longsleep): Android (Chrome 32) does have broken SCTP data channels
// which makes connection fail because of sdp set error for answer/offer.
// See https://code.google.com/p/webrtc/issues/detail?id=2253 Lets hope the
// crap gets fixed with Chrome on Android 33. For now disable SCTP in flags
// on Adroid to be able to accept offers with SCTP in it.
// chrome://flags/#disable-sctp-data-channels
this.createDatachannel(dataChannelDefaultLabel, {
ordered: true
});
}
}
}
return pc;
};
PeerConnection.prototype.createDatachannel = function(label, init) {
if (!label) {
console.error("Refusing to create a datachannel without a label.", label, init);
return;
}
var rtcinit = $.extend({}, init);
console.debug("Creating datachannel:", label, rtcinit, this);
// Create datachannel.
var datachannel;
try {
datachannel = this.pc.createDataChannel(label, rtcinit);
// Fake onDatachannel event.
this.onDatachannel({channel: datachannel});
} catch (e) {
console.error('Failed to create DataChannel, exception: ' + e.message);
if (label === dataChannelDefaultLabel) {
this.datachannel = null;
this.datachannelReady = false;
}
}
return datachannel;
};
PeerConnection.prototype.onDatachannel = function(event) {
var datachannel = event.channel;
if (datachannel) {
if (datachannel.label === dataChannelDefaultLabel) {
datachannel.binaryType = "arraybuffer";
// We handle the default data channel ourselves.
console.debug("Got default datachannel", datachannel.label, this.id, datachannel, this);
this.datachannel = datachannel;
var eventHandler = _.bind(this.currentcall.onDatachannelDefault, this.currentcall);
// Bind datachannel events and settings.
datachannel.onmessage = _.bind(this.currentcall.onMessage, this.currentcall);
datachannel.onopen = _.bind(function(event) {
console.log("Datachannel opened", datachannel.label, this.id, event);
this.datachannelReady = true;
eventHandler("open", datachannel);
}, this);
datachannel.onclose = _.bind(function(event) {
console.log("Datachannel closed", datachannel.label, this.id, event);
this.datachannelReady = false;
eventHandler("close", datachannel);
}, this);
datachannel.onerror = _.bind(function(event) {
console.warn("Datachannel error", datachannel.label, this.id ,event);
this.datachannelReady = false;
eventHandler("error", datachannel);
}, this);
} else {
// Delegate.
console.debug("Got datachannel", datachannel.label, this.id, datachannel);
_.defer(_.bind(this.currentcall.onDatachannel, this.currentcall), datachannel);
}
}
};
PeerConnection.prototype.send = function(data) {
if (!this.datachannelReady) {
console.error("Unable to send message by datachannel because datachannel is not ready.", data);
return;
}
if (data instanceof Blob) {
this.datachannel.send(data);
} else if (data instanceof ArrayBuffer) {
this.datachannel.send(data);
} else {
try {
this.datachannel.send(JSON.stringify(data));
} catch(e) {
console.warn("Data channel failed to send string -> closing.", e);
this.datachannelReady = false;
this.datachannel.close();
}
}
var count = 0;
var dataChannelDefaultLabel = "default";
var PeerConnection = function(webrtc, currentcall) {
this.webrtc = webrtc;
this.id = count++;
this.currentcall = null;
this.pc = null;
this.datachannel = null;
this.datachannelReady = false;
if (currentcall) {
this.createPeerConnection(currentcall);
}
}
PeerConnection.prototype.createPeerConnection = function(currentcall) {
// XXX(longsleep): This function is a mess.
var pc;
if (currentcall) {
this.currentcall = currentcall;
} else {
currentcall = this.currentcall;
}
try {
// Create an RTCPeerConnection via the polyfill (adapter.js)
console.log('Creating RTCPeerConnnection with:\n' +
' config: \'' + JSON.stringify(currentcall.pcConfig) + '\';\n' +
' constraints: \'' + JSON.stringify(currentcall.pcConstraints) + '\'.');
pc = this.pc = new RTCPeerConnection(currentcall.pcConfig, currentcall.pcConstraints);
} catch (e) {
console.error('Failed to create PeerConnection, exception: ' + e.message);
pc = this.pc = null;
}
if (pc) {
// Bind peer connection events.
pc.onicecandidate = _.bind(currentcall.onIceCandidate, currentcall);
pc.oniceconnectionstatechange = _.bind(this.onIceConnectionStateChange, this)
// NOTE(longsleep): There are several szenarios where onaddstream is never fired, when
// the peer does not provide a certain stream type (eg. has no camera). See
// for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546.
pc.onaddstream = _.bind(this.onRemoteStreamAdded, this);
pc.onremovestream = _.bind(this.onRemoteStreamRemoved, this);
pc.onnegotiationneeded = _.bind(this.onNegotiationNeeded, this);
pc.ondatachannel = _.bind(this.onDatachannel, this);
pc.onsignalingstatechange = function(event) {
// XXX(longsleep): Remove this or handle it in a real function.
// XXX(longsleep): Firefox 25 does send event as a string (like stable).
console.debug("Signaling state changed", pc.signalingState);
};
// NOTE(longsleep):
// Support old callback too (https://groups.google.com/forum/?fromgroups=#!topic/discuss-webrtc/glukq0OWwVM)
// Chrome < 27 and Firefox < 24 need this.
pc.onicechange = _.bind(function(iceConnectionState) {
//XXX(longsleep): Hack the compatibility to new style event.
console.warn("Old style onicechange event", arguments);
this.onIceConnectionStateChange({
target: {
iceConnectionState: iceConnectionState
}
});
}, this);
// Create default data channel when we are in initiate mode.
if (currentcall.initiate) {
if (webrtcDetectedBrowser !== "chrome" || !webrtcDetectedAndroid || (webrtcDetectedBrowser === "chrome" && webrtcDetectedVersion >= 33)) {
// NOTE(longsleep): Android (Chrome 32) does have broken SCTP data channels
// which makes connection fail because of sdp set error for answer/offer.
// See https://code.google.com/p/webrtc/issues/detail?id=2253 Lets hope the
// crap gets fixed with Chrome on Android 33. For now disable SCTP in flags
// on Adroid to be able to accept offers with SCTP in it.
// chrome://flags/#disable-sctp-data-channels
this.createDatachannel(dataChannelDefaultLabel, {
ordered: true
});
}
}
}
return pc;
};
PeerConnection.prototype.createDatachannel = function(label, init) {
if (!label) {
console.error("Refusing to create a datachannel without a label.", label, init);
return;
}
var rtcinit = $.extend({}, init);
console.debug("Creating datachannel:", label, rtcinit, this);
// Create datachannel.
var datachannel;
try {
datachannel = this.pc.createDataChannel(label, rtcinit);
// Fake onDatachannel event.
this.onDatachannel({
channel: datachannel
});
} catch (e) {
console.error('Failed to create DataChannel, exception: ' + e.message);
if (label === dataChannelDefaultLabel) {
this.datachannel = null;
this.datachannelReady = false;
}
}
return datachannel;
};
PeerConnection.prototype.onDatachannel = function(event) {
var datachannel = event.channel;
if (datachannel) {
if (datachannel.label === dataChannelDefaultLabel) {
datachannel.binaryType = "arraybuffer";
// We handle the default data channel ourselves.
console.debug("Got default datachannel", datachannel.label, this.id, datachannel, this);
this.datachannel = datachannel;
var eventHandler = _.bind(this.currentcall.onDatachannelDefault, this.currentcall);
// Bind datachannel events and settings.
datachannel.onmessage = _.bind(this.currentcall.onMessage, this.currentcall);
datachannel.onopen = _.bind(function(event) {
console.log("Datachannel opened", datachannel.label, this.id, event);
this.datachannelReady = true;
eventHandler("open", datachannel);
}, this);
datachannel.onclose = _.bind(function(event) {
console.log("Datachannel closed", datachannel.label, this.id, event);
this.datachannelReady = false;
eventHandler("close", datachannel);
}, this);
datachannel.onerror = _.bind(function(event) {
console.warn("Datachannel error", datachannel.label, this.id, event);
this.datachannelReady = false;
eventHandler("error", datachannel);
}, this);
} else {
// Delegate.
console.debug("Got datachannel", datachannel.label, this.id, datachannel);
_.defer(_.bind(this.currentcall.onDatachannel, this.currentcall), datachannel);
}
}
};
PeerConnection.prototype.send = function(data) {
if (!this.datachannelReady) {
console.error("Unable to send message by datachannel because datachannel is not ready.", data);
return;
}
if (data instanceof Blob) {
this.datachannel.send(data);
} else if (data instanceof ArrayBuffer) {
this.datachannel.send(data);
} else {
try {
this.datachannel.send(JSON.stringify(data));
} catch (e) {
console.warn("Data channel failed to send string -> closing.", e);
this.datachannelReady = false;
this.datachannel.close();
}
}
};
};
PeerConnection.prototype.onIceConnectionStateChange = function(event) {
PeerConnection.prototype.onIceConnectionStateChange = function(event) {
var iceConnectionState = event.target.iceConnectionState;
console.info("ICE connection state change", iceConnectionState, event, this.currentcall);
this.currentcall.onIceConnectionStateChange(iceConnectionState);
var iceConnectionState = event.target.iceConnectionState;
console.info("ICE connection state change", iceConnectionState, event, this.currentcall);
this.currentcall.onIceConnectionStateChange(iceConnectionState);
};
};
PeerConnection.prototype.onRemoteStreamAdded = function(event) {
PeerConnection.prototype.onRemoteStreamAdded = function(event) {
var stream = event.stream;
console.info('Remote stream added.', stream);
this.currentcall.onRemoteStreamAdded(stream);
var stream = event.stream;
console.info('Remote stream added.', stream);
this.currentcall.onRemoteStreamAdded(stream);
};
};
PeerConnection.prototype.onRemoteStreamRemoved = function(event) {
PeerConnection.prototype.onRemoteStreamRemoved = function(event) {
var stream = event.stream;
console.info('Remote stream removed.', stream);
this.currentcall.onRemoteStreamRemoved(stream);
var stream = event.stream;
console.info('Remote stream removed.', stream);
this.currentcall.onRemoteStreamRemoved(stream);
};
};
PeerConnection.prototype.onNegotiationNeeded = function(event) {
PeerConnection.prototype.onNegotiationNeeded = function(event) {
// XXX(longsleep): Renegotiation seems to break video streams on Chrome 31.
// XXX(longsleep): Renegotiation can happen from both sides, meaning this
// could switch offer/answer side - oh crap.
if (false) {
var peerconnection = event.target;
console.log("Negotiation needed.", peerconnection.remoteDescription, peerconnection.iceConnectionState, peerconnection.signalingState, this);
this.currentcall.onNegotiationNeeded(this);
}
// XXX(longsleep): Renegotiation seems to break video streams on Chrome 31.
// XXX(longsleep): Renegotiation can happen from both sides, meaning this
// could switch offer/answer side - oh crap.
if (false) {
var peerconnection = event.target;
console.log("Negotiation needed.", peerconnection.remoteDescription, peerconnection.iceConnectionState, peerconnection.signalingState, this);
this.currentcall.onNegotiationNeeded(this);
}
};
};
PeerConnection.prototype.close = function() {
PeerConnection.prototype.close = function() {
if (this.datachannel) {
this.datachannel.close()
}
if (this.pc) {
this.pc.close();
}
if (this.datachannel) {
this.datachannel.close()
}
if (this.pc) {
this.pc.close();
}
this.currentcall.onRemoteStreamRemoved(null);
this.currentcall.onRemoteStreamRemoved(null);
this.datachannel = null;
this.pc = null;
this.datachannel = null;
this.pc = null;
};
};
PeerConnection.prototype.setRemoteDescription = function() {
PeerConnection.prototype.setRemoteDescription = function() {
return this.pc.setRemoteDescription.apply(this.pc, arguments);
return this.pc.setRemoteDescription.apply(this.pc, arguments);
};
};
PeerConnection.prototype.setLocalDescription = function() {
PeerConnection.prototype.setLocalDescription = function() {
return this.pc.setLocalDescription.apply(this.pc, arguments);
return this.pc.setLocalDescription.apply(this.pc, arguments);
};
};
PeerConnection.prototype.addIceCandidate = function() {
PeerConnection.prototype.addIceCandidate = function() {
return this.pc.addIceCandidate.apply(this.pc, arguments);
return this.pc.addIceCandidate.apply(this.pc, arguments);
};
};
PeerConnection.prototype.addStream = function() {
PeerConnection.prototype.addStream = function() {
return this.pc.addStream.apply(this.pc, arguments);
return this.pc.addStream.apply(this.pc, arguments);
};
};
PeerConnection.prototype.createAnswer = function() {
PeerConnection.prototype.createAnswer = function() {
return this.pc.createAnswer.apply(this.pc, arguments);
return this.pc.createAnswer.apply(this.pc, arguments);
};
};
PeerConnection.prototype.createOffer = function() {
PeerConnection.prototype.createOffer = function() {
return this.pc.createOffer.apply(this.pc, arguments);
return this.pc.createOffer.apply(this.pc, arguments);
};
};
PeerConnection.prototype.getRemoteStreams = function() {
PeerConnection.prototype.getRemoteStreams = function() {
return this.pc.getRemoteStreams.apply(this.pc, arguments);
return this.pc.getRemoteStreams.apply(this.pc, arguments);
};
};
PeerConnection.prototype.getLocalStreams = function() {
PeerConnection.prototype.getLocalStreams = function() {
return this.pc.getRemoteStreams.apply(this.pc, arguments);
return this.pc.getRemoteStreams.apply(this.pc, arguments);
};
};
PeerConnection.prototype.getStreamById = function() {
PeerConnection.prototype.getStreamById = function() {
return this.pc.getStreamById.appy(this.pc, arguments);
return this.pc.getStreamById.appy(this.pc, arguments);
};
};
return PeerConnection;
return PeerConnection;
});

8
static/js/mediastream/peerscreenshare.js

@ -75,9 +75,9 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens'], f @@ -75,9 +75,9 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens'], f
// support in Chrome 31+. This needs to be enabled in flags:
// chrome://flags/#enable-usermedia-screen-capture
var mandatoryVideoConstraints = $.extend({
chromeMediaSource: 'screen',
maxWidth: screenWidth,
maxHeight: screenHeight
chromeMediaSource: 'screen',
maxWidth: screenWidth,
maxHeight: screenHeight
}, options);
var mediaConstraints = {
audio: false,
@ -119,4 +119,4 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens'], f @@ -119,4 +119,4 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens'], f
return PeerScreenshare;
});
});

2
static/js/mediastream/peerxfer.js

@ -93,4 +93,4 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens', 'w @@ -93,4 +93,4 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens', 'w
return PeerXfer;
});
});

78
static/js/mediastream/tokens.js

@ -131,44 +131,48 @@ define(['jquery', 'underscore'], function(jquery, _) { @@ -131,44 +131,48 @@ define(['jquery', 'underscore'], function(jquery, _) {
var handler = obj.getHandler(this.makeId(from, id));
switch (type) {
case "Offer":
if (!handler) {
var creator = this.handlers[obj.handlerKey];
if (!creator) {
console.warn("Incoming offer for unknown handler", obj.handlerKey);
return;
case "Offer":
if (!handler) {
var creator = this.handlers[obj.handlerKey];
if (!creator) {
console.warn("Incoming offer for unknown handler", obj.handlerKey);
return;
}
// Create new handler based on type.
handler = creator(webrtc, id, token, from);
obj.addHandler(handler, from, id);
handler.createPeerConnection(function() {
obj.e.triggerHandler("created", [token, to, data, type, to2, from, handler]);
});
// Set message implementation.
handler.messageHandler = _.bind(obj.trigger, obj);
}
// Create new handler based on type.
handler = creator(webrtc, id, token, from);
obj.addHandler(handler, from, id);
handler.createPeerConnection(function() {
obj.e.triggerHandler("created", [token, to, data, type, to2, from, handler]);
});
// Set message implementation.
handler.messageHandler = _.bind(obj.trigger, obj);
}
handler.setRemoteDescription(new RTCSessionDescription(data), _.bind(function() {
handler.createAnswer(_.bind(function(sessionDescription, currenthandler) {
//console.log("Sending handler answer", sessionDescription, currenthandler.id);
webrtc.api.sendAnswer(from, sessionDescription);
handler.setRemoteDescription(new RTCSessionDescription(data), _.bind(function() {
handler.createAnswer(_.bind(function(sessionDescription, currenthandler) {
//console.log("Sending handler answer", sessionDescription, currenthandler.id);
webrtc.api.sendAnswer(from, sessionDescription);
}, this));
}, this));
}, this));
break;
case "Answer":
console.log("Token answer process.");
if (!handler.messageHandler) {
handler.messageHandler = _.bind(obj.trigger, obj);
}
handler.setRemoteDescription(new RTCSessionDescription(data));
break;
case "Candidate":
var candidate = new RTCIceCandidate({sdpMLineIndex: data.sdpMLineIndex, sdpMid: data.sdpMid, candidate: data.candidate});
handler.addIceCandidate(candidate);
break;
default:
//console.log("Processing token message", type, token);
obj.e.triggerHandler("message", [token, to, data, type, to2, from, handler]);
break;
break;
case "Answer":
console.log("Token answer process.");
if (!handler.messageHandler) {
handler.messageHandler = _.bind(obj.trigger, obj);
}
handler.setRemoteDescription(new RTCSessionDescription(data));
break;
case "Candidate":
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.sdpMLineIndex,
sdpMid: data.sdpMid,
candidate: data.candidate
});
handler.addIceCandidate(candidate);
break;
default:
//console.log("Processing token message", type, token);
obj.e.triggerHandler("message", [token, to, data, type, to2, from, handler]);
break;
}
};
@ -176,4 +180,4 @@ define(['jquery', 'underscore'], function(jquery, _) { @@ -176,4 +180,4 @@ define(['jquery', 'underscore'], function(jquery, _) {
var tokens = new Tokens();
return tokens;
});
});

351
static/js/mediastream/usermedia.js

@ -20,203 +20,206 @@ @@ -20,203 +20,206 @@
*/
define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _, AudioContext) {
// Create AudioContext singleton, if supported.
var context = AudioContext ? new AudioContext() : null;
var UserMedia = function(options) {
this.options = $.extend({}, options);
this.e = $({}); // Events.
this.localStream = null;
this.started = false;
// Audio level.
this.audioLevel = 0;
if (!this.options.noaudio && context && context.createScriptProcessor) {
this.audioSource = null;
this.audioProcessor = context.createScriptProcessor(2048, 1, 1);
this.audioProcessor.onaudioprocess = _.bind(function(event) {
// Compute audio input level based on raw PCM data.
if (!this.audioSource) {
return;
}
var input = event.inputBuffer.getChannelData(0);
var len = input.length
var total = 0;
var i = 0;
while (i < len) {
total += Math.abs(input[i++]);
}
// http://en.wikipedia.org/wiki/Root_mean_square
var rms = Math.sqrt(total / len);
this.audioLevel = rms;
//console.log("this.audioLevel", this.audioLevel);
}, this);
}
};
// Static.
UserMedia.testGetUserMedia = function(success_cb, error_cb) {
console.log("Requesting testGetUserMedia");
try {
getUserMedia({video:true, audio:true}, success_cb, error_cb);
} catch(e) {
console.error('getUserMedia failed with exception: '+ e.message);
error_cb(e);
}
};
UserMedia.prototype.doGetUserMedia = function(currentcall, mediaConstraints) {
if (!mediaConstraints) {
mediaConstraints = currentcall.mediaConstraints;
}
try {
console.log('Requesting access to local media with mediaConstraints:\n' +
' \'' + JSON.stringify(mediaConstraints) + '\'');
getUserMedia(mediaConstraints, _.bind(this.onUserMediaSuccess, this), _.bind(this.onUserMediaError, this));
this.started = true;
return true;
} catch(e) {
console.error('getUserMedia failed with exception: ' + e.message);
return false;
}
};
UserMedia.prototype.onUserMediaSuccess = function(stream) {
console.log('User has granted access to local media.');
if (!this.started) {
stream.stop();
return;
}
// Get notified of end events.
stream.onended = _.bind(function(event) {
console.log("User media stream ended.");
this.stop();
}, this);
if (this.audioProcessor) {
// Connect to audioProcessor.
this.audioSource = context.createMediaStreamSource(stream);
//console.log("got source", this.audioSource);
this.audioSource.connect(this.audioProcessor);
this.audioProcessor.connect(context.destination);
}
this.localStream = stream;
// Let webrtc handle the rest.
this.e.triggerHandler("mediasuccess", [this]);
};
UserMedia.prototype.onUserMediaError = function(error) {
console.error('Failed to get access to local media. Error was ' + error.name, error);
if (!this.started) {
return;
}
// Let webrtc handle the rest.
this.e.triggerHandler("mediaerror", [this, error]);
};
UserMedia.prototype.stop = function() {
if (this.audioSource) {
this.audioSource.disconnect();
this.audioSource = null;
}
if (this.localStream) {
this.localStream.stop()
this.localStream = null;
}
if (this.audioProcessor) {
this.audioProcessor.disconnect()
}
this.audioLevel = 0;
this.started = false;
console.log("Stopped user media.");
this.e.triggerHandler("stopped", [this]);
// Create AudioContext singleton, if supported.
var context = AudioContext ? new AudioContext() : null;
var UserMedia = function(options) {
this.options = $.extend({}, options);
this.e = $({}); // Events.
this.localStream = null;
this.started = false;
// Audio level.
this.audioLevel = 0;
if (!this.options.noaudio && context && context.createScriptProcessor) {
this.audioSource = null;
this.audioProcessor = context.createScriptProcessor(2048, 1, 1);
this.audioProcessor.onaudioprocess = _.bind(function(event) {
// Compute audio input level based on raw PCM data.
if (!this.audioSource) {
return;
}
var input = event.inputBuffer.getChannelData(0);
var len = input.length
var total = 0;
var i = 0;
while (i < len) {
total += Math.abs(input[i++]);
}
// http://en.wikipedia.org/wiki/Root_mean_square
var rms = Math.sqrt(total / len);
this.audioLevel = rms;
//console.log("this.audioLevel", this.audioLevel);
}, this);
}
};
// Static.
UserMedia.testGetUserMedia = function(success_cb, error_cb) {
console.log("Requesting testGetUserMedia");
try {
getUserMedia({
video: true,
audio: true
}, success_cb, error_cb);
} catch (e) {
console.error('getUserMedia failed with exception: ' + e.message);
error_cb(e);
}
};
UserMedia.prototype.doGetUserMedia = function(currentcall, mediaConstraints) {
if (!mediaConstraints) {
mediaConstraints = currentcall.mediaConstraints;
}
try {
console.log('Requesting access to local media with mediaConstraints:\n' +
' \'' + JSON.stringify(mediaConstraints) + '\'');
getUserMedia(mediaConstraints, _.bind(this.onUserMediaSuccess, this), _.bind(this.onUserMediaError, this));
this.started = true;
return true;
} catch (e) {
console.error('getUserMedia failed with exception: ' + e.message);
return false;
}
};
UserMedia.prototype.onUserMediaSuccess = function(stream) {
console.log('User has granted access to local media.');
if (!this.started) {
stream.stop();
return;
}
// Get notified of end events.
stream.onended = _.bind(function(event) {
console.log("User media stream ended.");
this.stop();
}, this);
if (this.audioProcessor) {
// Connect to audioProcessor.
this.audioSource = context.createMediaStreamSource(stream);
//console.log("got source", this.audioSource);
this.audioSource.connect(this.audioProcessor);
this.audioProcessor.connect(context.destination);
}
this.localStream = stream;
// Let webrtc handle the rest.
this.e.triggerHandler("mediasuccess", [this]);
};
UserMedia.prototype.onUserMediaError = function(error) {
console.error('Failed to get access to local media. Error was ' + error.name, error);
if (!this.started) {
return;
}
// Let webrtc handle the rest.
this.e.triggerHandler("mediaerror", [this, error]);
};
UserMedia.prototype.stop = function() {
if (this.audioSource) {
this.audioSource.disconnect();
this.audioSource = null;
}
if (this.localStream) {
this.localStream.stop()
this.localStream = null;
}
if (this.audioProcessor) {
this.audioProcessor.disconnect()
}
this.audioLevel = 0;
this.started = false;
console.log("Stopped user media.");
this.e.triggerHandler("stopped", [this]);
};
};
UserMedia.prototype.applyAudioMute = function(mute) {
UserMedia.prototype.applyAudioMute = function(mute) {
if (this.localStream) {
if (this.localStream) {
var audioTracks = this.localStream.getAudioTracks();
if (audioTracks.length === 0) {
//console.log('No local audio available.');
return;
}
var audioTracks = this.localStream.getAudioTracks();
if (audioTracks.length === 0) {
//console.log('No local audio available.');
return;
}
for (i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = !mute;
}
for (i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = !mute;
}
if (mute) {
console.log("Local audio muted.")
} else {
console.log("Local audio unmuted.")
}
if (mute) {
console.log("Local audio muted.")
} else {
console.log("Local audio unmuted.")
}
}
}
return mute;
return mute;
};
};
UserMedia.prototype.applyVideoMute = function(mute) {
UserMedia.prototype.applyVideoMute = function(mute) {
if (this.localStream) {
if (this.localStream) {
var videoTracks = this.localStream.getVideoTracks();
if (videoTracks.length === 0) {
//console.log('No local video available.');
return;
}
var videoTracks = this.localStream.getVideoTracks();
if (videoTracks.length === 0) {
//console.log('No local video available.');
return;
}
for (i = 0; i < videoTracks.length; i++) {
videoTracks[i].enabled = !mute;
}
for (i = 0; i < videoTracks.length; i++) {
videoTracks[i].enabled = !mute;
}
if (mute) {
console.log("Local video muted.")
} else {
console.log("Local video unmuted.")
}
if (mute) {
console.log("Local video muted.")
} else {
console.log("Local video unmuted.")
}
}
}
return mute;
return mute;
};
};
UserMedia.prototype.addToPeerConnection = function(pc) {
UserMedia.prototype.addToPeerConnection = function(pc) {
console.log("Add stream to peer connection", pc, this.localStream);
if (this.localStream) {
pc.addStream(this.localStream);
}
console.log("Add stream to peer connection", pc, this.localStream);
if (this.localStream) {
pc.addStream(this.localStream);
}
};
};
UserMedia.prototype.attachMediaStream = function(video) {
UserMedia.prototype.attachMediaStream = function(video) {
//console.log("attach", video, this.localStream);
attachMediaStream(video, this.localStream);
//console.log("attach", video, this.localStream);
attachMediaStream(video, this.localStream);
};
};
return UserMedia;
return UserMedia;
});

269
static/js/mediastream/utils.js

@ -20,140 +20,139 @@ @@ -20,140 +20,139 @@
*/
define(['jquery', 'underscore'], function($, _) {
var Utils = function() {
}
Utils.prototype.mergeConstraints = function(cons1, cons2) {
var merged = cons1;
var name;
for (name in cons2.mandatory) {
merged.mandatory[name] = cons2.mandatory[name];
}
merged.optional.concat(cons2.optional);
return merged;
};
Utils.prototype.extractSdp = function(sdpLine, pattern) {
var result = sdpLine.match(pattern);
return (result && result.length == 2)? result[1]: null;
};
Utils.prototype.addStereo = function(sdp) {
// Set Opus in Stereo.
var sdpLines = sdp.split('\r\n');
var opusPayload = "";
var fmtpLineIndex = null;
var i;
// Find opus payload.
for (i=0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
break;
}
}
// Find the payload in fmtp line.
for (i=0; i < sdpLines.length; i++) {
if (sdpLines[i].search('a=fmtp') !== -1) {
var payload = this.extractSdp(sdpLines[i], /a=fmtp:(\d+)/ );
if (payload === opusPayload) {
fmtpLineIndex = i;
break;
}
}
}
// No fmtp line found.
if (fmtpLineIndex === null) {
console.log("Unable to add stereo (no fmtp line for opus payload)", opusPayload);
return sdp;
}
// Append stereo=1 to fmtp line.
sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1');
sdp = sdpLines.join('\r\n');
console.log("Enabled opus stereo.");
return sdp;
};
Utils.prototype.preferOpus = function(sdp) {
// Set Opus as the preferred codec in SDP if Opus is present.
var sdpLines = sdp.split('\r\n');
var mLineIndex = null;
var i;
// Search for m line.
for (i=0; i < sdpLines.length; i++) {
if (sdpLines[i].search('m=audio') !== -1) {
mLineIndex = i;
break;
}
}
if (mLineIndex === null) {
return sdp;
}
// If Opus is available, set it as the default in m line.
for (i=0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
var opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
if (opusPayload) {
sdpLines[mLineIndex] = this.setDefaultCodec(sdpLines[mLineIndex], opusPayload);
}
break;
}
}
// Remove CN in m line and sdp.
sdpLines = this.removeCN(sdpLines, mLineIndex);
sdp = sdpLines.join('\r\n');
return sdp;
};
Utils.prototype.setDefaultCodec = function (mLine, payload) {
// Set the selected codec to the first in m line.
var elements = mLine.split(' ');
var newLine = [];
var index = 0;
for (var i = 0; i < elements.length; i++) {
// Format of media starts from the fourth.
if (index === 3) {
newLine[index++] = payload; // Put target payload to the first.
}
if (elements[i] !== payload) {
newLine[index++] = elements[i];
}
}
return newLine.join(' ');
};
Utils.prototype.removeCN = function(sdpLines, mLineIndex) {
// Strip CN from sdp before CN constraints is ready.
var mLineElements = sdpLines[mLineIndex].split(' ');
// Scan from end for the convenience of removing an item.
for (var i = sdpLines.length-1; i >= 0; i--) {
var payload = this.extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
if (payload) {
var cnPos = mLineElements.indexOf(payload);
if (cnPos !== -1) {
// Remove CN payload from m line.
mLineElements.splice(cnPos, 1);
}
// Remove CN line in sdp
sdpLines.splice(i, 1);
}
}
sdpLines[mLineIndex] = mLineElements.join(' ');
return sdpLines;
};
var utils = new Utils();
return utils;
var Utils = function() {}
Utils.prototype.mergeConstraints = function(cons1, cons2) {
var merged = cons1;
var name;
for (name in cons2.mandatory) {
merged.mandatory[name] = cons2.mandatory[name];
}
merged.optional.concat(cons2.optional);
return merged;
};
Utils.prototype.extractSdp = function(sdpLine, pattern) {
var result = sdpLine.match(pattern);
return (result && result.length == 2) ? result[1] : null;
};
Utils.prototype.addStereo = function(sdp) {
// Set Opus in Stereo.
var sdpLines = sdp.split('\r\n');
var opusPayload = "";
var fmtpLineIndex = null;
var i;
// Find opus payload.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
break;
}
}
// Find the payload in fmtp line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('a=fmtp') !== -1) {
var payload = this.extractSdp(sdpLines[i], /a=fmtp:(\d+)/);
if (payload === opusPayload) {
fmtpLineIndex = i;
break;
}
}
}
// No fmtp line found.
if (fmtpLineIndex === null) {
console.log("Unable to add stereo (no fmtp line for opus payload)", opusPayload);
return sdp;
}
// Append stereo=1 to fmtp line.
sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1');
sdp = sdpLines.join('\r\n');
console.log("Enabled opus stereo.");
return sdp;
};
Utils.prototype.preferOpus = function(sdp) {
// Set Opus as the preferred codec in SDP if Opus is present.
var sdpLines = sdp.split('\r\n');
var mLineIndex = null;
var i;
// Search for m line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('m=audio') !== -1) {
mLineIndex = i;
break;
}
}
if (mLineIndex === null) {
return sdp;
}
// If Opus is available, set it as the default in m line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
var opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
if (opusPayload) {
sdpLines[mLineIndex] = this.setDefaultCodec(sdpLines[mLineIndex], opusPayload);
}
break;
}
}
// Remove CN in m line and sdp.
sdpLines = this.removeCN(sdpLines, mLineIndex);
sdp = sdpLines.join('\r\n');
return sdp;
};
Utils.prototype.setDefaultCodec = function(mLine, payload) {
// Set the selected codec to the first in m line.
var elements = mLine.split(' ');
var newLine = [];
var index = 0;
for (var i = 0; i < elements.length; i++) {
// Format of media starts from the fourth.
if (index === 3) {
newLine[index++] = payload; // Put target payload to the first.
}
if (elements[i] !== payload) {
newLine[index++] = elements[i];
}
}
return newLine.join(' ');
};
Utils.prototype.removeCN = function(sdpLines, mLineIndex) {
// Strip CN from sdp before CN constraints is ready.
var mLineElements = sdpLines[mLineIndex].split(' ');
// Scan from end for the convenience of removing an item.
for (var i = sdpLines.length - 1; i >= 0; i--) {
var payload = this.extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
if (payload) {
var cnPos = mLineElements.indexOf(payload);
if (cnPos !== -1) {
// Remove CN payload from m line.
mLineElements.splice(cnPos, 1);
}
// Remove CN line in sdp
sdpLines.splice(i, 1);
}
}
sdpLines[mLineIndex] = mLineElements.join(' ');
return sdpLines;
};
var utils = new Utils();
return utils;
});

1319
static/js/mediastream/webrtc.js

File diff suppressed because it is too large Load Diff

209
static/js/modules/angular-humanize.js vendored

@ -3,120 +3,121 @@ @@ -3,120 +3,121 @@
* Copyright 2013-2014 struktur AG, http://www.struktur.de
* License: MIT
*/
(function(window, angular, humanize, undefined) {'use strict';
(function(window, angular, humanize, undefined) {
'use strict';
/**
* # ngHumanize
*
* `ngHumanize` is the name of the optional Angular module that provides
* filters for humanization of data.
*
* The implementation uses [humanize.js](https://github.com/taijinlee/humanize)
* for humanization implementation functions.
*
*/
/**
* # ngHumanize
*
* `ngHumanize` is the name of the optional Angular module that provides
* filters for humanization of data.
*
* The implementation uses [humanize.js](https://github.com/taijinlee/humanize)
* for humanization implementation functions.
*
*/
// define ngHumanize module
var ngHumanize = angular.module('ngHumanize', []);
// define ngHumanize module
var ngHumanize = angular.module('ngHumanize', []);
/**
* This is a port of php.js date and behaves exactly like PHP's date.
* http://php.net/manual/en/function.date.php
*/
ngHumanize.filter("humanizeDate", function() {
return function(input, format) {
return humanize.date(format, input);
}
});
/**
* This is a port of php.js date and behaves exactly like PHP's date.
* http://php.net/manual/en/function.date.php
*/
ngHumanize.filter("humanizeDate", function() {
return function(input, format) {
return humanize.date(format, input);
}
});
/**
* Format a number to have decimal significant decimal places, using
* decPoint as the decimal separator, and thousandsSep as thousands separater.
*/
ngHumanize.filter("humanizeNumber", function() {
return function(input, decimals, decPoint, thousandsSep) {
return humanize.numberFormat(input, decimals, decPoint, thousandsSep);
}
});
/**
* Format a number to have decimal significant decimal places, using
* decPoint as the decimal separator, and thousandsSep as thousands separater.
*/
ngHumanize.filter("humanizeNumber", function() {
return function(input, decimals, decPoint, thousandsSep) {
return humanize.numberFormat(input, decimals, decPoint, thousandsSep);
}
});
/**
* Returns 'today', 'tomorrow' or 'yesterday', as appropriate,
* otherwise format the date using the passed format with
* humanize.date().
*/
ngHumanize.filter("humanizeNaturalDay", function() {
return function(input, format) {
return humanize.naturalDay(input, format);
}
});
/**
* Returns 'today', 'tomorrow' or 'yesterday', as appropriate,
* otherwise format the date using the passed format with
* humanize.date().
*/
ngHumanize.filter("humanizeNaturalDay", function() {
return function(input, format) {
return humanize.naturalDay(input, format);
}
});
/**
* Returns a relative time to the current time, seconds as the most
* granular up to years to the least granular.
*/
ngHumanize.filter("humanizeRelativeTime", function() {
return function(input) {
return humanize.relativeTime(input);
}
});
/**
* Returns a relative time to the current time, seconds as the most
* granular up to years to the least granular.
*/
ngHumanize.filter("humanizeRelativeTime", function() {
return function(input) {
return humanize.relativeTime(input);
}
});
/**
* Converts a number into its ordinal representation.
* http://en.wikipedia.org/wiki/Ordinal_number_(linguistics)
*/
ngHumanize.filter("humanizeOrdinal", function() {
return function(format) {
return humanize.ordinal(format);
}
});
/**
* Converts a number into its ordinal representation.
* http://en.wikipedia.org/wiki/Ordinal_number_(linguistics)
*/
ngHumanize.filter("humanizeOrdinal", function() {
return function(format) {
return humanize.ordinal(format);
}
});
/**
* Converts a byte count to a human readable value using kilo as the basis,
* and numberFormat formatting.
*/
ngHumanize.filter("humanizeFilesize", function() {
return function(input, kilo, decimals, decPoint, thousandsSep) {
return humanize.filesize(input, kilo, decimals, decPoint, thousandsSep);
}
});
/**
* Converts a byte count to a human readable value using kilo as the basis,
* and numberFormat formatting.
*/
ngHumanize.filter("humanizeFilesize", function() {
return function(input, kilo, decimals, decPoint, thousandsSep) {
return humanize.filesize(input, kilo, decimals, decPoint, thousandsSep);
}
});
/**
* Converts a string's newlines into properly formatted html ie. one
* new line -> br, two new lines -> p, entire thing wrapped in p.
*/
ngHumanize.filter("humanizeLinebreaks", function() {
return function(input) {
return humanize.linebreaks(input);
}
});
/**
* Converts a string's newlines into properly formatted html ie. one
* new line -> br, two new lines -> p, entire thing wrapped in p.
*/
ngHumanize.filter("humanizeLinebreaks", function() {
return function(input) {
return humanize.linebreaks(input);
}
});
/**
* Converts a string's newlines into br's.
*/
ngHumanize.filter("humanizeNl2br", function() {
return function(input) {
return humanize.nl2br(input);
}
});
/**
* Converts a string's newlines into br's.
*/
ngHumanize.filter("humanizeNl2br", function() {
return function(input) {
return humanize.nl2br(input);
}
});
/**
* Truncates a string to length-1 and appends '…'. If string is shorter
* than length, then no-op.
*/
ngHumanize.filter("humanizeTruncatechars", function() {
return function(input, length) {
return humanize.truncatechars(input, length);
}
});
/**
* Truncates a string to length-1 and appends '…'. If string is shorter
* than length, then no-op.
*/
ngHumanize.filter("humanizeTruncatechars", function() {
return function(input, length) {
return humanize.truncatechars(input, length);
}
});
/**
* Truncates a string to only include the first numWords words and
* appends '…'. If string has fewer words than numWords, then no-op.
*/
ngHumanize.filter("humanizeTruncatewords", function() {
return function(input, numWords) {
return humanize.truncatewords(input, numWords);
}
});
/**
* Truncates a string to only include the first numWords words and
* appends '…'. If string has fewer words than numWords, then no-op.
*/
ngHumanize.filter("humanizeTruncatewords", function() {
return function(input, numWords) {
return humanize.truncatewords(input, numWords);
}
});
}(window, window.angular, window.humanize));
}(window, window.angular, window.humanize));

26
static/js/services/alertify.js

@ -30,14 +30,14 @@ define(["angular"], function(angular) { @@ -30,14 +30,14 @@ define(["angular"], function(angular) {
$scope.okButtonLabel = data.okButtonLabel || "Ok";
$scope.cancelButtonLabel = data.cancelButtonLabel || "Cancel";
$scope.cancel = function(){
$scope.cancel = function() {
$modalInstance.dismiss('Canceled');
};
$scope.save = function(){
$scope.save = function() {
$modalInstance.close($scope.input.text);
};
$scope.hitEnter = function(evt){
if(angular.equals(evt.keyCode, 13) && !(angular.equals($scope.input.text, null) || angular.equals($scope.input.text, ''))) {
$scope.hitEnter = function(evt) {
if (angular.equals(evt.keyCode, 13) && !(angular.equals($scope.input.text, null) || angular.equals($scope.input.text, ''))) {
$scope.save();
}
};
@ -48,13 +48,13 @@ define(["angular"], function(angular) { @@ -48,13 +48,13 @@ define(["angular"], function(angular) {
return ["$window", "$dialogs", "$templateCache", "translation", function($window, $dialogs, $templateCache, translation) {
// Overwrite templates from dialogs with fontawesome/i18n variants.
$templateCache.put('/dialogs/error.html','<div class="modal-header dialog-header-error"><button type="button" class="close" ng-click="close()">&times;</button><h4 class="modal-title text-danger"><span class="fa fa-warning"></span> <span ng-bind-html="header"></span></h4></div><div class="modal-body text-danger" ng-bind-html="msg"></div><div class="modal-footer"><button type="button" class="btn btn-default" ng-click="close()">{{_("Close")}}</button></div>');
$templateCache.put('/dialogs/wait.html','<div class="modal-header dialog-header-wait"><h4 class="modal-title"><span class="fa fa-clock-o"></span> Please Wait</h4></div><div class="modal-body"><p ng-bind-html="msg"></p><div class="progress progress-striped active"><div class="progress-bar progress-bar-info" ng-style="getProgress()"></div><span class="sr-only">{{progress}}% Complete</span></div></div>');
$templateCache.put('/dialogs/notify.html','<div class="modal-header dialog-header-notify"><button type="button" class="close" ng-click="close()" class="pull-right">&times;</button><h4 class="modal-title text-info"><span class="fa fa-info-circle"></span> {{header}}</h4></div><div class="modal-body text-info" ng-bind-html="msg"></div><div class="modal-footer"><button type="button" class="btn btn-primary" ng-click="close()">{{_("Ok")}}</button></div>');
$templateCache.put('/dialogs/confirm.html','<div class="modal-header dialog-header-confirm"><button type="button" class="close" ng-click="no()">&times;</button><h4 class="modal-title"><span class="fa fa-check-square-o"></span> {{header}}</h4></div><div class="modal-body" ng-bind-html="msg"></div><div class="modal-footer"><button type="button" class="btn btn-default" ng-click="yes()">{{_("Ok")}}</button><button type="button" class="btn btn-primary" ng-click="no()">{{_("Cancel")}}</button></div>');
$templateCache.put('/dialogs/error.html', '<div class="modal-header dialog-header-error"><button type="button" class="close" ng-click="close()">&times;</button><h4 class="modal-title text-danger"><span class="fa fa-warning"></span> <span ng-bind-html="header"></span></h4></div><div class="modal-body text-danger" ng-bind-html="msg"></div><div class="modal-footer"><button type="button" class="btn btn-default" ng-click="close()">{{_("Close")}}</button></div>');
$templateCache.put('/dialogs/wait.html', '<div class="modal-header dialog-header-wait"><h4 class="modal-title"><span class="fa fa-clock-o"></span> Please Wait</h4></div><div class="modal-body"><p ng-bind-html="msg"></p><div class="progress progress-striped active"><div class="progress-bar progress-bar-info" ng-style="getProgress()"></div><span class="sr-only">{{progress}}% Complete</span></div></div>');
$templateCache.put('/dialogs/notify.html', '<div class="modal-header dialog-header-notify"><button type="button" class="close" ng-click="close()" class="pull-right">&times;</button><h4 class="modal-title text-info"><span class="fa fa-info-circle"></span> {{header}}</h4></div><div class="modal-body text-info" ng-bind-html="msg"></div><div class="modal-footer"><button type="button" class="btn btn-primary" ng-click="close()">{{_("Ok")}}</button></div>');
$templateCache.put('/dialogs/confirm.html', '<div class="modal-header dialog-header-confirm"><button type="button" class="close" ng-click="no()">&times;</button><h4 class="modal-title"><span class="fa fa-check-square-o"></span> {{header}}</h4></div><div class="modal-body" ng-bind-html="msg"></div><div class="modal-footer"><button type="button" class="btn btn-default" ng-click="yes()">{{_("Ok")}}</button><button type="button" class="btn btn-primary" ng-click="no()">{{_("Cancel")}}</button></div>');
// Add new template for prompt.
$templateCache.put('/alertify/prompt.html','<div class="modal-header"><h4 class="modal-title"><span class="fa fa-star"></span> <span ng-bind-html="header"></span></h4></div><div class="modal-body"><ng-form name="promptDialog" novalidate role="form"><div class="form-group input-group-lg" ng-class="{true: \'has-error\'}[promptDialog.text.$dirty && promptDialog.text.$invalid]"><label class="control-label"></label><input type="text" id="{{id}}" class="form-control" name="text" ng-model="input.text" ng-keyup="hitEnter($event)" required></div></ng-form></div><div class="modal-footer"><button type="button" class="btn btn-default" ng-click="cancel()">{{cancelButtonLabel}}</button><button type="button" class="btn btn-primary" ng-click="save()" ng-disabled="(promptDialog.$dirty && promptDialog.$invalid) || promptDialog.$pristine">{{okButtonLabel}}</button></div>');
$templateCache.put('/alertify/prompt.html', '<div class="modal-header"><h4 class="modal-title"><span class="fa fa-star"></span> <span ng-bind-html="header"></span></h4></div><div class="modal-body"><ng-form name="promptDialog" novalidate role="form"><div class="form-group input-group-lg" ng-class="{true: \'has-error\'}[promptDialog.text.$dirty && promptDialog.text.$invalid]"><label class="control-label"></label><input type="text" id="{{id}}" class="form-control" name="text" ng-model="input.text" ng-keyup="hitEnter($event)" required></div></ng-form></div><div class="modal-footer"><button type="button" class="btn btn-default" ng-click="cancel()">{{cancelButtonLabel}}</button><button type="button" class="btn btn-primary" ng-click="save()" ng-disabled="(promptDialog.$dirty && promptDialog.$invalid) || promptDialog.$pristine">{{okButtonLabel}}</button></div>');
var defaultMessages = {
error: translation._("Error"),
@ -103,7 +103,7 @@ define(["angular"], function(angular) { @@ -103,7 +103,7 @@ define(["angular"], function(angular) {
return dialog.exec("confirm", null, message, ok_cb, err_cb);
},
prompt: function(title, ok_cb, err_cb) {
var id = "allertifyPrompt"+(promptIdx++);
var id = "allertifyPrompt" + (promptIdx++);
var data = {
okButtonLabel: api.defaultMessages.okButtonLabel || "Ok",
cancelButtonLabel: api.defaultMessages.cancelButtonLabel || "Cancel",
@ -111,11 +111,11 @@ define(["angular"], function(angular) { @@ -111,11 +111,11 @@ define(["angular"], function(angular) {
id: id
}
var dlg = $dialogs.create('/alertify/prompt.html', promptController, data, {});
dlg.result.then(function(text){
dlg.result.then(function(text) {
if (ok_cb) {
ok_cb(text);
}
}, function(){
}, function() {
if (err_cb) {
err_cb();
}
@ -140,4 +140,4 @@ define(["angular"], function(angular) { @@ -140,4 +140,4 @@ define(["angular"], function(angular) {
}];
});
});

34
static/js/services/appdata.js

@ -20,23 +20,23 @@ @@ -20,23 +20,23 @@
*/
define([], function() {
// appData
return [function() {
// appData
return [function() {
var data = {
data: null
}
var appData = {
get: function() {
return data.data;
},
set: function(d) {
data.data = d;
return d;
}
}
return appData;
var data = {
data: null
}
var appData = {
get: function() {
return data.data;
},
set: function(d) {
data.data = d;
return d;
}
}
return appData;
}];
}];
});
});

177
static/js/services/buddydata.js

@ -20,95 +20,98 @@ @@ -20,95 +20,98 @@
*/
define(['underscore'], function(underscore) {
// buddyData
return [function() {
// buddyData
return [function() {
var scopes = {};
var brain = {};
var pushed = {};
var count = 0;
var scopes = {};
var brain = {};
var pushed = {};
var count = 0;
var buddyData = {
clear: function() {
_.each(scopes, function(scope, id) {
scope.$destroy();
brain[id]=scope;
});
scopes = {};
},
push: function(id) {
var entry = pushed[id];
if (!entry) {
entry = pushed[id] = {count: 1, scope: scopes[id]};
} else {
entry.count++;
}
//console.log("pushed buddy", id, entry);
return entry.count;
},
pop: function(id) {
var entry = pushed[id];
//console.log("popped buddy", id, entry);
if (entry) {
entry.count--;
if (entry.count <= 0) {
delete pushed[id];
}
return entry.count;
}
return 0;
},
get: function(id, createInParent, afterCreateCallback) {
if (scopes.hasOwnProperty(id)) {
return scopes[id];
} else if (!createInParent && pushed.hasOwnProperty(id)) {
return pushed[id].scope;
} else {
if (createInParent) {
// If we have a parent we can create a new scope.
var scope = scopes[id] = createInParent.$new();
scope.buddyIndex = ++count;
scope.buddyIndexSortable = ("0000000" + scope.buddyIndex).slice(-7);
if (pushed.hasOwnProperty(id)) {
// Refresh pushed scope reference.
pushed[id].scope = scope;
}
if (afterCreateCallback) {
afterCreateCallback(scope);
}
return scope;
} else {
return null;
}
}
},
lookup: function(id, onlyactive) {
var scope = null;
if (scopes.hasOwnProperty(id)) {
scope = scopes[id];
} else if (!onlyactive) {
if (brain.hasOwnProperty(id)) {
scope = brain[id];
} else if (pushed.hasOwnProperty(id)) {
scope = pushed[id].scope;
}
}
return scope;
},
del: function(id, hard) {
var scope = scopes[id];
if (scope) {
scope.$destroy();
brain[id]=scope;
delete scopes[id];
return scope;
} else {
return null;
}
}
};
return buddyData;
var buddyData = {
clear: function() {
_.each(scopes, function(scope, id) {
scope.$destroy();
brain[id] = scope;
});
scopes = {};
},
push: function(id) {
var entry = pushed[id];
if (!entry) {
entry = pushed[id] = {
count: 1,
scope: scopes[id]
};
} else {
entry.count++;
}
//console.log("pushed buddy", id, entry);
return entry.count;
},
pop: function(id) {
var entry = pushed[id];
//console.log("popped buddy", id, entry);
if (entry) {
entry.count--;
if (entry.count <= 0) {
delete pushed[id];
}
return entry.count;
}
return 0;
},
get: function(id, createInParent, afterCreateCallback) {
if (scopes.hasOwnProperty(id)) {
return scopes[id];
} else if (!createInParent && pushed.hasOwnProperty(id)) {
return pushed[id].scope;
} else {
if (createInParent) {
// If we have a parent we can create a new scope.
var scope = scopes[id] = createInParent.$new();
scope.buddyIndex = ++count;
scope.buddyIndexSortable = ("0000000" + scope.buddyIndex).slice(-7);
if (pushed.hasOwnProperty(id)) {
// Refresh pushed scope reference.
pushed[id].scope = scope;
}
if (afterCreateCallback) {
afterCreateCallback(scope);
}
return scope;
} else {
return null;
}
}
},
lookup: function(id, onlyactive) {
var scope = null;
if (scopes.hasOwnProperty(id)) {
scope = scopes[id];
} else if (!onlyactive) {
if (brain.hasOwnProperty(id)) {
scope = brain[id];
} else if (pushed.hasOwnProperty(id)) {
scope = pushed[id].scope;
}
}
return scope;
},
del: function(id, hard) {
var scope = scopes[id];
if (scope) {
scope.$destroy();
brain[id] = scope;
delete scopes[id];
return scope;
} else {
return null;
}
}
};
return buddyData;
}];
}];
});

902
static/js/services/buddylist.js

@ -20,464 +20,464 @@ @@ -20,464 +20,464 @@
*/
define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!partials/buddyactions.html', 'text!partials/buddyactionsforaudiomixer.html', 'rAF'], function(_, Modernizr, AvlTree, templateBuddy, templateBuddyActions, templateBuddyActionsForAudioMixer) {
var BuddyTree = function() {
var BuddyTree = function() {
this.data = {};
this.tree = new AvlTree(function(a ,b) {
return a.sort.localeCompare(b.sort);
});
this.data = {};
this.tree = new AvlTree(function(a, b) {
return a.sort.localeCompare(b.sort);
});
};
};
BuddyTree.prototype.create = function(id, scope) {
BuddyTree.prototype.create = function(id, scope) {
var sort = scope.displayName ? scope.displayName : "session "+scope.buddyIndexSortable+" "+id;
var data = {
id: id,
sort: sort + "z" + id
}
return data;
var sort = scope.displayName ? scope.displayName : "session " + scope.buddyIndexSortable + " " + id;
var data = {
id: id,
sort: sort + "z" + id
}
return data;
};
};
BuddyTree.prototype.add = function(id, scope) {
BuddyTree.prototype.add = function(id, scope) {
var data = this.create(id, scope);
this._add(id, data);
return this.position(id);
var data = this.create(id, scope);
this._add(id, data);
return this.position(id);
};
BuddyTree.prototype._add = function(id, data) {
if (this.tree.add(data)) {
this.data[id] = data;
}
};
BuddyTree.prototype.remove = function(id) {
if (this.data.hasOwnProperty(id)) {
this.tree.remove(this.data[id]);
delete this.data[id];
}
};
/**
* Returns undefined when no change required. Position result otherwise.
*/
BuddyTree.prototype.update = function(id, scope) {
var current = this.data[id];
if (!current) {
return this.add(id, scope);
}
var data = this.create(id, scope);
if (data.sort !== current.sort) {
this.tree.remove(current);
this._add(id, data);
return this.position(id);
}
return undefined;
};
/**
* Returns null when end position. Id of element to insert before otherwise.
*/
BuddyTree.prototype.position = function(id) {
var result = null;
this.tree.inOrderTraverse(function(c) {
if (c.id !== id) {
result = c.id;
return true;
}
}, this.data[id]);
return result;
};
BuddyTree.prototype.clear = function() {
this.tree.clear();
this.data = {};
};
// buddyList
return ["$window", "$compile", "playSound", "buddyData", "fastScroll", "mediaStream", function ($window, $compile, playSound, buddyData, fastScroll, mediaStream) {
var requestAnimationFrame = $window.requestAnimationFrame;
var buddyTemplate = $compile(templateBuddy);
var buddyActions = $compile(templateBuddyActions);
var buddyActionsForAudioMixer = $compile(templateBuddyActionsForAudioMixer);
//console.log("$buddylist $get");
var doc = $window.document;
var buddyCount = 0;
var Buddylist = function($element, $scope, opts) {
this.$scope = $scope;
this.$element = $element.find(".buddycontainer > div");
this.options = angular.extend({}, opts);
this.$element.empty();
this.actionElements = {};
this.tree = new BuddyTree();
this.queue = [];
this.lefts = {};
this.playSoundLeft = false;
this.playSoundJoined = false;
fastScroll.apply($element, this.$element);
$element.on("mouseenter mouseleave", ".buddy", _.bind(function(event) {
// Hover handler for on Buddy actions.
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().session.Id);
}, this));
$element.on("click", ".buddy", _.bind(function(event) {
var buddyElement = $(event.currentTarget);
buddyElement.scope().doDefault();
}, this));
$element.attr("data-xthreshold", "10");
$element.on("swipeleft", ".buddy", _.bind(function(event) {
event.preventDefault();
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().session.Id);
}, this));
$window.setInterval(_.bind(this.soundLoop, this), 500);
var update = _.bind(function refreshBuddies() {
this.refreshBuddies();
requestAnimationFrame(update);
}, this);
requestAnimationFrame(update);
};
Buddylist.prototype.addBuddyElementToScope = function(scope, before, container) {
if (!container) {
container = this.$element[0];
}
buddyTemplate(scope, function($clonedElement, $scope) {
//console.log("create", $scope.displayName, before)
if (before) {
// Insert directly before another node.
var beforeScope = buddyData.get(before);
if (beforeScope && beforeScope.element) {
container.insertBefore($clonedElement[0], beforeScope.element[0]);
} else {
// Append to end assuming before element was removed. Can this happen?
container.appendChild($clonedElement[0]);
}
} else {
// Append to end.
container.appendChild($clonedElement[0]);
}
$scope.element=$clonedElement;
});
};
Buddylist.prototype.onBuddyScope = function(scope) {
scope.element = null;
scope.doDefault = function() {
var id = scope.session.Id;
if (scope.status.isMixer) {
return scope.doAudioConference(id);
}
return scope.doCall(id);
};
scope.$on("$destroy", function() {
scope.element = null;
scope.killed = true;
});
};
Buddylist.prototype.soundLoop = function() {
if (this.playSoundLeft) {
playSound.play("left");
this.playSoundLeft = false;
}
if (this.playSoundJoined) {
playSound.play("joined");
this.playSoundJoined = false;
}
};
Buddylist.prototype.refreshBuddies = function() {
//console.log("processing", this.queue.length);
var processed = 0;
var entry;
var scope;
var id;
var before;
var action;
var not_exists;
// Cleanup lefts.
var lefts = this.lefts;
if (!_.isEmpty(lefts)) {
_.each(lefts, function(element, k) {
if (element) {
element.remove();
}
});
this.lefts = {};
}
var refresh = false;
var queue = this.queue;
if (queue.length) {
//var $element = this.$element;
var container = this.$element[0];
while (true) {
entry = queue.shift();
if (!entry) {
// Queue empty.
break;
}
id = entry[1];
scope = null;
if (id) {
scope = buddyData.lookup(id, false);
if (!scope || scope.killed) {
continue;
}
}
action = entry[0];
before = entry[2];
not_exists = scope.element ? false : true;
if (not_exists) {
this.addBuddyElementToScope(scope, before, container);
} else {
if (typeof(before) === "undefined") {
// No action when undefined.
} else if (before) {
// Move directly before another node.
var beforeScope = buddyData.get(before);
if (beforeScope && beforeScope.element) {
container.insertBefore(scope.element[0], beforeScope.element[0]);
} else {
// Move to end, assuming before element was removed.
container.appendChild(scope.element[0]);
}
} else {
// Move to end.
container.appendChild(scope.element[0]);
}
}
processed++;
refresh = true;
if (processed > 10) {
break;
}
}
}
scope = this.$scope;
var apply = refresh;
var loading = scope.loading;
if (refresh) {
if (!loading) {
scope.loading = true;
}
} else {
if (scope.loading) {
scope.loading = false;
apply = true;
}
}
var empty = buddyCount === 0;
if (empty != scope.empty) {
scope.empty = empty;
apply = true;
}
if (apply) {
scope.$apply();
}
};
Buddylist.prototype.updateBuddyPicture = function(status) {
url = status.buddyPicture;
if (!url) {
return;
}
if (url.indexOf("img:") === 0) {
status.buddyPicture = status.buddyPictureLocalUrl = mediaStream.url.buddy(url.substr(4));
}
};
Buddylist.prototype.onStatus = function(data) {
//console.log("onStatus", status);
var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
// Update session.
scope.session.Userid = data.Userid;
scope.session.Rev = data.Rev;
// Update status.
if (scope.status && scope.status.Rev >= data.Rev) {
console.warn("Received old status update in status", data.Rev, scope.status.Rev);
} else {
scope.status = data.Status;
this.updateBuddyPicture(scope.status);
var displayName = scope.displayName;
if (scope.status.displayName) {
scope.displayName = scope.status.displayName;
} else {
scope.displayName = null;
}
if (displayName !== scope.displayName) {
var before = this.tree.update(id, scope);
this.queue.push(["status", id, before]);
}
scope.$apply();
}
};
Buddylist.prototype.onJoined = function(data) {
//console.log("Joined", data);
var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
// Create session.
scope.session = {
Id: data.Id,
Userid: data.Userid,
Ua: data.Ua,
Rev: 0
};
// Add status.
buddyCount++;
if (data.Status) {
if (scope.status && scope.status.Rev >= data.Status.Rev) {
console.warn("Received old status update in join", data.Status.Rev, scope.status.Rev);
} else {
scope.status = data.Status;
scope.displayName = scope.status.displayName;
this.updateBuddyPicture(scope.status);
}
}
//console.log("Joined scope", scope, scope.element);
if (!scope.element) {
var before = this.tree.add(id, scope);
this.queue.push(["joined", id, before]);
this.playSoundJoined = true;
}
};
Buddylist.prototype.onLeft = function(data) {
//console.log("Left", session);
var id = data.Id;
this.tree.remove(id);
var scope = buddyData.get(id);
if (!scope) {
//console.warn("Trying to remove buddy with no registered scope", session);
return;
}
if (buddyCount>0) {
buddyCount--;
}
if (scope.element) {
this.lefts[id] = scope.element;
this.playSoundLeft = true;
}
buddyData.del(id);
delete this.actionElements[id];
};
Buddylist.prototype.onClosed = function() {
//console.log("Closed");
this.$element.empty();
buddyCount=0;
buddyData.clear();
this.tree.clear();
this.actionElements = {};
this.queue = [];
};
Buddylist.prototype.hover = function(buddyElement, hover, id) {
//console.log("hover handler", event, hover, id);
var buddy = $(buddyElement);
var actionElements = this.actionElements;
var elem;
if (!hover) {
buddy.removeClass("hovered");
setTimeout(_.bind(function() {
if (!buddy.hasClass("hovered")) {
elem = actionElements[id];
if (elem) {
delete actionElements[id];
elem.remove();
//console.log("cleaned up actions", id);
}
}
}, this), 1000);
} else {
elem = actionElements[id];
if (elem) {
buddy.addClass("hovered");
} else {
var scope = buddyData.get(id);
var template = buddyActions;
if (scope.status.isMixer) {
template = buddyActionsForAudioMixer;
}
//console.log("scope", scope, id);
template(scope, _.bind(function(clonedElement, $scope) {
actionElements[id] = clonedElement;
buddy.append(clonedElement);
_.defer(function() {
buddy.addClass("hovered");
});
}, this));
scope.$apply();
}
}
};
return {
buddylist: function($element, $scope, opts) {
return new Buddylist($element, $scope, opts);
}
}
}];
};
BuddyTree.prototype._add = function(id, data) {
if (this.tree.add(data)) {
this.data[id] = data;
}
};
BuddyTree.prototype.remove = function(id) {
if (this.data.hasOwnProperty(id)) {
this.tree.remove(this.data[id]);
delete this.data[id];
}
};
/**
* Returns undefined when no change required. Position result otherwise.
*/
BuddyTree.prototype.update = function(id, scope) {
var current = this.data[id];
if (!current) {
return this.add(id, scope);
}
var data = this.create(id, scope);
if (data.sort !== current.sort) {
this.tree.remove(current);
this._add(id, data);
return this.position(id);
}
return undefined;
};
/**
* Returns null when end position. Id of element to insert before otherwise.
*/
BuddyTree.prototype.position = function(id) {
var result = null;
this.tree.inOrderTraverse(function(c) {
if (c.id !== id) {
result = c.id;
return true;
}
}, this.data[id]);
return result;
};
BuddyTree.prototype.clear = function() {
this.tree.clear();
this.data = {};
};
// buddyList
return ["$window", "$compile", "playSound", "buddyData", "fastScroll", "mediaStream", function($window, $compile, playSound, buddyData, fastScroll, mediaStream) {
var requestAnimationFrame = $window.requestAnimationFrame;
var buddyTemplate = $compile(templateBuddy);
var buddyActions = $compile(templateBuddyActions);
var buddyActionsForAudioMixer = $compile(templateBuddyActionsForAudioMixer);
//console.log("$buddylist $get");
var doc = $window.document;
var buddyCount = 0;
var Buddylist = function($element, $scope, opts) {
this.$scope = $scope;
this.$element = $element.find(".buddycontainer > div");
this.options = angular.extend({}, opts);
this.$element.empty();
this.actionElements = {};
this.tree = new BuddyTree();
this.queue = [];
this.lefts = {};
this.playSoundLeft = false;
this.playSoundJoined = false;
fastScroll.apply($element, this.$element);
$element.on("mouseenter mouseleave", ".buddy", _.bind(function(event) {
// Hover handler for on Buddy actions.
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().session.Id);
}, this));
$element.on("click", ".buddy", _.bind(function(event) {
var buddyElement = $(event.currentTarget);
buddyElement.scope().doDefault();
}, this));
$element.attr("data-xthreshold", "10");
$element.on("swipeleft", ".buddy", _.bind(function(event) {
event.preventDefault();
var buddyElement = $(event.currentTarget);
this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().session.Id);
}, this));
$window.setInterval(_.bind(this.soundLoop, this), 500);
var update = _.bind(function refreshBuddies() {
this.refreshBuddies();
requestAnimationFrame(update);
}, this);
requestAnimationFrame(update);
};
Buddylist.prototype.addBuddyElementToScope = function(scope, before, container) {
if (!container) {
container = this.$element[0];
}
buddyTemplate(scope, function($clonedElement, $scope) {
//console.log("create", $scope.displayName, before)
if (before) {
// Insert directly before another node.
var beforeScope = buddyData.get(before);
if (beforeScope && beforeScope.element) {
container.insertBefore($clonedElement[0], beforeScope.element[0]);
} else {
// Append to end assuming before element was removed. Can this happen?
container.appendChild($clonedElement[0]);
}
} else {
// Append to end.
container.appendChild($clonedElement[0]);
}
$scope.element = $clonedElement;
});
};
Buddylist.prototype.onBuddyScope = function(scope) {
scope.element = null;
scope.doDefault = function() {
var id = scope.session.Id;
if (scope.status.isMixer) {
return scope.doAudioConference(id);
}
return scope.doCall(id);
};
scope.$on("$destroy", function() {
scope.element = null;
scope.killed = true;
});
};
Buddylist.prototype.soundLoop = function() {
if (this.playSoundLeft) {
playSound.play("left");
this.playSoundLeft = false;
}
if (this.playSoundJoined) {
playSound.play("joined");
this.playSoundJoined = false;
}
};
Buddylist.prototype.refreshBuddies = function() {
//console.log("processing", this.queue.length);
var processed = 0;
var entry;
var scope;
var id;
var before;
var action;
var not_exists;
// Cleanup lefts.
var lefts = this.lefts;
if (!_.isEmpty(lefts)) {
_.each(lefts, function(element, k) {
if (element) {
element.remove();
}
});
this.lefts = {};
}
var refresh = false;
var queue = this.queue;
if (queue.length) {
//var $element = this.$element;
var container = this.$element[0];
while (true) {
entry = queue.shift();
if (!entry) {
// Queue empty.
break;
}
id = entry[1];
scope = null;
if (id) {
scope = buddyData.lookup(id, false);
if (!scope || scope.killed) {
continue;
}
}
action = entry[0];
before = entry[2];
not_exists = scope.element ? false : true;
if (not_exists) {
this.addBuddyElementToScope(scope, before, container);
} else {
if (typeof(before) === "undefined") {
// No action when undefined.
} else if (before) {
// Move directly before another node.
var beforeScope = buddyData.get(before);
if (beforeScope && beforeScope.element) {
container.insertBefore(scope.element[0], beforeScope.element[0]);
} else {
// Move to end, assuming before element was removed.
container.appendChild(scope.element[0]);
}
} else {
// Move to end.
container.appendChild(scope.element[0]);
}
}
processed++;
refresh = true;
if (processed > 10) {
break;
}
}
}
scope = this.$scope;
var apply = refresh;
var loading = scope.loading;
if (refresh) {
if (!loading) {
scope.loading = true;
}
} else {
if (scope.loading) {
scope.loading = false;
apply = true;
}
}
var empty = buddyCount === 0;
if (empty != scope.empty) {
scope.empty = empty;
apply = true;
}
if (apply) {
scope.$apply();
}
};
Buddylist.prototype.updateBuddyPicture = function(status) {
url = status.buddyPicture;
if (!url) {
return;
}
if (url.indexOf("img:") === 0) {
status.buddyPicture = status.buddyPictureLocalUrl = mediaStream.url.buddy(url.substr(4));
}
};
Buddylist.prototype.onStatus = function(data) {
//console.log("onStatus", status);
var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
// Update session.
scope.session.Userid = data.Userid;
scope.session.Rev = data.Rev;
// Update status.
if (scope.status && scope.status.Rev >= data.Rev) {
console.warn("Received old status update in status", data.Rev, scope.status.Rev);
} else {
scope.status = data.Status;
this.updateBuddyPicture(scope.status);
var displayName = scope.displayName;
if (scope.status.displayName) {
scope.displayName = scope.status.displayName;
} else {
scope.displayName = null;
}
if (displayName !== scope.displayName) {
var before = this.tree.update(id, scope);
this.queue.push(["status", id, before]);
}
scope.$apply();
}
};
Buddylist.prototype.onJoined = function(data) {
//console.log("Joined", data);
var id = data.Id;
var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
// Create session.
scope.session = {
Id: data.Id,
Userid: data.Userid,
Ua: data.Ua,
Rev: 0
};
// Add status.
buddyCount++;
if (data.Status) {
if (scope.status && scope.status.Rev >= data.Status.Rev) {
console.warn("Received old status update in join", data.Status.Rev, scope.status.Rev);
} else {
scope.status = data.Status;
scope.displayName = scope.status.displayName;
this.updateBuddyPicture(scope.status);
}
}
//console.log("Joined scope", scope, scope.element);
if (!scope.element) {
var before = this.tree.add(id, scope);
this.queue.push(["joined", id, before]);
this.playSoundJoined = true;
}
};
Buddylist.prototype.onLeft = function(data) {
//console.log("Left", session);
var id = data.Id;
this.tree.remove(id);
var scope = buddyData.get(id);
if (!scope) {
//console.warn("Trying to remove buddy with no registered scope", session);
return;
}
if (buddyCount > 0) {
buddyCount--;
}
if (scope.element) {
this.lefts[id] = scope.element;
this.playSoundLeft = true;
}
buddyData.del(id);
delete this.actionElements[id];
};
Buddylist.prototype.onClosed = function() {
//console.log("Closed");
this.$element.empty();
buddyCount = 0;
buddyData.clear();
this.tree.clear();
this.actionElements = {};
this.queue = [];
};
Buddylist.prototype.hover = function(buddyElement, hover, id) {
//console.log("hover handler", event, hover, id);
var buddy = $(buddyElement);
var actionElements = this.actionElements;
var elem;
if (!hover) {
buddy.removeClass("hovered");
setTimeout(_.bind(function() {
if (!buddy.hasClass("hovered")) {
elem = actionElements[id];
if (elem) {
delete actionElements[id];
elem.remove();
//console.log("cleaned up actions", id);
}
}
}, this), 1000);
} else {
elem = actionElements[id];
if (elem) {
buddy.addClass("hovered");
} else {
var scope = buddyData.get(id);
var template = buddyActions;
if (scope.status.isMixer) {
template = buddyActionsForAudioMixer;
}
//console.log("scope", scope, id);
template(scope, _.bind(function(clonedElement, $scope) {
actionElements[id] = clonedElement;
buddy.append(clonedElement);
_.defer(function() {
buddy.addClass("hovered");
});
}, this));
scope.$apply();
}
}
};
return {
buddylist: function($element, $scope, opts) {
return new Buddylist($element, $scope, opts);
}
}
}];
});

120
static/js/services/desktopnotify.js

@ -22,88 +22,88 @@ define(['jquery', 'underscore', 'desktop-notify'], function($, _) { @@ -22,88 +22,88 @@ define(['jquery', 'underscore', 'desktop-notify'], function($, _) {
return ["$window", function($window) {
var helper = notify;
var helper = notify;
var desktopNotify = function() {
var desktopNotify = function() {
this.asked = false;
this.windowHasFocus = true;
this.dummy = {
close: function() {}
};
this.refresh();
this.enabled();
this.asked = false;
this.windowHasFocus = true;
this.dummy = {
close: function() {}
};
this.refresh();
this.enabled();
$($window).on("focus blur", _.bind(function(event) {
this.windowHasFocus = event.type === "focus" ? true : false;
}, this));
$($window).on("focus blur", _.bind(function(event) {
this.windowHasFocus = event.type === "focus" ? true : false;
}, this));
};
};
desktopNotify.prototype.enabled = function() {
desktopNotify.prototype.enabled = function() {
if (this.level === "default") {
this.asked = true;
this.requestPermission();
}
return (this.supported && this.level === "granted") ? true : false;
if (this.level === "default") {
this.asked = true;
this.requestPermission();
}
return (this.supported && this.level === "granted") ? true : false;
};
};
desktopNotify.prototype.refresh = function() {
desktopNotify.prototype.refresh = function() {
this.supported = helper.isSupported;
this.level = helper.permissionLevel();
this.supported = helper.isSupported;
this.level = helper.permissionLevel();
};
};
desktopNotify.prototype.requestPermission = function(cb) {
desktopNotify.prototype.requestPermission = function(cb) {
//console.log("request permission");
return helper.requestPermission(_.bind(function() {
//console.log("request permission");
return helper.requestPermission(_.bind(function() {
//console.log("requestPermission result", arguments);
this.refresh();
if (cb) {
cb.apply(helper, arguments);
}
//console.log("requestPermission result", arguments);
this.refresh();
if (cb) {
cb.apply(helper, arguments);
}
}, this));
}, this));
};
};
desktopNotify.prototype.createNotification = function(title, options) {
desktopNotify.prototype.createNotification = function(title, options) {
return helper.createNotification(title, options);
return helper.createNotification(title, options);
};
};
desktopNotify.prototype.notify = function(title, body, options) {
desktopNotify.prototype.notify = function(title, body, options) {
if (!this.enabled()) {
return this.dummy;
};
if (!this.enabled()) {
return this.dummy;
};
var opts = {
body: body,
icon: "static/img/notify.ico",
timeout: 7000
}
$.extend(opts, options);
var timeout = opts.timeout;
delete opts.timeout;
var n = this.createNotification(title, opts);
if (timeout) {
$window.setTimeout(function() {
n.close();
}, timeout);
}
return n;
var opts = {
body: body,
icon: "static/img/notify.ico",
timeout: 7000
}
$.extend(opts, options);
var timeout = opts.timeout;
delete opts.timeout;
var n = this.createNotification(title, opts);
if (timeout) {
$window.setTimeout(function() {
n.close();
}, timeout);
}
return n;
};
};
return new desktopNotify();
return new desktopNotify();
}];
}];
});
});

46
static/js/services/enrichmessage.js

@ -20,30 +20,30 @@ @@ -20,30 +20,30 @@
*/
define([], function() {
// enrichMessage
return ["$filter", function($filter) {
// enrichMessage
return ["$filter", function($filter) {
var linky = $filter("linky");
var enrichMessage = {
url: function(s) {
s = linky(s);
s = s.replace(/<a/g, '<a rel="external"');
return s;
},
multiline: function(s) {
s = s.replace(/\r\n/g, "<br/>");
s = s.replace(/\n/g, "<br/>");
s = s.replace(/&#10;/g, "<br/>"); // Also supported quoted newlines.
return s;
},
all: function(s) {
s = enrichMessage.url(s);
s = enrichMessage.multiline(s);
return s;
}
};
return enrichMessage;
var linky = $filter("linky");
var enrichMessage = {
url: function(s) {
s = linky(s);
s = s.replace(/<a/g, '<a rel="external"');
return s;
},
multiline: function(s) {
s = s.replace(/\r\n/g, "<br/>");
s = s.replace(/\n/g, "<br/>");
s = s.replace(/&#10;/g, "<br/>"); // Also supported quoted newlines.
return s;
},
all: function(s) {
s = enrichMessage.url(s);
s = enrichMessage.multiline(s);
return s;
}
};
return enrichMessage;
}];
}];
});

137
static/js/services/fastscroll.js

@ -43,83 +43,84 @@ @@ -43,83 +43,84 @@
*/
define(["jquery"], function($) {
function dispatchClick(coords) {
var event = document.createEvent('MouseEvent'),
elem = document.elementFromPoint(coords.x, coords.y);
function dispatchClick(coords) {
var event = document.createEvent('MouseEvent'),
elem = document.elementFromPoint(coords.x, coords.y);
event.initMouseEvent(
'click',
true /* bubble */, true /* cancelable */,
window, null,
coords.x, coords.y, 0, 0, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left*/, null
);
event.synthetic = true;
event.initMouseEvent(
'click',
true /* bubble */ , true /* cancelable */ ,
window, null,
coords.x, coords.y, 0, 0, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left*/ , null);
event.synthetic = true;
elem.dispatchEvent(event);
};
elem.dispatchEvent(event);
};
// fastScroll
return ["$window", function($window) {
// fastScroll
return ["$window", function($window) {
var fastScroll = {
apply: function(scroller, container) {
var fastScroll = {
apply: function(scroller, container) {
if (!container) {
container = scroller;
}
if (!container) {
container = scroller;
}
var cover = $window.document.createElement('div'),
coverStyle = cover.style,
scrollStarted = false,
timer,
clicked = false,
pos = { x: 0, y: 0 };
var cover = $window.document.createElement('div'),
coverStyle = cover.style,
scrollStarted = false,
timer,
clicked = false,
pos = {
x: 0,
y: 0
};
coverStyle.cssText = [
'-webkit-transform: translate3d(0,0,0);',
'transform: translate3d(0,0,0);',
'position: absolute;',
'top: 0;',
'right: 0;',
'left: 0;',
'bottom: 0;',
'opacity: 0;',
'z-index: 9;',
'pointer-events: none'
].join('');
container.append(cover);
coverStyle.cssText = [
'-webkit-transform: translate3d(0,0,0);',
'transform: translate3d(0,0,0);',
'position: absolute;',
'top: 0;',
'right: 0;',
'left: 0;',
'bottom: 0;',
'opacity: 0;',
'z-index: 9;',
'pointer-events: none'].join('');
container.append(cover);
scroller.on("scroll", function scroll() {
if(!scrollStarted) {
coverStyle.pointerEvents = 'auto';
scrollStarted = true;
}
$window.clearTimeout(timer);
timer = $window.setTimeout(function(){
coverStyle.pointerEvents = 'none';
scrollStarted = false;
if(clicked) {
dispatchClick(pos);
clicked = false;
}
}, 500);
});
scroller.on("scroll", function scroll() {
if (!scrollStarted) {
coverStyle.pointerEvents = 'auto';
scrollStarted = true;
}
$window.clearTimeout(timer);
timer = $window.setTimeout(function() {
coverStyle.pointerEvents = 'none';
scrollStarted = false;
if (clicked) {
dispatchClick(pos);
clicked = false;
}
}, 500);
});
// Capture all clicks and store x, y coords for later.
$(cover).on('click', function clickCatcher(event) {
if(event.target === cover && !event.synthetic) {
pos.x = event.clientX;
pos.y = event.clientY;
clicked = true;
}
});
// Capture all clicks and store x, y coords for later.
$(cover).on('click', function clickCatcher(event) {
if (event.target === cover && !event.synthetic) {
pos.x = event.clientX;
pos.y = event.clientY;
clicked = true;
}
});
}
}
return fastScroll;
}
}
return fastScroll;
}];
}];
});
});

29
static/js/services/filedata.js

@ -51,7 +51,9 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -51,7 +51,9 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
return d[1];
});
// Create.
var data = new Blob(dataBuffer, {type: this.owner.info.type||"application/octet-stream"});
var data = new Blob(dataBuffer, {
type: this.owner.info.type || "application/octet-stream"
});
this.file = {
toURL: function() {
return URL.createObjectURL(data);
@ -86,13 +88,16 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -86,13 +88,16 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
};
FileWriterFileSystem.prototype.supported = !!requestFileSystem;
FileWriterFileSystem.prototype.supported = !! requestFileSystem;
FileWriterFileSystem.prototype.filesystem = null;
FileWriterFileSystem.prototype.create = function() {
var generator = _.bind(function() {
FileWriterFileSystem.prototype.filesystem.root.getFile(this.owner.id, {create: true, exclusive: true}, _.bind(function(fileEntry) {
FileWriterFileSystem.prototype.filesystem.root.getFile(this.owner.id, {
create: true,
exclusive: true
}, _.bind(function(fileEntry) {
console.log("Generate file", this.owner, fileEntry);
this.setup(fileEntry);
this.flush();
@ -117,7 +122,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -117,7 +122,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
if (!size) {
// Default size.
size = 5*1024*1024;
size = 5 * 1024 * 1024;
}
requestFileSystem(window.TEMPORARY, size, _.bind(function(fs) {
// Success call.
@ -150,7 +155,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -150,7 +155,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
}
}, this);
fileWriter.onwriteend = _.bind(function(e) {
this.written+=this.writing_written;
this.written += this.writing_written;
this.writing_written = 0;
this.writing = false;
//console.log("Done file writing.", e, fileWriter.position, fileWriter.length);
@ -199,7 +204,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -199,7 +204,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
var d;
var s;
var tmp
while(true) {
while (true) {
// Grab next item.
next = writeBuffer.shift();
@ -213,7 +218,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -213,7 +218,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
}
}
if (pos!==s || !next) {
if (pos !== s || !next) {
// We have a gap or nothing more to write, seek and write the shit.
//console.log("eyikes gap or finished", s, pos, next, this.writer.position, tmp.length);
@ -225,7 +230,9 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -225,7 +230,9 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
// Write to file system.
this.writing_written = tmp.length;
this.writer.write(new Blob(tmp, {type: this.owner.info.type||"application/octet-stream"}));
this.writer.write(new Blob(tmp, {
type: this.owner.info.type || "application/octet-stream"
}));
tmp = null;
if (next) {
@ -298,10 +305,10 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -298,10 +305,10 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
var f = this.file;
var size = f.size;
var i;
for (i=0; i < size; i+=fileChunkSize) {
for (i = 0; i < size; i += fileChunkSize) {
(function(file, position) {
var reader = new FileReader();
var blob = file.slice(position, position+fileChunkSize);
var blob = file.slice(position, position + fileChunkSize);
reader.onload = function(event) {
if (reader.readyState == FileReader.DONE) {
cb(event.target.result);
@ -411,7 +418,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA) @@ -411,7 +418,7 @@ define(["jquery", "underscore", "sha", "webrtc.adapter"], function($, _, jsSHA)
// public API.
var fileData = {
generateFile: function(id, info) {
id = filenamePrefix+id;
id = filenamePrefix + id;
var file = filesTable[id] = new File(id, null, info);
file.createWriter();
return file;

12
static/js/services/filedownload.js

@ -61,7 +61,9 @@ define(["jquery", "underscore"], function($, _) { @@ -61,7 +61,9 @@ define(["jquery", "underscore"], function($, _) {
_.each(this.xfer_all, function(xfer) {
// Implement own clean up message.
// NOTE(longsleep): See https://code.google.com/p/webrtc/issues/detail?id=1676 for reason.
xfer.send({m: "bye"});
xfer.send({
m: "bye"
});
$timeout(function() {
xfer.cancel();
}, 0);
@ -82,7 +84,7 @@ define(["jquery", "underscore"], function($, _) { @@ -82,7 +84,7 @@ define(["jquery", "underscore"], function($, _) {
this.running = true;
// Make file.
var file = this.file = fileData.generateFile(""+(downloads++)+".file", this.scope.info);
var file = this.file = fileData.generateFile("" + (downloads++) + ".file", this.scope.info);
// Bind file events.
file.e.bind("complete", _.bind(function() {
safeApply(this.scope, function($scope) {
@ -122,7 +124,7 @@ define(["jquery", "underscore"], function($, _) { @@ -122,7 +124,7 @@ define(["jquery", "underscore"], function($, _) {
this.interval = $window.setInterval(_.bind(function() {
//console.log("download session running", this.xfer_all.length, this.concurrent, this.chunk, this.scope.info.chunks, this.scope.info.chunks / this.fragments);
this.process();
if (this.running && this.xfer_all.length < this.concurrent && this.chunk <= this.end && (this.end+1) / this.fragments >= this.xfer_all.length-2) {
if (this.running && this.xfer_all.length < this.concurrent && this.chunk <= this.end && (this.end + 1) / this.fragments >= this.xfer_all.length - 2) {
// Start more if file is large enough (about 10 MB).
this.addXfer(this.known_peer_id);
}
@ -156,7 +158,7 @@ define(["jquery", "underscore"], function($, _) { @@ -156,7 +158,7 @@ define(["jquery", "underscore"], function($, _) {
if (job.end > this.end) {
job.end = this.end;
}
this.chunk = job.end+1;
this.chunk = job.end + 1;
job.next = _.bind(function() {
if (this.stop) {
this.next = null;
@ -331,4 +333,4 @@ define(["jquery", "underscore"], function($, _) { @@ -331,4 +333,4 @@ define(["jquery", "underscore"], function($, _) {
}];
});
});

40
static/js/services/filetransfer.js

@ -18,25 +18,25 @@ @@ -18,25 +18,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(["mediastream/webrtc", "webrtc.adapter"], function() {
define(["mediastream/webrtc", "webrtc.adapter"], function() {
// Simple array buffer join function, to create a third array buffer
// NOTE(longsleep): This acutally copies the data in memory.
var arrayBufferJoin = function(one, two) {
var three = new Uint8Array(one.byteLength+two.byteLength);
var three = new Uint8Array(one.byteLength + two.byteLength);
three.set(new Uint8Array(one), 0);
three.set(new Uint8Array(two), one.byteLength);
return three.buffer;
}
// Simple fast crc32 implementation for ArrayBuffers.
var makeCRCTable = function(){
var makeCRCTable = function() {
var c;
var crcTable = [];
for(var n =0; n < 256; n++){
for (var n = 0; n < 256; n++) {
c = n;
for(var k =0; k < 8; k++){
c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
for (var k = 0; k < 8; k++) {
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
crcTable[n] = c;
}
@ -46,7 +46,7 @@ @@ -46,7 +46,7 @@
var crc32 = function(view) {
// Pass a Uint8Array.
var crc = 0 ^ (-1);
for (var i = 0; i < view.length; i++ ) {
for (var i = 0; i < view.length; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ view[i]) & 0xFF];
}
return (crc ^ (-1)) >>> 0;
@ -72,7 +72,7 @@ @@ -72,7 +72,7 @@
this._c = new Uint32Array(buffer, 4, 1);
this._crc32 = new Uint32Array(buffer, 8, 1);
this._buffer = buffer;
this._data = new Uint8Array(this._buffer, 12, this._buffer.byteLength-12);
this._data = new Uint8Array(this._buffer, 12, this._buffer.byteLength - 12);
if (!create) {
// Validate.
if (this._v[0] !== this.version) {
@ -118,9 +118,7 @@ @@ -118,9 +118,7 @@
// SCTP does not work properly in Chrome 31 when used with other Chrome versions.
// Thus we require Chrome 32 for now. Firefox 28 can interop with Chrome 32 data channels - yay.
var supported =
($window.webrtcDetectedBrowser === "chrome" && $window.webrtcDetectedVersion >= 32 && !$window.webrtcDetectedAndroid) ||
($window.webrtcDetectedBrowser === "firefox" && $window.webrtcDetectedVersion >= 28 && !$window.webrtcDetectedAndroid);
var supported = ($window.webrtcDetectedBrowser === "chrome" && $window.webrtcDetectedVersion >= 32 && !$window.webrtcDetectedAndroid) || ($window.webrtcDetectedBrowser === "firefox" && $window.webrtcDetectedVersion >= 28 && !$window.webrtcDetectedAndroid);
if (!supported) {
console.warn("Browser support for binary file transfers not found.");
}
@ -132,15 +130,15 @@ @@ -132,15 +130,15 @@
parseChunk: function(data, cb) {
var version = new Uint8Array(data, 0, 1)[0];
switch (version) {
case 0:
var filechunk = new FileChunkV0(data);
if (cb) {
cb(filechunk.index(), filechunk.data(), filechunk.size());
}
return filechunk;
default:
console.warn("Unknow data version.");
break;
case 0:
var filechunk = new FileChunkV0(data);
if (cb) {
cb(filechunk.index(), filechunk.data(), filechunk.size());
}
return filechunk;
default:
console.warn("Unknow data version.");
break;
}
},
makeChunk: function(idx, data) {
@ -150,4 +148,4 @@ @@ -150,4 +148,4 @@
}];
});
});

54
static/js/services/fileupload.js

@ -59,7 +59,7 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) { @@ -59,7 +59,7 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) {
var msg;
try {
msg = JSON.parse(data);
} catch(e) {
} catch (e) {
// Invalid JSON.
console.warn("Invalid JSON received from file download request.", data);
xfer.cancel();
@ -97,30 +97,30 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) { @@ -97,30 +97,30 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) {
var info = this.scope.info;
switch (msg.m) {
case "r":
// Request chunk.
//console.debug("Request a chunk", msg, xfer.id);
var chunk = parseInt(msg.i || 0, 10);
var file = fileData.getFile(info.id);
file.getChunk(chunk, _.bind(function(data) {
//console.log("Sending chunk", chunk, data.byteLength);
if (!this.running) {
return;
}
var filechunk = fileTransfer.makeChunk(chunk, data);
xfer.send(filechunk.raw());
safeApply(this.scope, function($scope) {
$scope.$emit("uploadedChunk", chunk, data.byteLength);
});
}, this));
break;
case "bye":
// Close this xfer.
xfer.cancel();
break;
default:
console.log("Unknown xfer control request", msg.m, msg);
break;
case "r":
// Request chunk.
//console.debug("Request a chunk", msg, xfer.id);
var chunk = parseInt(msg.i || 0, 10);
var file = fileData.getFile(info.id);
file.getChunk(chunk, _.bind(function(data) {
//console.log("Sending chunk", chunk, data.byteLength);
if (!this.running) {
return;
}
var filechunk = fileTransfer.makeChunk(chunk, data);
xfer.send(filechunk.raw());
safeApply(this.scope, function($scope) {
$scope.$emit("uploadedChunk", chunk, data.byteLength);
});
}, this));
break;
case "bye":
// Close this xfer.
xfer.cancel();
break;
default:
console.log("Unknown xfer control request", msg.m, msg);
break;
}
};
@ -155,7 +155,7 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) { @@ -155,7 +155,7 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) {
var dataTransfer = event.originalEvent.dataTransfer;
var files = [];
var i;
for (i = 0; i<dataTransfer.files.length; i++) {
for (i = 0; i < dataTransfer.files.length; i++) {
files.push(fileData.createFile(binder.namespace(), dataTransfer.files[i]));
}
//console.log("drop event", dataTransfer, files, files.length);
@ -192,4 +192,4 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) { @@ -192,4 +192,4 @@ define(["jquery", "underscore", "webrtc.adapter"], function($, _) {
}];
});
});

169
static/js/services/mediasources.js

@ -20,88 +20,91 @@ @@ -20,88 +20,91 @@
*/
define(['jquery', 'underscore'], function($, _) {
return ["$window", function($window) {
var mediaSources = function() {
this.supported = window.MediaStreamTrack && window.MediaStreamTrack.getSources
this.audio = [];
this.video = [];
};
mediaSources.prototype.refresh = function(cb) {
if (!this.supported) {
if (cb) {
cb([], []);
}
return;
}
// NOTE(longsleep): Put this in a try/catch to continue with
// broken implementation like in node-webkit 0.7.2.
try {
this._refresh(cb);
} catch(e) {
console.error("Failed to get media sources: "+e.message);
this.supported = false;
if (cb) {
cb([], []);
}
}
};
mediaSources.prototype._refresh = function(cb) {
MediaStreamTrack.getSources(_.bind(function(sources) {
var audio = this.audio = [];
var video = this.video = [];
_.each(sources, function(source) {
var o = {id: source.id, facing: source.facing};
if (source.kind === "audio") {
o.label = source.label ? source.label : "Microphone " + (audio.length+1);
audio.push(o);
} else if (source.kind === "video") {
o.label = source.label ? source.label : "Camera " + (video.length+1);
video.push(o);
}
});
if (cb) {
cb(audio, video);
}
}, this));
};
mediaSources.prototype.hasAudioId = function(id) {
var i;
for (i=0; i<this.audio.length; i++) {
if (this.audio[i].id === id) {
return true;
}
}
return false;
};
mediaSources.prototype.hasVideoId = function(id) {
var i;
for (i=0; i<this.video.length; i++) {
if (this.video[i].id === id) {
return true;
}
}
return false;
};
return new mediaSources();
}];
return ["$window", function($window) {
var mediaSources = function() {
this.supported = window.MediaStreamTrack && window.MediaStreamTrack.getSources
this.audio = [];
this.video = [];
};
mediaSources.prototype.refresh = function(cb) {
if (!this.supported) {
if (cb) {
cb([], []);
}
return;
}
// NOTE(longsleep): Put this in a try/catch to continue with
// broken implementation like in node-webkit 0.7.2.
try {
this._refresh(cb);
} catch (e) {
console.error("Failed to get media sources: " + e.message);
this.supported = false;
if (cb) {
cb([], []);
}
}
};
mediaSources.prototype._refresh = function(cb) {
MediaStreamTrack.getSources(_.bind(function(sources) {
var audio = this.audio = [];
var video = this.video = [];
_.each(sources, function(source) {
var o = {
id: source.id,
facing: source.facing
};
if (source.kind === "audio") {
o.label = source.label ? source.label : "Microphone " + (audio.length + 1);
audio.push(o);
} else if (source.kind === "video") {
o.label = source.label ? source.label : "Camera " + (video.length + 1);
video.push(o);
}
});
if (cb) {
cb(audio, video);
}
}, this));
};
mediaSources.prototype.hasAudioId = function(id) {
var i;
for (i = 0; i < this.audio.length; i++) {
if (this.audio[i].id === id) {
return true;
}
}
return false;
};
mediaSources.prototype.hasVideoId = function(id) {
var i;
for (i = 0; i < this.video.length; i++) {
if (this.video[i].id === id) {
return true;
}
}
return false;
};
return new mediaSources();
}];
});

616
static/js/services/mediastream.js

@ -19,333 +19,339 @@ @@ -19,333 +19,339 @@
*
*/
define([
'jquery',
'underscore',
'ua-parser',
'mediastream/connector',
'mediastream/api',
'mediastream/webrtc',
'mediastream/tokens'
'jquery',
'underscore',
'ua-parser',
'mediastream/connector',
'mediastream/api',
'mediastream/webrtc',
'mediastream/tokens'
], function($, _, uaparser, Connector, Api, WebRTC, tokens) {
return ["globalContext", "$route", "$location", "$window", "visibility", "alertify", "$http", "safeApply", "$timeout", "$sce", function(context, $route, $location, $window, visibility, alertify, $http, safeApply, $timeout, $sce) {
return ["globalContext", "$route", "$location", "$window", "visibility", "alertify", "$http", "safeApply", "$timeout", "$sce", function(context, $route, $location, $window, visibility, alertify, $http, safeApply, $timeout, $sce) {
var url = (context.Ssl ? "wss" : "ws") + "://" + context.Host + (context.Cfg.B || "/") + "ws";
var version = context.Cfg.Version || "unknown";
console.log("Service version: "+version);
console.log("Ws URL: "+ url);
console.log("Secure Contextual Escaping: "+$sce.isEnabled());
var url = (context.Ssl ? "wss" : "ws") + "://" + context.Host + (context.Cfg.B || "/") + "ws";
var version = context.Cfg.Version || "unknown";
console.log("Service version: " + version);
console.log("Ws URL: " + url);
console.log("Secure Contextual Escaping: " + $sce.isEnabled());
var connector = new Connector(version);
var api = new Api(connector);
var webrtc = new WebRTC(api);
var connector = new Connector(version);
var api = new Api(connector);
var webrtc = new WebRTC(api);
// Create encryption key from server token and browser name.
var secureKey = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(context.Cfg.Token + uaparser().browser.name));
// Create encryption key from server token and browser name.
var secureKey = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(context.Cfg.Token + uaparser().browser.name));
var mediaStream = {
version: version,
ws: url,
config: context.Cfg,
webrtc: webrtc,
connector: connector,
api: api,
tokens: tokens,
url: {
room: function(id) {
id = $window.encodeURIComponent(id);
return $window.location.protocol+'//'+$window.location.host+context.Cfg.B+id;
},
buddy: function(id) {
return $window.location.protocol+'//'+$window.location.host+context.Cfg.B+"static/img/buddy/s46/"+id;
},
api: function(path) {
return (context.Cfg.B || "/") + "api/v1/" + path;
}
},
users: {
register: function(form, success_cb, error_cb) {
var url = mediaStream.url.api("users");
if (form) {
// Form submit mode.
$(form).attr("action", url).attr("method", "POST");
var idE = $('<input name="id" type="hidden">');
idE.val(mediaStream.api.id);
var sidE = $('<input name="sid" type="hidden">');
sidE.val(mediaStream.api.sid);
$(form).append(idE);
$(form).append(sidE);
var iframe = $(form).find("iframe");
form.submit();
$timeout(function() {
idE.remove();
sidE.remove();
idE=null;
sidE=null;
}, 0);
var retries = 0;
var authorize = function() {
mediaStream.users.authorize({
count: retries
}, success_cb, function(data, status) {
// Error handler retry.
retries++;
if (retries <= 10) {
$timeout(authorize, 2000);
} else {
console.error("Failed to authorize session", status, data);
if (error_cb) {
error_cb(data, status)
}
}
});
};
$timeout(authorize, 1500);
} else {
// AJAX mode.
var data = {
id: mediaStream.api.id,
sid: mediaStream.api.sid
}
$http({
method: "POST",
url: url,
data: JSON.stringify(data),
headers: {'Content-Type': 'application/json'}
}).
success(function(data, status) {
if (data.userid !== "" && data.success) {
success_cb(data, status);
} else {
if (error_cb) {
error_cb(data, status);
}
}
}).
error(function(data, status) {
if (error_cb) {
error_cb(data, status)
}
});
}
},
authorize: function(data, success_cb, error_cb) {
var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/";
var login = _.clone(data);
login.id = mediaStream.api.id;
login.sid = mediaStream.api.sid;
$http({
method: "PATCH",
url: url,
data: JSON.stringify(login),
headers: {'Content-Type': 'application/json'}
}).
success(function(data, status) {
if (data.nonce !== "" && data.success) {
success_cb(data, status);
} else {
if (error_cb) {
error_cb(data, status);
}
}
}).
error(function(data, status) {
if (error_cb) {
error_cb(data, status)
}
});
},
store: function(data) {
// So we store the stuff in localStorage for later use.
var store = _.clone(data);
store.v = 42; // No idea what number - so use 42.
var login = sjcl.encrypt(secureKey, JSON.stringify(store));
localStorage.setItem("mediastream-login-"+context.Cfg.UsersMode, login);
return login;
},
load: function() {
// Check if we have something in store.
var login = localStorage.getItem("mediastream-login-"+context.Cfg.UsersMode);
if (login) {
try {
login = sjcl.decrypt(secureKey, login);
login = JSON.parse(login)
} catch(err) {
console.error("Failed to parse stored login data", err);
login = {};
}
switch (login.v) {
case 42:
return login;
default:
console.warn("Unknown stored credentials", login.v);
break;
}
}
return null;
},
forget: function() {
localStorage.removeItem("mediastream-login-"+context.Cfg.UsersMode);
}
},
initialize: function($rootScope, translation) {
var mediaStream = {
version: version,
ws: url,
config: context.Cfg,
webrtc: webrtc,
connector: connector,
api: api,
tokens: tokens,
url: {
room: function(id) {
id = $window.encodeURIComponent(id);
return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + id;
},
buddy: function(id) {
return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + "static/img/buddy/s46/" + id;
},
api: function(path) {
return (context.Cfg.B || "/") + "api/v1/" + path;
}
},
users: {
register: function(form, success_cb, error_cb) {
var url = mediaStream.url.api("users");
if (form) {
// Form submit mode.
$(form).attr("action", url).attr("method", "POST");
var idE = $('<input name="id" type="hidden">');
idE.val(mediaStream.api.id);
var sidE = $('<input name="sid" type="hidden">');
sidE.val(mediaStream.api.sid);
$(form).append(idE);
$(form).append(sidE);
var iframe = $(form).find("iframe");
form.submit();
$timeout(function() {
idE.remove();
sidE.remove();
idE = null;
sidE = null;
}, 0);
var retries = 0;
var authorize = function() {
mediaStream.users.authorize({
count: retries
}, success_cb, function(data, status) {
// Error handler retry.
retries++;
if (retries <= 10) {
$timeout(authorize, 2000);
} else {
console.error("Failed to authorize session", status, data);
if (error_cb) {
error_cb(data, status)
}
}
});
};
$timeout(authorize, 1500);
} else {
// AJAX mode.
var data = {
id: mediaStream.api.id,
sid: mediaStream.api.sid
}
$http({
method: "POST",
url: url,
data: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).
success(function(data, status) {
if (data.userid !== "" && data.success) {
success_cb(data, status);
} else {
if (error_cb) {
error_cb(data, status);
}
}
}).
error(function(data, status) {
if (error_cb) {
error_cb(data, status)
}
});
}
},
authorize: function(data, success_cb, error_cb) {
var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/";
var login = _.clone(data);
login.id = mediaStream.api.id;
login.sid = mediaStream.api.sid;
$http({
method: "PATCH",
url: url,
data: JSON.stringify(login),
headers: {
'Content-Type': 'application/json'
}
}).
success(function(data, status) {
if (data.nonce !== "" && data.success) {
success_cb(data, status);
} else {
if (error_cb) {
error_cb(data, status);
}
}
}).
error(function(data, status) {
if (error_cb) {
error_cb(data, status)
}
});
},
store: function(data) {
// So we store the stuff in localStorage for later use.
var store = _.clone(data);
store.v = 42; // No idea what number - so use 42.
var login = sjcl.encrypt(secureKey, JSON.stringify(store));
localStorage.setItem("mediastream-login-" + context.Cfg.UsersMode, login);
return login;
},
load: function() {
// Check if we have something in store.
var login = localStorage.getItem("mediastream-login-" + context.Cfg.UsersMode);
if (login) {
try {
login = sjcl.decrypt(secureKey, login);
login = JSON.parse(login)
} catch (err) {
console.error("Failed to parse stored login data", err);
login = {};
}
switch (login.v) {
case 42:
return login;
default:
console.warn("Unknown stored credentials", login.v);
break;
}
}
return null;
},
forget: function() {
localStorage.removeItem("mediastream-login-" + context.Cfg.UsersMode);
}
},
initialize: function($rootScope, translation) {
var cont = false;
var ready = false;
var cont = false;
var ready = false;
$rootScope.version = version;
$rootScope.roomid = null;
$rootScope.roomlink = null;
$rootScope.roomstatus = false;
$rootScope.version = version;
$rootScope.roomid = null;
$rootScope.roomlink = null;
$rootScope.roomstatus = false;
var connect = function() {
if (ready && cont) {
// Inject connector function into scope, so that controllers can pick it up.
safeApply($rootScope, function(scope) {
scope.connect = function() {
connector.connect(url);
};
});
}
};
var connect = function() {
if (ready && cont) {
// Inject connector function into scope, so that controllers can pick it up.
safeApply($rootScope, function(scope) {
scope.connect = function() {
connector.connect(url);
};
});
}
};
$window.changeRoom = function(room) {
$rootScope.$apply(function(scope) {
$location.path("/"+room).replace();
});
};
$window.changeRoom = function(room) {
$rootScope.$apply(function(scope) {
$location.path("/" + room).replace();
});
};
var title = (function(e) {
return {
element: e,
text: e.text()
}
}($("title")));
var title = (function(e) {
return {
element: e,
text: e.text()
}
}($("title")));
// Room selector.
$rootScope.$on("$locationChangeSuccess", function(event) {
//console.log("location change", $route, $rootScope.roomid);
var defaultRoom, room;
room = defaultRoom = $rootScope.roomid || "";
if ($route.current) {
room = $route.current.params.room;
} else {
room = "";
}
if (!ready && room !== defaultRoom && !room) {
// First start.
$location.path("/"+defaultRoom).replace();
return;
}
console.info("Selected room is:", [room]);
if (!ready || !cont) {
ready = true;
connector.roomid = room;
connect();
} else {
connector.room(room);
}
$rootScope.roomid = room;
$rootScope.roomlink = room ? mediaStream.url.room(room) : null;
// Room selector.
$rootScope.$on("$locationChangeSuccess", function(event) {
//console.log("location change", $route, $rootScope.roomid);
var defaultRoom, room;
room = defaultRoom = $rootScope.roomid || "";
if ($route.current) {
room = $route.current.params.room;
} else {
room = "";
}
if (!ready && room !== defaultRoom && !room) {
// First start.
$location.path("/" + defaultRoom).replace();
return;
}
console.info("Selected room is:", [room]);
if (!ready || !cont) {
ready = true;
connector.roomid = room;
connect();
} else {
connector.room(room);
}
$rootScope.roomid = room;
$rootScope.roomlink = room ? mediaStream.url.room(room) : null;
if ($rootScope.roomlink) {
title.element.text(room + " - " + title.text);
} else {
title.element.text(title.text);
}
if ($rootScope.roomlink) {
title.element.text(room + " - " + title.text);
} else {
title.element.text(title.text);
}
});
});
// Cache events, to avoid ui flicker during quick room changes.
var roomStatusCache = $rootScope.roomstatus;
var roomCache = null;
var roomCache2 = null;
$rootScope.$on("roomStatus", function(event, status) {
roomStatusCache = status ? true : false;
roomCache = status ? $rootScope.roomid : null;
$timeout(function() {
if ($rootScope.roomstatus !== roomStatusCache) {
$rootScope.roomstatus = roomStatusCache;
}
if (roomCache !== roomCache2) {
$rootScope.$broadcast("room", roomCache);
roomCache2 = roomCache;
}
}, 100);
});
// Cache events, to avoid ui flicker during quick room changes.
var roomStatusCache = $rootScope.roomstatus;
var roomCache = null;
var roomCache2 = null;
$rootScope.$on("roomStatus", function(event, status) {
roomStatusCache = status ? true : false;
roomCache = status ? $rootScope.roomid : null;
$timeout(function() {
if ($rootScope.roomstatus !== roomStatusCache) {
$rootScope.roomstatus = roomStatusCache;
}
if (roomCache !== roomCache2) {
$rootScope.$broadcast("room", roomCache);
roomCache2 = roomCache;
}
}, 100);
});
visibility.afterPrerendering(function() {
visibility.afterPrerendering(function() {
// Hide loader when we are visible.
var loader = $("#loader");
loader.addClass("done");
_.delay(function() {
loader.remove();
},1000);
// Hide loader when we are visible.
var loader = $("#loader");
loader.addClass("done");
_.delay(function() {
loader.remove();
}, 1000);
if (context.Cfg.Tokens) {
var storedCode = localStorage.getItem("mediastream-access-code");
var prompt = function() {
alertify.dialog.prompt(translation._("Access code required"), function(code) {
if (!code) {
prompt();
} else {
check(code);
return;
}
}, prompt);
};
var url = mediaStream.url.api("tokens");
var check = function(code) {
$http({
method: "POST",
url: url,
data: $.param({
a: code
}),
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}).
success(function(data, status) {
if (data.token !== "" && data.success) {
localStorage.setItem("mediastream-access-code", code);
cont = true;
connect();
} else {
alertify.dialog.error(translation._("Access denied"), translation._("Please provide a valid access code."), function() {
prompt();
});
}
}).
error(function(data, status) {
if ((status == 403 || status == 413) && data.success === false) {
alertify.dialog.error(translation._("Access denied"), translation._("Please provide a valid access code."), function() {
prompt();
});
} else {
alertify.dialog.error(translation._("Error"), translation._("Failed to verify access code. Check your Internet connection and try again."), function() {
prompt();
});
}
});
};
if (storedCode) {
check(storedCode);
} else {
prompt();
}
} else {
cont = true;
connect();
}
if (context.Cfg.Tokens) {
var storedCode = localStorage.getItem("mediastream-access-code");
var prompt = function() {
alertify.dialog.prompt(translation._("Access code required"), function(code) {
if (!code) {
prompt();
} else {
check(code);
return;
}
}, prompt);
};
var url = mediaStream.url.api("tokens");
var check = function(code) {
$http({
method: "POST",
url: url,
data: $.param({
a: code
}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).
success(function(data, status) {
if (data.token !== "" && data.success) {
localStorage.setItem("mediastream-access-code", code);
cont = true;
connect();
} else {
alertify.dialog.error(translation._("Access denied"), translation._("Please provide a valid access code."), function() {
prompt();
});
}
}).
error(function(data, status) {
if ((status == 403 || status == 413) && data.success === false) {
alertify.dialog.error(translation._("Access denied"), translation._("Please provide a valid access code."), function() {
prompt();
});
} else {
alertify.dialog.error(translation._("Error"), translation._("Failed to verify access code. Check your Internet connection and try again."), function() {
prompt();
});
}
});
};
if (storedCode) {
check(storedCode);
} else {
prompt();
}
} else {
cont = true;
connect();
}
});
});
}
};
}
};
return mediaStream;
return mediaStream;
}];
}];
});

346
static/js/services/playsound.js

@ -20,178 +20,178 @@ @@ -20,178 +20,178 @@
*/
define(['underscore', 'Howler', 'require'], function(_, Howler, require) {
var SoundInterval = function(sound, id, time) {
this.sound = sound;
this.id = id;
this.interval = null;
this.time = time;
};
SoundInterval.prototype.start = function() {
if (this.interval !== null) {
return;
}
var id = this.id;
var player = _.bind(function() {
return this.sound.play(id);
}, this);
player();
this.interval = setInterval(player, this.time);
};
SoundInterval.prototype.stop = function() {
clearInterval(this.interval);
this.interval = null;
delete this.sound.intervals[this.id];
};
var Sound = function(options, aliases) {
this.sound = null;
this.intervals = {};
if (options) {
this.initialize(options, aliases);
}
};
Sound.prototype.initialize = function(options, aliases) {
// Kill all the existing stuff if any.
if (this.sound) {
this.sound.stop();
}
_.each(this.intervals, function(i) {
i.stop();
});
this.intervals = {};
// Add error handler.
var onloaderror = options.onloaderror;
options.onloaderror = function(event) {
console.error("Failed to load sounds", event);
if (onloaderror) {
onloaderror.apply(this, arguments);
}
};
// Replace urls with their require generated URLs.
var urls = options.urls;
if (urls) {
var new_urls = [];
_.each(urls, function(u) {
u = require.toUrl(u);
new_urls.push(u);
});
options.urls = new_urls;
}
// Create the new shit.
this.players = {};
this.aliases = _.extend({}, aliases);
this.sound = new Howler.Howl(options);
return this;
};
Sound.prototype.getId = function(id) {
if (this.aliases.hasOwnProperty(id)) {
return this.aliases[id];
};
return id;
};
Sound.prototype.play = function(id, interval, autostart) {
if (!this.sound) {
console.log("Play sound but not initialized.", id);
return null;
}
id = this.getId(id);
if (interval) {
if (this.intervals.hasOwnProperty(id)) {
return this.intervals[id];
}
var i = this.intervals[id] = new SoundInterval(this, id, interval);
if (autostart) {
i.start();
}
return i;
} else {
var player = this.players[id];
var sound = this.sound;
if (!player) {
player = this.players[id] = (function(id) {
var data = {};
var cb = function(soundId) {
data.soundId = soundId;
};
var play = _.debounce(function() {
if (data.soundId) {
sound.stop(data.soundId);
data.soundId = null;
}
sound.play(id, cb);
}, 10);
return play;
}(id));
}
player()
}
};
// Active initialized sound instances are kept here.
var registry = {};
window.PLAYSOUND = registry; // make available for debug.
// playSound
return [function() {
return {
initialize: function(options, name, aliases) {
if (!name) {
name = null;
}
var s = registry[name] = new Sound(options, aliases);
return s;
},
play: function(id, name) {
if (!name) {
name = null;
}
var s = registry[name];
if (!s) {
console.log("Play sound with unknown player", name);
return null;
}
return s.play(id);
},
interval: function(id, name, time) {
if (!name) {
name = null;
}
var s = registry[name];
if (!s) {
console.log("Play sound with unknown player", name);
return null;
}
if (!time) {
time = 1500;
}
return s.play(id, time);
}
}
}];
var SoundInterval = function(sound, id, time) {
this.sound = sound;
this.id = id;
this.interval = null;
this.time = time;
};
SoundInterval.prototype.start = function() {
if (this.interval !== null) {
return;
}
var id = this.id;
var player = _.bind(function() {
return this.sound.play(id);
}, this);
player();
this.interval = setInterval(player, this.time);
};
SoundInterval.prototype.stop = function() {
clearInterval(this.interval);
this.interval = null;
delete this.sound.intervals[this.id];
};
var Sound = function(options, aliases) {
this.sound = null;
this.intervals = {};
if (options) {
this.initialize(options, aliases);
}
};
Sound.prototype.initialize = function(options, aliases) {
// Kill all the existing stuff if any.
if (this.sound) {
this.sound.stop();
}
_.each(this.intervals, function(i) {
i.stop();
});
this.intervals = {};
// Add error handler.
var onloaderror = options.onloaderror;
options.onloaderror = function(event) {
console.error("Failed to load sounds", event);
if (onloaderror) {
onloaderror.apply(this, arguments);
}
};
// Replace urls with their require generated URLs.
var urls = options.urls;
if (urls) {
var new_urls = [];
_.each(urls, function(u) {
u = require.toUrl(u);
new_urls.push(u);
});
options.urls = new_urls;
}
// Create the new shit.
this.players = {};
this.aliases = _.extend({}, aliases);
this.sound = new Howler.Howl(options);
return this;
};
Sound.prototype.getId = function(id) {
if (this.aliases.hasOwnProperty(id)) {
return this.aliases[id];
};
return id;
};
Sound.prototype.play = function(id, interval, autostart) {
if (!this.sound) {
console.log("Play sound but not initialized.", id);
return null;
}
id = this.getId(id);
if (interval) {
if (this.intervals.hasOwnProperty(id)) {
return this.intervals[id];
}
var i = this.intervals[id] = new SoundInterval(this, id, interval);
if (autostart) {
i.start();
}
return i;
} else {
var player = this.players[id];
var sound = this.sound;
if (!player) {
player = this.players[id] = (function(id) {
var data = {};
var cb = function(soundId) {
data.soundId = soundId;
};
var play = _.debounce(function() {
if (data.soundId) {
sound.stop(data.soundId);
data.soundId = null;
}
sound.play(id, cb);
}, 10);
return play;
}(id));
}
player()
}
};
// Active initialized sound instances are kept here.
var registry = {};
window.PLAYSOUND = registry; // make available for debug.
// playSound
return [function() {
return {
initialize: function(options, name, aliases) {
if (!name) {
name = null;
}
var s = registry[name] = new Sound(options, aliases);
return s;
},
play: function(id, name) {
if (!name) {
name = null;
}
var s = registry[name];
if (!s) {
console.log("Play sound with unknown player", name);
return null;
}
return s.play(id);
},
interval: function(id, name, time) {
if (!name) {
name = null;
}
var s = registry[name];
if (!s) {
console.log("Play sound with unknown player", name);
return null;
}
if (!time) {
time = 1500;
}
return s.play(id, time);
}
}
}];
});

92
static/js/services/randomgen.js

@ -20,55 +20,57 @@ @@ -20,55 +20,57 @@
*/
define(["underscore"], function(_) {
// randomGen
return ["$window", function($window) {
// randomGen
return ["$window", function($window) {
var getRandomValues;
if ($window.crypto && $window.crypto.getRandomValues) {
getRandomValues = _.bind($window.crypto.getRandomValues, $window.crypto);
}
var getRandomValues;
if ($window.crypto && $window.crypto.getRandomValues) {
getRandomValues = _.bind($window.crypto.getRandomValues, $window.crypto);
}
// Simple id generator. Do not use for crypto.
var makeRandomId = function() {
return (Math.random()+1).toString(36).substr(2, 7);
};
// Simple id generator. Do not use for crypto.
var makeRandomId = function() {
return (Math.random() + 1).toString(36).substr(2, 7);
};
// Fast binary to hex function.
function binStringToHex(s) {
var s2 = '', c;
for (var i = 0, l = s.length; i < l; ++i) {
c = s.charCodeAt(i);
s2 += (c >> 4).toString(16);
s2 += (c & 0xF).toString(16);
}
return s2;
};
// Fast binary to hex function.
// Public api.
var randomGen = {
id: makeRandomId,
random: (function() {
if (getRandomValues) {
return function(options) {
var opts = _.extend({}, options);
var d = new Uint8Array(16);
getRandomValues(d);
var s = String.fromCharCode.apply(null, d);
if (opts.hex) {
return binStringToHex(s);
} else {
return s;
}
};
} else {
return makeRandomId;
}
}()),
binStringToHex: binStringToHex
};
function binStringToHex(s) {
var s2 = '',
c;
for (var i = 0, l = s.length; i < l; ++i) {
c = s.charCodeAt(i);
s2 += (c >> 4).toString(16);
s2 += (c & 0xF).toString(16);
}
return s2;
};
return randomGen;
// Public api.
var randomGen = {
id: makeRandomId,
random: (function() {
if (getRandomValues) {
return function(options) {
var opts = _.extend({}, options);
var d = new Uint8Array(16);
getRandomValues(d);
var s = String.fromCharCode.apply(null, d);
if (opts.hex) {
return binStringToHex(s);
} else {
return s;
}
};
} else {
return makeRandomId;
}
}()),
binStringToHex: binStringToHex
};
}];
return randomGen;
});
}];
});

34
static/js/services/safeapply.js

@ -20,21 +20,21 @@ @@ -20,21 +20,21 @@
*/
define([], function() {
return ["$rootScope", function($rootScope) {
return function($scope, fn) {
var phase = $scope.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if (fn) {
$scope.$eval(fn);
}
} else {
if (fn) {
$scope.$apply(fn);
} else {
$scope.$apply();
}
}
}
}];
return ["$rootScope", function($rootScope) {
return function($scope, fn) {
var phase = $scope.$root.$$phase;
if (phase == '$apply' || phase == '$digest') {
if (fn) {
$scope.$eval(fn);
}
} else {
if (fn) {
$scope.$apply(fn);
} else {
$scope.$apply();
}
}
}
}];
});
});

18
static/js/services/safedisplayname.js

@ -20,15 +20,15 @@ @@ -20,15 +20,15 @@
*/
define([], function() {
// safeDisplayName
return ["safeMessage", "$filter", function(safeMessage, $filter) {
// safeDisplayName
return ["safeMessage", "$filter", function(safeMessage, $filter) {
var displayName = $filter("displayName");
return function() {
var s = displayName.apply(this, arguments);
return safeMessage(s);
}
var displayName = $filter("displayName");
return function() {
var s = displayName.apply(this, arguments);
return safeMessage(s);
}
}];
}];
});
});

14
static/js/services/safemessage.js

@ -20,13 +20,13 @@ @@ -20,13 +20,13 @@
*/
define([], function() {
// safeMessage
return ["$sanitize", "enrichMessage", function($sanitize, enrichMessage) {
// safeMessage
return ["$sanitize", "enrichMessage", function($sanitize, enrichMessage) {
return function(s) {
return $sanitize(enrichMessage.all(s));
}
return function(s) {
return $sanitize(enrichMessage.all(s));
}
}];
}];
});
});

162
static/js/services/services.js

@ -19,91 +19,89 @@ @@ -19,91 +19,89 @@
*
*/
define([
'underscore',
'underscore',
'services/desktopnotify',
'services/playsound',
'services/safeapply',
'services/mediastream',
'services/appdata',
'services/buddydata',
'services/buddylist',
'services/enrichmessage',
'services/safemessage',
'services/alertify',
'services/toastr',
'services/visibility',
'services/translation',
'services/mediasources',
'services/fileupload',
'services/filedownload',
'services/filedata',
'services/filetransfer',
'services/safedisplayname',
'services/randomgen',
'services/fastscroll',
'services/videowaiter',
'services/videolayout'
], function(_,
desktopNotify,
playSound,
safeApply,
mediaStream,
appData,
buddyData,
buddyList,
enrichMessage,
safeMessage,
alertify,
toastr,
visibility,
translation,
mediaSources,
fileUpload,
fileDownload,
fileData,
fileTransfer,
safeDisplayName,
randomGen,
fastScroll,
videoWaiter,
videoLayout
) {
'services/desktopnotify',
'services/playsound',
'services/safeapply',
'services/mediastream',
'services/appdata',
'services/buddydata',
'services/buddylist',
'services/enrichmessage',
'services/safemessage',
'services/alertify',
'services/toastr',
'services/visibility',
'services/translation',
'services/mediasources',
'services/fileupload',
'services/filedownload',
'services/filedata',
'services/filetransfer',
'services/safedisplayname',
'services/randomgen',
'services/fastscroll',
'services/videowaiter',
'services/videolayout'], function(_,
desktopNotify,
playSound,
safeApply,
mediaStream,
appData,
buddyData,
buddyList,
enrichMessage,
safeMessage,
alertify,
toastr,
visibility,
translation,
mediaSources,
fileUpload,
fileDownload,
fileData,
fileTransfer,
safeDisplayName,
randomGen,
fastScroll,
videoWaiter,
videoLayout) {
var services = {
desktopNotify: desktopNotify,
playSound: playSound,
safeApply: safeApply,
mediaStream: mediaStream,
appData: appData,
buddyData: buddyData,
buddyList: buddyList,
enrichMessage: enrichMessage,
safeMessage: safeMessage,
alertify: alertify,
toastr: toastr,
visibility: visibility,
translation: translation,
mediaSources: mediaSources,
fileUpload: fileUpload,
fileDownload: fileDownload,
fileData: fileData,
fileTransfer: fileTransfer,
safeDisplayName: safeDisplayName,
randomGen: randomGen,
fastScroll: fastScroll,
videoWaiter: videoWaiter,
videoLayout: videoLayout
};
var services = {
desktopNotify: desktopNotify,
playSound: playSound,
safeApply: safeApply,
mediaStream: mediaStream,
appData: appData,
buddyData: buddyData,
buddyList: buddyList,
enrichMessage: enrichMessage,
safeMessage: safeMessage,
alertify: alertify,
toastr: toastr,
visibility: visibility,
translation: translation,
mediaSources: mediaSources,
fileUpload: fileUpload,
fileDownload: fileDownload,
fileData: fileData,
fileTransfer: fileTransfer,
safeDisplayName: safeDisplayName,
randomGen: randomGen,
fastScroll: fastScroll,
videoWaiter: videoWaiter,
videoLayout: videoLayout
};
var initialize = function (angModule) {
_.each(services, function(service, name) {
angModule.factory(name, service);
})
}
var initialize = function(angModule) {
_.each(services, function(service, name) {
angModule.factory(name, service);
})
}
return {
initialize: initialize
};
return {
initialize: initialize
};
});

2
static/js/services/toastr.js

@ -38,4 +38,4 @@ define(['toastr'], function(toastr) { @@ -38,4 +38,4 @@ define(['toastr'], function(toastr) {
}];
});
});

154
static/js/services/translation.js

@ -20,89 +20,89 @@ @@ -20,89 +20,89 @@
*/
define(["jed", "underscore"], function(Jed, _) {
var TranslationScope = function(service, context, domain) {
var TranslationScope = function(service, context, domain) {
var i18n = service.i18n;
var i18n = service.i18n;
// _
this._ = _.bind(function() {
if (domain && context) {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular).onDomain(domain).withContext(context);
return r.fetch.apply(r, vars);
}, this);
} else if (domain) {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular).onDomain(domain);
return r.fetch.apply(r, vars);
}, this);
} else if (context) {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular).withContext(context);
return r.fetch.apply(r, vars);
}, this);
} else {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular);
return r.fetch.apply(r, vars);
}, this);
}
}, this)();
// _
this._ = _.bind(function() {
if (domain && context) {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular).onDomain(domain).withContext(context);
return r.fetch.apply(r, vars);
}, this);
} else if (domain) {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular).onDomain(domain);
return r.fetch.apply(r, vars);
}, this);
} else if (context) {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular).withContext(context);
return r.fetch.apply(r, vars);
}, this);
} else {
return _.bind(function(singular) {
var vars = Array.prototype.slice.call(arguments, 1);
var r = i18n.translate(singular);
return r.fetch.apply(r, vars);
}, this);
}
}, this)();
// _n
this._n = _.bind(function() {
if (domain && context) {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).onDomain(domain).withContext(context).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
});
} else if (domain) {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).onDomain(domain).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
});
} else if (context) {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).withContext(context).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
});
} else {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
})
}
}, this)();
// _n
this._n = _.bind(function() {
if (domain && context) {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).onDomain(domain).withContext(context).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
});
} else if (domain) {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).onDomain(domain).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
});
} else if (context) {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).withContext(context).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
});
} else {
return _.bind(function(singular, plural) {
var vars = Array.prototype.slice.call(arguments, 2);
var r = i18n.translate(singular).ifPlural(vars[0], plural);
return r.fetch.apply(r, vars);
})
}
}, this)();
};
};
var TranslationService = function(translationData) {
this.i18n = new Jed(translationData);
var ts = new TranslationScope(this);
this._ = _.bind(ts._, ts);
this._n = _.bind(ts._n, ts);
};
var TranslationService = function(translationData) {
this.i18n = new Jed(translationData);
var ts = new TranslationScope(this);
this._ = _.bind(ts._, ts);
this._n = _.bind(ts._n, ts);
};
TranslationService.prototype.inject = function(obj, context, domain) {
// Inject our functions into objects.
var ts = new TranslationScope(this, context, domain);
obj._ = _.bind(ts._, ts);
obj._n = _.bind(ts._n, ts);
//console.log("Injected translation service", ts, obj);
return obj;
};
TranslationService.prototype.inject = function(obj, context, domain) {
// Inject our functions into objects.
var ts = new TranslationScope(this, context, domain);
obj._ = _.bind(ts._, ts);
obj._n = _.bind(ts._n, ts);
//console.log("Injected translation service", ts, obj);
return obj;
};
return ["translationData", function(translationData) {
var translationService = new TranslationService(translationData);
return translationService;
}];
return ["translationData", function(translationData) {
var translationService = new TranslationService(translationData);
return translationService;
}];
});

573
static/js/services/videolayout.js

@ -20,114 +20,113 @@ @@ -20,114 +20,113 @@
*/
define(["jquery", "underscore", "modernizr"], function($, _, Modernizr) {
var dynamicCSSContainer = "audiovideo-dynamic";
var renderers = {};
var getRemoteVideoSize = function(videos, peers) {
var size = {
width: 1920,
height: 1080
}
if (videos.length) {
if (videos.length === 1) {
var remoteVideo = peers[videos[0]].element.find("video").get(0);
size.width = remoteVideo.videoWidth;
size.height = remoteVideo.videoHeight;
console.log("Remote video size: ", size);
}
}
return size;
}
var objectFitSupport = Modernizr["object-fit"] && true;
// videoLayout
return ["$window", function($window) {
// Video layout with all videos rendered the same size.
var OnePeople = function(container, scope, controller) {
};
OnePeople.prototype.name = "onepeople";
OnePeople.prototype.render = function(container, size, scope, videos, peers) {
if (this.closed) {
return;
}
var videoWidth;
var videoHeight;
if (videos.length) {
var remoteSize = getRemoteVideoSize(videos, peers);
videoWidth = remoteSize.width;
videoHeight = remoteSize.height;
}
if (!videoWidth) {
// XXX(longsleep): Improve this condition - its crap to compare style opacity (tm)!
if (scope.localVideo.style.opacity === '1') {
videoWidth = scope.localVideo.videoWidth;
videoHeight = scope.localVideo.videoHeight;
console.log("Local video size: ", videoWidth, videoHeight);
videos = [null];
}
}
if (!videos.length) {
return;
}
if (!videoWidth) {
videoWidth = 640;
}
if (!videoHeight) {
videoHeight = 360;
}
var aspectRatio = videoWidth/videoHeight;
var innerHeight = size.height;
var innerWidth = size.width;
//console.log("resize", innerHeight, innerWidth);
//console.log("resize", container, videos.length, aspectRatio, innerHeight, innerWidth);
var extraCSS = {};
if (!objectFitSupport) {
// Make mini video fit into available space on browsers with no object-fit support.
// http://caniuse.com/object-fit
var aspectRatioLocal = scope.localVideo.videoWidth/scope.localVideo.videoHeight;
extraCSS = {
".renderer-onepeople .miniVideo": {
width: ($(scope.mini).height()*aspectRatioLocal)+"px"
}
};
}
if (videos.length === 1) {
var newVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight;
var newVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio;
container.style.width = newVideoWidth + 'px';
container.style.left = ((innerWidth - newVideoWidth) / 2) + 'px';
} else {
var space = innerHeight*innerWidth; // square pixels
var videoSpace = space/videos.length;
var singleVideoWidthOptimal = Math.pow(videoSpace * aspectRatio, 0.5);
var videosPerRow = Math.ceil(innerWidth/singleVideoWidthOptimal);
if (videosPerRow > videos.length) {
videosPerRow = videos.length;
}
var singleVideoWidth = Math.ceil(innerWidth/videosPerRow);
var singleVideoHeight = Math.ceil(singleVideoWidth/aspectRatio);
var newContainerWidth = (videosPerRow*singleVideoWidth);
var newContainerHeight = Math.ceil(videos.length/videosPerRow)*singleVideoHeight;
if (newContainerHeight > innerHeight) {
var tooHigh = (newContainerHeight-innerHeight) / Math.ceil(videos.length / videosPerRow);
singleVideoHeight -= tooHigh;
singleVideoWidth = singleVideoHeight * aspectRatio;
}
/*
var dynamicCSSContainer = "audiovideo-dynamic";
var renderers = {};
var getRemoteVideoSize = function(videos, peers) {
var size = {
width: 1920,
height: 1080
}
if (videos.length) {
if (videos.length === 1) {
var remoteVideo = peers[videos[0]].element.find("video").get(0);
size.width = remoteVideo.videoWidth;
size.height = remoteVideo.videoHeight;
console.log("Remote video size: ", size);
}
}
return size;
}
var objectFitSupport = Modernizr["object-fit"] && true;
// videoLayout
return ["$window", function($window) {
// Video layout with all videos rendered the same size.
var OnePeople = function(container, scope, controller) {};
OnePeople.prototype.name = "onepeople";
OnePeople.prototype.render = function(container, size, scope, videos, peers) {
if (this.closed) {
return;
}
var videoWidth;
var videoHeight;
if (videos.length) {
var remoteSize = getRemoteVideoSize(videos, peers);
videoWidth = remoteSize.width;
videoHeight = remoteSize.height;
}
if (!videoWidth) {
// XXX(longsleep): Improve this condition - its crap to compare style opacity (tm)!
if (scope.localVideo.style.opacity === '1') {
videoWidth = scope.localVideo.videoWidth;
videoHeight = scope.localVideo.videoHeight;
console.log("Local video size: ", videoWidth, videoHeight);
videos = [null];
}
}
if (!videos.length) {
return;
}
if (!videoWidth) {
videoWidth = 640;
}
if (!videoHeight) {
videoHeight = 360;
}
var aspectRatio = videoWidth / videoHeight;
var innerHeight = size.height;
var innerWidth = size.width;
//console.log("resize", innerHeight, innerWidth);
//console.log("resize", container, videos.length, aspectRatio, innerHeight, innerWidth);
var extraCSS = {};
if (!objectFitSupport) {
// Make mini video fit into available space on browsers with no object-fit support.
// http://caniuse.com/object-fit
var aspectRatioLocal = scope.localVideo.videoWidth / scope.localVideo.videoHeight;
extraCSS = {
".renderer-onepeople .miniVideo": {
width: ($(scope.mini).height() * aspectRatioLocal) + "px"
}
};
}
if (videos.length === 1) {
var newVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight;
var newVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio;
container.style.width = newVideoWidth + 'px';
container.style.left = ((innerWidth - newVideoWidth) / 2) + 'px';
} else {
var space = innerHeight * innerWidth; // square pixels
var videoSpace = space / videos.length;
var singleVideoWidthOptimal = Math.pow(videoSpace * aspectRatio, 0.5);
var videosPerRow = Math.ceil(innerWidth / singleVideoWidthOptimal);
if (videosPerRow > videos.length) {
videosPerRow = videos.length;
}
var singleVideoWidth = Math.ceil(innerWidth / videosPerRow);
var singleVideoHeight = Math.ceil(singleVideoWidth / aspectRatio);
var newContainerWidth = (videosPerRow * singleVideoWidth);
var newContainerHeight = Math.ceil(videos.length / videosPerRow) * singleVideoHeight;
if (newContainerHeight > innerHeight) {
var tooHigh = (newContainerHeight - innerHeight) / Math.ceil(videos.length / videosPerRow);
singleVideoHeight -= tooHigh;
singleVideoWidth = singleVideoHeight * aspectRatio;
}
/*
console.log("space", space);
console.log("videospace", videoSpace);
console.log("singleVideoWidthOptimal", singleVideoWidthOptimal);
@ -135,182 +134,182 @@ define(["jquery", "underscore", "modernizr"], function($, _, Modernizr) { @@ -135,182 +134,182 @@ define(["jquery", "underscore", "modernizr"], function($, _, Modernizr) {
console.log("singleVideoWidth", singleVideoWidth);
console.log("singleVideoHeight", singleVideoHeight);
*/
container.style.width = newContainerWidth + "px";
container.style.left = ((innerWidth - newContainerWidth) / 2) + 'px';
extraCSS = $.extend(extraCSS, {
".renderer-onepeople .remoteVideos": {
">div": {
width: singleVideoWidth+"px",
height: singleVideoHeight+"px"
}
}
});
}
$.injectCSS(extraCSS, {
truncateFirst: true,
containerName: dynamicCSSContainer
});
};
OnePeople.prototype.close = function(container, scope, controller) {
this.closed = true;
};
// Smally inherits from OnePeople
var Smally = function(container, scope, controller) {
// Call super.
OnePeople.call(this, container, scope, controller);
}
Smally.prototype = Object.create(OnePeople.prototype);
Smally.prototype.constructor = Smally;
Smally.prototype.name = "smally";
// A view with one selectable large video. The others are small.
var ConferenceKiosk = function(container, scope, controller) {
this.remoteVideos = $(container).find(".remoteVideos");
this.bigVideo = $("<div>").addClass("bigVideo").get(0);
this.remoteVideos.before(this.bigVideo);
this.big = null;
this.remoteVideos.on("click", ".remoteVideo", _.bind(function(event) {
if ($(event.currentTarget).hasClass("remoteVideo")) {
event.stopPropagation();
this.makeBig($(event.currentTarget));
}
}, this));
};
ConferenceKiosk.prototype.name = "conferencekiosk";
ConferenceKiosk.prototype.makeBig = function(remoteVideo) {
if (this.big === remoteVideo) {
return;
}
if (this.big) {
// Add old video back.
this.big.insertAfter(remoteVideo);
this.big.find("video").get(0).play();
}
this.big = remoteVideo;
remoteVideo.appendTo(this.bigVideo);
remoteVideo.find("video").get(0).play();
};
ConferenceKiosk.prototype.render = function(container, size, scope, videos, peers) {
var big = this.big;
if (big) {
var currentbigpeerid = this.big.data("peerid");
if (!peers[currentbigpeerid]) {
console.log("Current big peer is no longer there", currentbigpeerid);
this.big = big = null;
}
}
if (!big) {
if (videos.length) {
this.makeBig(peers[videos[0]].element);
this.bigVideo.style.opacity = 1;
}
}
var remoteSize = getRemoteVideoSize(videos, peers);
var aspectRatio = remoteSize.width/remoteSize.height;
var innerHeight = size.height - 110;
var innerWidth = size.width;
var extraCSS = {};
var bigVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight;
var bigVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio;
this.bigVideo.style.width = bigVideoWidth + 'px';
this.bigVideo.style.height = bigVideoHeight + 'px';
// Make space for own video on the right if width goes low.
if (((size.width - (videos.length-1) * 192) / 2) < 192) {
extraCSS = {
".renderer-conferencekiosk .remoteVideos": {
"margin-right": "192px",
"overflow-x": "auto",
"overflow-y": "hidden"
}
};
}
$.injectCSS(extraCSS, {
truncateFirst: true,
containerName: dynamicCSSContainer
});
};
ConferenceKiosk.prototype.close = function(container, scope, controller) {
this.closed = true;
if (this.big) {
this.remoteVideos.append(this.big);
this.big.find("video").get(0).play();
}
this.big = null;
this.bigVideo.remove()
this.bigVideo = null;
this.remoteVideos = null;
};
// Register renderers.
renderers[OnePeople.prototype.name] = OnePeople;
renderers[Smally.prototype.name] = Smally;
renderers[ConferenceKiosk.prototype.name] = ConferenceKiosk;
// Public api.
var current = null;
return {
update: function(name, size, scope, controller) {
var videos = _.keys(controller.peers);
var peers = controller.peers;
var container = scope.container;
var layoutparent = scope.layoutparent;
if (!current) {
current = new renderers[name](container, scope, controller)
console.log("Created new video layout renderer", name, current);
$(layoutparent).addClass("renderer-"+name);
return true;
} else {
if (current.name !== name) {
current.close(container, scope, controller);
$(container).removeAttr("style");
$(layoutparent).removeClass("renderer-"+current.name);
current = new renderers[name](container, scope, controller)
$(layoutparent).addClass("renderer-"+name);
console.log("Switched to new video layout renderer", name, current);
return true;
}
}
return current.render(container, size, scope, videos, peers);
},
register: function(name, impl) {
renderers[name] = impl;
},
layouts: function() {
return _.keys(renderers);
}
}
}];
});
container.style.width = newContainerWidth + "px";
container.style.left = ((innerWidth - newContainerWidth) / 2) + 'px';
extraCSS = $.extend(extraCSS, {
".renderer-onepeople .remoteVideos": {
">div": {
width: singleVideoWidth + "px",
height: singleVideoHeight + "px"
}
}
});
}
$.injectCSS(extraCSS, {
truncateFirst: true,
containerName: dynamicCSSContainer
});
};
OnePeople.prototype.close = function(container, scope, controller) {
this.closed = true;
};
// Smally inherits from OnePeople
var Smally = function(container, scope, controller) {
// Call super.
OnePeople.call(this, container, scope, controller);
}
Smally.prototype = Object.create(OnePeople.prototype);
Smally.prototype.constructor = Smally;
Smally.prototype.name = "smally";
// A view with one selectable large video. The others are small.
var ConferenceKiosk = function(container, scope, controller) {
this.remoteVideos = $(container).find(".remoteVideos");
this.bigVideo = $("<div>").addClass("bigVideo").get(0);
this.remoteVideos.before(this.bigVideo);
this.big = null;
this.remoteVideos.on("click", ".remoteVideo", _.bind(function(event) {
if ($(event.currentTarget).hasClass("remoteVideo")) {
event.stopPropagation();
this.makeBig($(event.currentTarget));
}
}, this));
};
ConferenceKiosk.prototype.name = "conferencekiosk";
ConferenceKiosk.prototype.makeBig = function(remoteVideo) {
if (this.big === remoteVideo) {
return;
}
if (this.big) {
// Add old video back.
this.big.insertAfter(remoteVideo);
this.big.find("video").get(0).play();
}
this.big = remoteVideo;
remoteVideo.appendTo(this.bigVideo);
remoteVideo.find("video").get(0).play();
};
ConferenceKiosk.prototype.render = function(container, size, scope, videos, peers) {
var big = this.big;
if (big) {
var currentbigpeerid = this.big.data("peerid");
if (!peers[currentbigpeerid]) {
console.log("Current big peer is no longer there", currentbigpeerid);
this.big = big = null;
}
}
if (!big) {
if (videos.length) {
this.makeBig(peers[videos[0]].element);
this.bigVideo.style.opacity = 1;
}
}
var remoteSize = getRemoteVideoSize(videos, peers);
var aspectRatio = remoteSize.width / remoteSize.height;
var innerHeight = size.height - 110;
var innerWidth = size.width;
var extraCSS = {};
var bigVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight;
var bigVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio;
this.bigVideo.style.width = bigVideoWidth + 'px';
this.bigVideo.style.height = bigVideoHeight + 'px';
// Make space for own video on the right if width goes low.
if (((size.width - (videos.length - 1) * 192) / 2) < 192) {
extraCSS = {
".renderer-conferencekiosk .remoteVideos": {
"margin-right": "192px",
"overflow-x": "auto",
"overflow-y": "hidden"
}
};
}
$.injectCSS(extraCSS, {
truncateFirst: true,
containerName: dynamicCSSContainer
});
};
ConferenceKiosk.prototype.close = function(container, scope, controller) {
this.closed = true;
if (this.big) {
this.remoteVideos.append(this.big);
this.big.find("video").get(0).play();
}
this.big = null;
this.bigVideo.remove()
this.bigVideo = null;
this.remoteVideos = null;
};
// Register renderers.
renderers[OnePeople.prototype.name] = OnePeople;
renderers[Smally.prototype.name] = Smally;
renderers[ConferenceKiosk.prototype.name] = ConferenceKiosk;
// Public api.
var current = null;
return {
update: function(name, size, scope, controller) {
var videos = _.keys(controller.peers);
var peers = controller.peers;
var container = scope.container;
var layoutparent = scope.layoutparent;
if (!current) {
current = new renderers[name](container, scope, controller)
console.log("Created new video layout renderer", name, current);
$(layoutparent).addClass("renderer-" + name);
return true;
} else {
if (current.name !== name) {
current.close(container, scope, controller);
$(container).removeAttr("style");
$(layoutparent).removeClass("renderer-" + current.name);
current = new renderers[name](container, scope, controller)
$(layoutparent).addClass("renderer-" + name);
console.log("Switched to new video layout renderer", name, current);
return true;
}
}
return current.render(container, size, scope, videos, peers);
},
register: function(name, impl) {
renderers[name] = impl;
},
layouts: function() {
return _.keys(renderers);
}
}
}];
});

90
static/js/services/videowaiter.js

@ -20,53 +20,53 @@ @@ -20,53 +20,53 @@
*/
define(["underscore"], function(_) {
return ["$window", function($window) {
return ["$window", function($window) {
var Waiter = function() {
this.stop = false;
this.count = 0;
this.retries = 100;
};
Waiter.prototype.start = function(video, stream, cb, err_cb) {
if (this.stop) {
if (err_cb) {
err_cb(video, stream);
}
return;
}
var videoTracks = stream.getVideoTracks();
//console.log("wait for video", videoTracks.length, video.currentTime, video)
if (videoTracks.length === 0) {
cb(false, video, stream);
} else if (video.currentTime > 0 && video.videoHeight > 0) {
cb(true, video, stream);
} else {
this.count++;
if (this.count < this.retries) {
$window.setTimeout(_.bind(this.start, this, video, stream, cb, err_cb), 100);
} else {
if (err_cb) {
err_cb(video, stream);
}
}
}
};
Waiter.prototype.stop = function() {
this.stop = true;
};
var Waiter = function() {
this.stop = false;
this.count = 0;
this.retries = 100;
};
Waiter.prototype.start = function(video, stream, cb, err_cb) {
if (this.stop) {
if (err_cb) {
err_cb(video, stream);
}
return;
}
var videoTracks = stream.getVideoTracks();
//console.log("wait for video", videoTracks.length, video.currentTime, video)
if (videoTracks.length === 0) {
cb(false, video, stream);
} else if (video.currentTime > 0 && video.videoHeight > 0) {
cb(true, video, stream);
} else {
this.count++;
if (this.count < this.retries) {
$window.setTimeout(_.bind(this.start, this, video, stream, cb, err_cb), 100);
} else {
if (err_cb) {
err_cb(video, stream);
}
}
}
};
Waiter.prototype.stop = function() {
this.stop = true;
};
// videoWaiter wait
return {
wait: function(video, stream, cb, err_cb) {
var waiter = new Waiter();
_.defer(function() {
waiter.start(video, stream, cb, err_cb);
});
return waiter;
}
}
// videoWaiter wait
return {
wait: function(video, stream, cb, err_cb) {
var waiter = new Waiter();
_.defer(function() {
waiter.start(video, stream, cb, err_cb);
});
return waiter;
}
}
}]
}]
});
});

Loading…
Cancel
Save