Browse Source

Merge branch 'release-0.23'

Conflicts:
	.jshint
pull/175/head v0.23.0
Simon Eisenmann 11 years ago
parent
commit
2bdbdc09ee
  1. 27
      .jshint
  2. 28
      debian/changelog
  3. 119
      doc/CHANNELING-API.txt
  4. 39
      doc/plugin-example.js
  5. 2
      doc/plugin-test-authorize.js
  6. 1
      html/head.html
  7. 4
      html/main.html
  8. 15
      server.conf.in
  9. 18
      src/app/spreed-webrtc-server/buffercache.go
  10. 38
      src/app/spreed-webrtc-server/channeling.go
  11. 284
      src/app/spreed-webrtc-server/channelling_api.go
  12. 240
      src/app/spreed-webrtc-server/channelling_api_test.go
  13. 87
      src/app/spreed-webrtc-server/client.go
  14. 43
      src/app/spreed-webrtc-server/common_test.go
  15. 121
      src/app/spreed-webrtc-server/config.go
  16. 164
      src/app/spreed-webrtc-server/connection.go
  17. 1
      src/app/spreed-webrtc-server/context.go
  18. 566
      src/app/spreed-webrtc-server/hub.go
  19. 69
      src/app/spreed-webrtc-server/incoming_codec.go
  20. 181
      src/app/spreed-webrtc-server/main.go
  21. 201
      src/app/spreed-webrtc-server/room_manager.go
  22. 79
      src/app/spreed-webrtc-server/room_manager_test.go
  23. 189
      src/app/spreed-webrtc-server/roomworker.go
  24. 124
      src/app/spreed-webrtc-server/roomworker_test.go
  25. 280
      src/app/spreed-webrtc-server/server.go
  26. 35
      src/app/spreed-webrtc-server/session.go
  27. 173
      src/app/spreed-webrtc-server/session_manager.go
  28. 13
      src/app/spreed-webrtc-server/sessions.go
  29. 8
      src/app/spreed-webrtc-server/stats.go
  30. 104
      src/app/spreed-webrtc-server/stats_manager.go
  31. 130
      src/app/spreed-webrtc-server/tickets.go
  32. 32
      src/app/spreed-webrtc-server/users.go
  33. 35
      src/app/spreed-webrtc-server/ws.go
  34. 1
      src/i18n/helpers/languages.py
  35. 127
      src/i18n/messages-de.po
  36. 111
      src/i18n/messages-ja.po
  37. 111
      src/i18n/messages-ko.po
  38. 111
      src/i18n/messages-zh-cn.po
  39. 111
      src/i18n/messages-zh-tw.po
  40. 93
      src/i18n/messages.pot
  41. 2
      src/styles/Makefile.am
  42. 49
      src/styles/_shame.scss
  43. 35
      src/styles/components/_audiovideo.scss
  44. 2
      src/styles/components/_buddylist.scss
  45. 5
      src/styles/components/_contactsmanager.scss
  46. 3
      src/styles/components/_social.scss
  47. 5
      src/styles/csp.scss
  48. 4
      src/styles/global/_overlaybar.scss
  49. 40
      src/styles/global/_pages.scss
  50. 3
      src/styles/global/_variables.scss
  51. 91
      src/styles/global/skins/_dark.scss
  52. 6
      src/styles/libs/_dialogs.scss
  53. 1
      src/styles/libs/_libs.scss
  54. 41
      src/styles/libs/angular/angular-csp.scss
  55. 0
      src/styles/libs/angular/angular.scss
  56. 44
      src/styles/libs/toastr/toastr.scss
  57. 1
      src/styles/main.scss
  58. 20
      static/css/csp.min.css
  59. 2
      static/css/main.min.css
  60. 59
      static/js/app.js
  61. 5
      static/js/base.js
  62. 14
      static/js/controllers/chatroomcontroller.js
  63. 25
      static/js/controllers/contactsmanagercontroller.js
  64. 2
      static/js/controllers/contactsmanagereditcontroller.js
  65. 6
      static/js/controllers/controllers.js
  66. 257
      static/js/controllers/mediastreamcontroller.js
  67. 90
      static/js/controllers/roomchangecontroller.js
  68. 2
      static/js/controllers/statusmessagecontroller.js
  69. 11
      static/js/controllers/usersettingscontroller.js
  70. 2
      static/js/directives/audiolevel.js
  71. 283
      static/js/directives/audiovideo.js
  72. 2
      static/js/directives/bfi.js
  73. 65
      static/js/directives/buddylist.js
  74. 14
      static/js/directives/buddypicturecapture.js
  75. 46
      static/js/directives/buddypictureupload.js
  76. 40
      static/js/directives/chat.js
  77. 2
      static/js/directives/contactrequest.js
  78. 2
      static/js/directives/defaultdialog.js
  79. 12
      static/js/directives/directives.js
  80. 4
      static/js/directives/fileinfo.js
  81. 2
      static/js/directives/odfcanvas.js
  82. 2
      static/js/directives/onenter.js
  83. 2
      static/js/directives/onescape.js
  84. 52
      static/js/directives/page.js
  85. 2
      static/js/directives/pdfcanvas.js
  86. 14
      static/js/directives/presentation.js
  87. 55
      static/js/directives/roombar.js
  88. 9
      static/js/directives/screenshare.js
  89. 160
      static/js/directives/settings.js
  90. 22
      static/js/directives/socialshare.js
  91. 2
      static/js/directives/statusmessage.js
  92. 50
      static/js/directives/title.js
  93. 40
      static/js/directives/usability.js
  94. 92
      static/js/directives/welcome.js
  95. 31
      static/js/directives/youtubevideo.js
  96. 2
      static/js/filters/buddyimagesrc.js
  97. 2
      static/js/filters/displayconference.js
  98. 2
      static/js/filters/displayname.js
  99. 2
      static/js/filters/displaynameforsession.js
  100. 2
      static/js/filters/displayuserid.js
  101. Some files were not shown because too many files have changed in this diff Show More

27
.jshint

@ -1,10 +1,27 @@
{ {
"smarttabs": true, "asi": true,
"onecase": true, "bitwise": false,
"browser": true,
"camelcase": false, // Disabled for now.
"curly": true, "curly": true,
"forin": true, "forin": true,
"trailing": true, "immed": true,
"asi": true, "latedef": true,
"maxlen": 1000, "maxlen": 1000,
"quotmark": false "newcap": true,
"noarg": true,
"noempty": false, // Disabled for now.
"nonew": true,
"onecase": true,
"predef": [
"define",
"require",
"console"
],
"quotmark": false,
"smarttabs": true,
"trailing": true,
"undef": true,
"unused": false, // Disabled for now
"globalstrict": true
} }

28
debian/changelog vendored

@ -1,3 +1,31 @@
spreed-webrtc-server (0.23.0) precise; urgency=low
* Added support for renegotation in web client (disabled).
* Rooms were refactored to be able to confirm joins.
* Added support to PIN lock rooms (server side).
* Updated javascript to follow now jshin rules.
* Updated plugin API to make the main App object available.
* Refactored server side configuration loading.
* Improved usability of image upload positioning and scaling.
* Stream lined third party javascript to reduce size.
* Javascript is now using 'strict' mode everywhere.
* Added suppport for Content Security Policy (CSP).
* Added Javascript source mappings where missing.
* Fixed bye handling in conferences to avoid endless dial tones.
* Added support for audio and/or video only connections when
the corresponding device is not there.
* Several icons were changed for usability reasons.
* Improved dialogs and texts for usability reasons.
* Room bar is now automatically visible when not in a call.
* Updated auto focus behavior of room select forms.
* Implemented a room history on welcome screen.
* Added a sign in button to the top bar.
* Changed order of settings form for usability reasons.
* Missed call toast now always is shown.
* Improved toast notification styles.
-- Simon Eisenmann <simon@struktur.de> Tue, 09 Dec 2014 15:45:52 +0100
spreed-webrtc-server (0.22.8) precise; urgency=low spreed-webrtc-server (0.22.8) precise; urgency=low
* Removed opacity transition from chat pane to avoid compositing issues. * Removed opacity transition from chat pane to avoid compositing issues.

119
doc/CHANNELING-API.txt

@ -87,6 +87,16 @@ Sending vs receiving document data encapsulation
A : Session attestation token. Only available for incoming data A : Session attestation token. Only available for incoming data
created by other sessions (optional). created by other sessions (optional).
Error returns
Calls providing an Iid which fail will receive an Error document with the
following format:
{
"Type": "Error",
"Code": "value_identifying_error",
"Message": "A description of the error condition"
}
Special purpose documents for channling Special purpose documents for channling
@ -147,18 +157,115 @@ Special purpose documents for channling
Hello: { Hello: {
Version: "1.0.0", Version: "1.0.0",
Ua: "Test client 1.0", Ua: "Test client 1.0",
Id: "" Id: "",
"Credentials": {...}
} }
} }
Hello document is to be send by the client after connection was Hello document is to be send by the client after connection was established.
established. If an Iid is provided, a Welcome document will be returned if joining the
room with the given Id succeeds. Otherwise an Error document with one of the
error codes listed below will be returned. Note that any previous room will
have been left regardless of whether the response is successful.
Keys under Hello: Keys under Hello:
Version : Channel protocol version (string). Version : Channel protocol version (string).
Ua : User agent description (string). Ua : User agent description (string).
Id : Room id. The default Room has the empty string Id ("") (string). Id : Room id. The default Room has the empty string Id ("") (string).
Credentials : An optional RoomCredentials document containing room
authentication information. See the Room document for
information on how such credentials should be handled after
a Welcome is received for the requested room. Note that
providing credentials while joining an existing room which
does not require them is an error, such requests should be
retried without credentials. In contrast, joining a
non-existent room with credentials will create the room
using the given credentials. Note that an error with a code
of authorization_not_required or invalid_credentials shall
cause the client to discard any cached room credentials.
Error codes:
default_room_disabled : Joining the room "" is not allowed by this
server.
authorization_required : Joining the given room requires credentials.
authorization_not_required : No credentials should be provided for this
room.
invalid_credentials : The provided credentials are incorrect.
room_join_requires_account : Server configuration requires an
authenticated user account to join this room.
Welcome
{
"Type": "Welcome",
"Welcome": {
"Room": {...},
"Users": []
}
}
Welcome is sent in reply to a successful Hello, and contains all data
needed to set up the initial room connection.
Keys under Welcome:
Room: Contains the current state of the room, see the description of
the Room document for more details.
Users: Contains the user list for the room, see the description of
the Users document for more details.
RoomCredentials
{
"PIN": "my-super-sekrit-code"
}
RoomCredentials contains room authentication information, and is used as a
child document when joining or updating a room.
Keys under RoomCredentials:
PIN : A password string which may be used by clients to authenticate
themselves. Note that acceptable characters for this field may be
constrained by the server based upon its configuration.
Room
{
"Type": "Room",
"Name": "room-name-here"
"Credentials": {...}
}
Clients may send a Room document in order to update all room properties
to the values given in the document. The room name must be given and match
the currently joined room. Successful updates will receive an updated Room
document as a reply, or an Error document if the update fails.
Addtionally, the Room document is included in responses to initial joins
and broadcast when room properties are updated.
Keys under Room:
Name : The human readable ID of the room, currently must be globally
unique.
Credentials : Optional authentication information for the room, see the
documentation of the RoomCredentials document for more
details. This field shall only be present when sending or
receiving an update which alters room authentication data.
It should only be inferred that authentication is not
required if joining a room succeeds without credentials and
no updates containing credentials has been received.
Authentication may be disabled by sending a Room document
containing a RoomCredentials document with only empty
fields. Clients shall discard any cached authentication
information upon receiving such an update.
Error codes:
not_in_room : Clients may only update rooms which they have joined.
Peer connection documents Peer connection documents

39
doc/plugin-example.js

@ -18,17 +18,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['angular'], function(angular) { define(['angular'], function(angular) {
return { return {
// Angular modules - either overwrite existing modules or add new /**
// ones. All app dependencies are already loaded so modules defined * Modules function for this plugin. Use it to add Angular modules.
// here will be used instead of the default ones. * either overwrite existing modules or add new ones. All app dependencies
// To load add a new module to the angular app, append the name of * are already loaded so modules defined here will be used instead of
// the module to the modules array. * the default ones. Defining the module function on a plugin is optional.
// Defining a module in a plugin is optional. *
module: function(modules) { * @param {array} modules - To add a new module to the angular app, append
* the name of the module to the modules array.
* @param {Object} launcher - The launcher object containg $q and $http.
*
*/
module: function(modules, launcher) {
// Create and add a new module. // Create and add a new module.
// See http://docs.angularjs.org/guide/module for details on Angular. // See http://docs.angularjs.org/guide/module for details on Angular.
@ -61,12 +68,18 @@ define(['angular'], function(angular) {
}, },
// Initialize function for this module. /**
// Add your angular stuff here. The app is the base Angular module for * Initialize function for this plugin. Use this function to add your
// the web application. The plugin initialize function is called after * your Angular stuff to the app. The plugin initializse function is
// the app initialize function. * called after the app initialize function.
// Defining the initialize function is optional. *
initialize: function(app) { * @param {App} app - The base Angular module for the web application.
* @param {Object} launcher - The launcher object containg $q and $http.
* @returns {($q.promise|null)} - Return a $q.promise when you need the
* launcher to wait.
*
*/
initialize: function(app, launcher) {
console.log("Initializing plugin-example ..."); console.log("Initializing plugin-example ...");

2
doc/plugin-test-authorize.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['angular', 'sjcl'], function(angular, sjcl) { define(['angular', 'sjcl'], function(angular, sjcl) {
return { return {

1
html/head.html

@ -5,6 +5,7 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<base href="<%.Cfg.B%>"> <base href="<%.Cfg.B%>">
<%if.Csp%><link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/csp.min.css"><%end%>
<link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/font-awesome.min.css"> <link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/main.min.css"> <link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/main.min.css">

4
html/main.html

@ -1,5 +1,5 @@
<%define "mainPage"%><!doctype html> <%define "mainPage"%><!doctype html>
<html class="no-js"> <html class="no-js"<%if.Csp%> ng-csp<%end%>>
<head> <head>
<%template "head" .%> <%template "head" .%>
</head> </head>
@ -26,7 +26,7 @@
<button title="{{_('Share a file as presentation')}}" class="btn aenablebtn btn-presentation" ng-show="status=='connected' || status=='conference' || layout.presentation" ng-model="layout.presentation" btn-checkbox><i class="fa fa-folder-open-o"></i></button> <button title="{{_('Share a file as presentation')}}" class="btn aenablebtn btn-presentation" ng-show="status=='connected' || status=='conference' || layout.presentation" ng-model="layout.presentation" btn-checkbox><i class="fa fa-folder-open-o"></i></button>
<button title="{{_('Share your screen')}}" class="btn aenablebtn btn-screenshare" ng-disabled="!supported.screensharing" ng-show="status=='connected' || status=='conference' || layout.screenshare" ng-model="layout.screenshare" btn-checkbox><i class="fa fa-desktop"></i></button> <button title="{{_('Share your screen')}}" class="btn aenablebtn btn-screenshare" ng-disabled="!supported.screensharing" ng-show="status=='connected' || status=='conference' || layout.screenshare" ng-model="layout.screenshare" btn-checkbox><i class="fa fa-desktop"></i></button>
<button title="{{_('Chat')}}" class="btn btn-chat" ng-class="{messagesunseen: chatMessagesUnseen>0}" ng-model="layout.chat" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-comments-o"></i><span class="badge" ng-show="chatMessagesUnseen" ng-bind="chatMessagesUnseen"></span></button> <button title="{{_('Chat')}}" class="btn btn-chat" ng-class="{messagesunseen: chatMessagesUnseen>0}" ng-model="layout.chat" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-comments-o"></i><span class="badge" ng-show="chatMessagesUnseen" ng-bind="chatMessagesUnseen"></span></button>
<button ng-show="myid && myuserid" title="{{_('Contacts')}}" class="btn btn-contacts" ng-click="openContactsManager()"><i class="fa fa-bookmark-o"></i></button> <button ng-show="myid && myuserid" title="{{_('Contacts')}}" class="btn btn-contacts" ng-click="openContactsManager()"><i class="fa fa-sitemap"></i></button>
<button title="{{_('Mute microphone')}}" class="btn btn-mutemicrophone amutebtn" ng-model="microphoneMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button> <button title="{{_('Mute microphone')}}" class="btn btn-mutemicrophone amutebtn" ng-model="microphoneMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button>
<button title="{{_('Turn camera off')}}" class="btn btn-mutecamera amutebtn" ng-model="cameraMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button> <button title="{{_('Turn camera off')}}" class="btn btn-mutecamera amutebtn" ng-model="cameraMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button>
<button title="{{_('Settings')}}" class="btn btn-settings" ng-model="layout.settings" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-cog"></i></button> <button title="{{_('Settings')}}" class="btn btn-settings" ng-model="layout.settings" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-cog"></i></button>

15
server.conf.in

@ -78,6 +78,9 @@ encryptionSecret = tne-default-encryption-block-key
; all users will join this room if enabled. If it is disabled then a room join ; all users will join this room if enabled. If it is disabled then a room join
; form will be shown instead. ; form will be shown instead.
;defaultRoomEnabled = true ;defaultRoomEnabled = true
; Whether a user account is required to create a room. This only has an effect
; if user accounts are enabled. Optional, defaults to false.
;authorizeRoomCreation = false
; Server token is a public random string which is used to enhance security of ; Server token is a public random string which is used to enhance security of
; server generated security tokens. When the serverToken is changed all existing ; server generated security tokens. When the serverToken is changed all existing
; nonces become invalid. Use 32 or 64 characters (eg. 16 or 32 byte hex). ; nonces become invalid. Use 32 or 64 characters (eg. 16 or 32 byte hex).
@ -98,6 +101,18 @@ serverRealm = local
; a front end webserver. Check the doc folder for more info about plugins and ; a front end webserver. Check the doc folder for more info about plugins and
; examples. ; examples.
;plugin = extra/static/myplugin.js ;plugin = extra/static/myplugin.js
; Content-Security-Policy HTTP response header value.
; Spreed WebRTC requires inline styles, WebSocket connection to itself and
; data: URL for images.
; The currently recommended CSP is:
; default-src 'self';
; style-src 'self' 'unsafe-inline';
; img-src 'self' data:;
; connect-src 'self' wss://server:port/ws;
;contentSecurityPolicy =
; Content-Security-Policy-Report-Only HTTP response header value. Use this
; to test your CSP before putting it into production.
;contentSecurityPolicyReportOnly =
[log] [log]
;logfile = /var/log/spreed-webrtc-server.log ;logfile = /var/log/spreed-webrtc-server.log

18
src/app/spreed-webrtc-server/buffercache.go

@ -160,3 +160,21 @@ func (cache *bufferCache) New() Buffer {
func (cache *bufferCache) Wrap(data []byte) Buffer { func (cache *bufferCache) Wrap(data []byte) Buffer {
return &directBuffer{refcnt: 1, cache: cache, buf: bytes.NewBuffer(data)} return &directBuffer{refcnt: 1, cache: cache, buf: bytes.NewBuffer(data)}
} }
func readAll(dest Buffer, r io.Reader) error {
var err error
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
}
}()
_, err = dest.ReadFrom(r)
return err
}

38
src/app/spreed-webrtc-server/channeling.go

@ -21,10 +21,41 @@
package main package main
type DataError struct {
Type string
Code string
Message string
}
func NewDataError(code, message string) error {
return &DataError{"Error", code, message}
}
func (err *DataError) Error() string {
return err.Message
}
type DataRoomCredentials struct {
PIN string
}
type DataHello struct { type DataHello struct {
Version string Version string
Ua string Ua string
Id string Id string
Credentials *DataRoomCredentials
}
type DataWelcome struct {
Type string
Room *DataRoom
Users []*DataSession
}
type DataRoom struct {
Type string
Name string
Credentials *DataRoomCredentials
} }
type DataOffer struct { type DataOffer struct {
@ -159,6 +190,7 @@ type DataIncoming struct {
Alive *DataAlive Alive *DataAlive
Authentication *DataAuthentication Authentication *DataAuthentication
Sessions *DataSessions Sessions *DataSessions
Room *DataRoom
Iid string `json:",omitempty"` Iid string `json:",omitempty"`
} }

284
src/app/spreed-webrtc-server/channelling_api.go

@ -0,0 +1,284 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"log"
"strings"
"time"
)
const (
maxConferenceSize = 100
)
type ChannellingAPI interface {
OnConnect(Client, *Session)
OnIncoming(ResponseSender, *Session, *DataIncoming)
OnDisconnect(*Session)
}
type channellingAPI struct {
*Config
RoomStatusManager
SessionEncoder
SessionManager
StatsCounter
ContactManager
TurnDataCreator
Unicaster
Broadcaster
buddyImages ImageCache
}
func NewChannellingAPI(config *Config, roomStatus RoomStatusManager, sessionEncoder SessionEncoder, sessionManager SessionManager, statsCounter StatsCounter, contactManager ContactManager, turnDataCreator TurnDataCreator, unicaster Unicaster, broadcaster Broadcaster, buddyImages ImageCache) ChannellingAPI {
return &channellingAPI{
config,
roomStatus,
sessionEncoder,
sessionManager,
statsCounter,
contactManager,
turnDataCreator,
unicaster,
broadcaster,
buddyImages,
}
}
func (api *channellingAPI) OnConnect(client Client, session *Session) {
api.Unicaster.OnConnect(client, session)
api.SendSelf(client, session)
}
func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *DataIncoming) {
switch msg.Type {
case "Self":
api.SendSelf(c, session)
case "Hello":
//log.Println("Hello", msg.Hello, c.Index())
// TODO(longsleep): Filter room id and user agent.
api.UpdateSession(session, &SessionUpdate{Types: []string{"Ua"}, Ua: msg.Hello.Ua})
if session.Hello && session.Roomid != msg.Hello.Id {
api.LeaveRoom(session)
api.Broadcast(session, session.DataSessionLeft("soft"))
}
// NOTE(lcooper): Iid filtered for compatibility's sake.
// Evaluate sending unconditionally when supported by all clients.
if room, err := api.JoinRoom(msg.Hello.Id, msg.Hello.Credentials, session, c); err == nil {
session.Hello = true
session.Roomid = msg.Hello.Id
if msg.Iid != "" {
c.Reply(msg.Iid, &DataWelcome{
Type: "Welcome",
Room: room,
Users: api.RoomUsers(session),
})
}
api.Broadcast(session, session.DataSessionJoined())
} else {
session.Hello = false
if msg.Iid != "" {
c.Reply(msg.Iid, err)
}
}
case "Offer":
// TODO(longsleep): Validate offer
api.Unicast(session, msg.Offer.To, msg.Offer)
case "Candidate":
// TODO(longsleep): Validate candidate
api.Unicast(session, msg.Candidate.To, msg.Candidate)
case "Answer":
// TODO(longsleep): Validate Answer
api.Unicast(session, msg.Answer.To, msg.Answer)
case "Users":
if session.Hello {
sessions := &DataSessions{Type: "Users", Users: api.RoomUsers(session)}
c.Reply(msg.Iid, sessions)
}
case "Authentication":
st := msg.Authentication.Authentication
if st == nil {
return
}
if err := api.Authenticate(session, st, ""); err == nil {
log.Println("Authentication success", session.Userid)
api.SendSelf(c, session)
api.BroadcastSessionStatus(session)
} else {
log.Println("Authentication failed", err, st.Userid, st.Nonce)
}
case "Bye":
api.Unicast(session, msg.Bye.To, msg.Bye)
case "Status":
//log.Println("Status", msg.Status)
api.UpdateSession(session, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
api.BroadcastSessionStatus(session)
case "Chat":
// TODO(longsleep): Limit sent chat messages per incoming connection.
if !msg.Chat.Chat.NoEcho {
api.Unicast(session, session.Id, msg.Chat)
}
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339)
if msg.Chat.To == "" {
// TODO(longsleep): Check if chat broadcast is allowed.
if session.Hello {
api.CountBroadcastChat()
api.Broadcast(session, msg.Chat)
}
} else {
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil {
if err := api.contactrequestHandler(session, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest); err != nil {
log.Println("Ignoring invalid contact request.", err)
return
}
msg.Chat.Chat.Status.ContactRequest.Userid = session.Userid()
}
if msg.Chat.Chat.Status == nil {
api.CountUnicastChat()
}
api.Unicast(session, msg.Chat.To, msg.Chat)
if msg.Chat.Chat.Mid != "" {
// Send out delivery confirmation status chat message.
api.Unicast(session, session.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}})
}
}
case "Conference":
// Check conference maximum size.
if len(msg.Conference.Conference) > maxConferenceSize {
log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference))
} else {
// Send conference update to anyone.
for _, id := range msg.Conference.Conference {
if id != session.Id {
api.Unicast(session, id, msg.Conference)
}
}
}
case "Alive":
c.Reply(msg.Iid, msg.Alive)
case "Sessions":
var users []*DataSession
switch msg.Sessions.Sessions.Type {
case "contact":
if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil {
users = api.GetUserSessions(session, userID)
} else {
log.Printf(err.Error())
}
case "session":
id, err := session.attestation.Decode(msg.Sessions.Sessions.Token)
if err != nil {
log.Printf("Failed to decode incoming attestation", err, msg.Sessions.Sessions.Token)
break
}
session, ok := api.GetSession(id)
if !ok {
log.Printf("Cannot retrieve session for id %s", id)
break
}
users = make([]*DataSession, 1, 1)
users[0] = session.Data()
default:
log.Printf("Unkown incoming sessions request type %s", msg.Sessions.Sessions.Type)
}
// TODO(lcooper): We ought to reply with a *DataError here if failed.
if users != nil {
c.Reply(msg.Iid, &DataSessions{Type: "Sessions", Users: users, Sessions: msg.Sessions.Sessions})
}
case "Room":
if room, err := api.UpdateRoom(session, msg.Room); err == nil {
api.Broadcast(session, room)
c.Reply(msg.Iid, room)
} else {
c.Reply(msg.Iid, err)
}
default:
log.Println("OnText unhandled message type", msg.Type)
}
}
func (api *channellingAPI) OnDisconnect(session *Session) {
dsl := session.DataSessionLeft("hard")
if session.Hello {
api.LeaveRoom(session)
api.Broadcast(session, dsl)
}
session.RunForAllSubscribers(func(session *Session) {
log.Println("Notifying subscriber that we are gone", session.Id, session.Id)
api.Unicast(session, session.Id, dsl)
})
api.Unicaster.OnDisconnect(session)
api.buddyImages.Delete(session.Id)
}
func (api *channellingAPI) SendSelf(c Responder, session *Session) {
token, err := api.EncodeSessionToken(session)
if err != nil {
log.Println("Error in OnRegister", err)
return
}
log.Println("Created new session token", len(token), token)
self := &DataSelf{
Type: "Self",
Id: session.Id,
Sid: session.Sid,
Userid: session.Userid(),
Suserid: api.EncodeSessionUserID(session),
Token: token,
Version: api.Version,
Turn: api.CreateTurnData(session),
Stun: api.StunURIs,
}
c.Reply("", self)
}
func (api *channellingAPI) UpdateSession(session *Session, s *SessionUpdate) uint64 {
if s.Status != nil {
status, ok := s.Status.(map[string]interface{})
if ok && status["buddyPicture"] != nil {
pic := status["buddyPicture"].(string)
if strings.HasPrefix(pic, "data:") {
imageId := api.buddyImages.Update(session.Id, pic[5:])
if imageId != "" {
status["buddyPicture"] = "img:" + imageId
}
}
}
}
return session.Update(s)
}
func (api *channellingAPI) BroadcastSessionStatus(session *Session) {
if session.Hello {
api.Broadcast(session, session.DataSessionStatus())
}
}

240
src/app/spreed-webrtc-server/channelling_api_test.go

@ -0,0 +1,240 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"errors"
"testing"
)
type fakeClient struct {
replies map[string]interface{}
}
func (fake *fakeClient) Send(_ Buffer) {
}
func (fake *fakeClient) Reply(iid string, msg interface{}) {
if fake.replies == nil {
fake.replies = make(map[string]interface{})
}
fake.replies[iid] = msg
}
type fakeRoomManager struct {
joinedRoomID string
leftRoomID string
roomUsers []*DataSession
joinedID string
joinError error
leftID string
broadcasts []interface{}
updatedRoom *DataRoom
updateError error
}
func (fake *fakeRoomManager) RoomUsers(session *Session) []*DataSession {
return fake.roomUsers
}
func (fake *fakeRoomManager) JoinRoom(id string, _ *DataRoomCredentials, session *Session, _ Sender) (*DataRoom, error) {
fake.joinedID = id
return &DataRoom{Name: id}, fake.joinError
}
func (fake *fakeRoomManager) LeaveRoom(session *Session) {
fake.leftID = session.Roomid
}
func (fake *fakeRoomManager) Broadcast(_ *Session, msg interface{}) {
fake.broadcasts = append(fake.broadcasts, msg)
}
func (fake *fakeRoomManager) UpdateRoom(_ *Session, _ *DataRoom) (*DataRoom, error) {
return fake.updatedRoom, fake.updateError
}
func assertReply(t *testing.T, client *fakeClient, iid string) interface{} {
msg, ok := client.replies[iid]
if !ok {
t.Fatalf("No response received for Iid %v", iid)
}
return msg
}
func assertErrorReply(t *testing.T, client *fakeClient, iid, code string) {
err, ok := assertReply(t, client, iid).(*DataError)
if !ok {
t.Fatalf("Expected response message to be an Error")
}
if err.Type != "Error" {
t.Error("Message did not have the correct type")
}
if err.Code != code {
t.Errorf("Expected error code to be %v, but was %v", code, err.Code)
}
}
func NewTestChannellingAPI() (ChannellingAPI, *fakeClient, *Session, *fakeRoomManager) {
client, roomManager, session := &fakeClient{}, &fakeRoomManager{}, &Session{}
return NewChannellingAPI(nil, roomManager, nil, nil, nil, nil, nil, nil, roomManager, nil), client, session, roomManager
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing.T) {
roomID, ua := "foobar", "unit tests"
api, client, session, roomManager := NewTestChannellingAPI()
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID, Ua: ua}})
if roomManager.joinedID != roomID {
t.Errorf("Expected to have joined room %v, but got %v", roomID, roomManager.joinedID)
}
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 1 {
t.Fatalf("Expected 1 broadcast, but got %d", broadcastCount)
}
dataSession, ok := roomManager.broadcasts[0].(*DataSession)
if !ok {
t.Fatal("Expected a session data broadcast")
}
if dataSession.Ua != ua {
t.Errorf("Expected to have broadcasted a user agent of %v, but was %v", ua, dataSession.Ua)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_LeavesAnyPreviouslyJoinedRooms(t *testing.T) {
roomID := "foobar"
api, client, session, roomManager := NewTestChannellingAPI()
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID}})
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: "baz"}})
if roomManager.leftID != roomID {
t.Errorf("Expected to have left room %v, but got %v", roomID, roomManager.leftID)
}
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 3 {
t.Fatalf("Expected 3 broadcasts, but got %d", broadcastCount)
}
dataSession, ok := roomManager.broadcasts[1].(*DataSession)
if !ok {
t.Fatal("Expected a session data broadcast")
}
if status := "soft"; dataSession.Status != status {
t.Errorf("Expected to have broadcast a leave status of of %v, but was %v", status, dataSession.Status)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_DoesNotJoinIfNotPermitted(t *testing.T) {
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.joinError = errors.New("Can't enter this room")
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{}})
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 0 {
t.Fatalf("Expected no broadcasts, but got %d", broadcastCount)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAWelcome(t *testing.T) {
iid, roomID := "foo", "a-room"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.roomUsers = []*DataSession{&DataSession{}}
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{Id: roomID}})
msg, ok := client.replies[iid]
if !ok {
t.Fatalf("No response received for Iid %v", iid)
}
welcome, ok := msg.(*DataWelcome)
if !ok {
t.Fatalf("Expected response message %#v to be a Welcome", msg)
}
if welcome.Type != "Welcome" {
t.Error("Message did not have the correct type")
}
if welcome.Room == nil || welcome.Room.Name != roomID {
t.Errorf("Expected room with name %v, but got %#v", roomID, welcome.Room)
}
if len(welcome.Users) != len(roomManager.roomUsers) {
t.Errorf("Expected to get users %#v, but was %#v", roomManager.roomUsers, welcome.Users)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAnErrorIfTheRoomCannotBeJoined(t *testing.T) {
iid := "foo"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.joinError = NewDataError("bad_join", "")
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{}})
assertErrorReply(t, client, iid, "bad_join")
}
func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpdatedRoom(t *testing.T) {
iid, roomName := "123", "foo"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.updatedRoom = &DataRoom{Name: "FOO"}
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}})
api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}})
room, ok := assertReply(t, client, iid).(*DataRoom)
if !ok {
t.Fatalf("Expected response message to be a Room")
}
if room.Name != roomManager.updatedRoom.Name {
t.Errorf("Expected updated room with name %v, but got %#v", roomManager.updatedRoom, room)
}
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 2 {
t.Fatalf("Expected 1 broadcasts, but got %d", broadcastCount)
}
if _, ok := roomManager.broadcasts[1].(*DataRoom); !ok {
t.Fatal("Expected a room data broadcast")
}
}
func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAnErrorIfUpdatingTheRoomFails(t *testing.T) {
iid, roomName := "123", "foo"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.updateError = NewDataError("a_room_error", "")
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}})
api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}})
assertErrorReply(t, client, iid, "a_room_error")
}

87
src/app/spreed-webrtc-server/client.go

@ -0,0 +1,87 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"log"
)
type Sender interface {
Send(Buffer)
}
type ResponseSender interface {
Sender
Responder
}
type Responder interface {
Reply(iid string, m interface{})
}
type Client interface {
ResponseSender
Session() *Session
Index() uint64
Close(bool)
}
type client struct {
Codec
ChannellingAPI
Connection
session *Session
}
func NewClient(codec Codec, api ChannellingAPI, session *Session) *client {
return &client{codec, api, nil, session}
}
func (client *client) OnConnect(conn Connection) {
client.Connection = conn
client.ChannellingAPI.OnConnect(client, client.session)
}
func (client *client) OnText(b Buffer) {
if incoming, err := client.DecodeIncoming(b); err == nil {
client.OnIncoming(client, client.session, incoming)
} else {
log.Println("OnText error while decoding JSON", err)
log.Printf("JSON:\n%s\n", b)
}
}
func (client *client) OnDisconnect() {
client.ChannellingAPI.OnDisconnect(client.session)
}
func (client *client) Reply(iid string, m interface{}) {
outgoing := &DataOutgoing{From: client.session.Id, Iid: iid, Data: m}
if b, err := client.EncodeOutgoing(outgoing); err == nil {
client.Send(b)
b.Decref()
}
}
func (client *client) Session() *Session {
return client.session
}

43
src/app/spreed-webrtc-server/common_test.go

@ -0,0 +1,43 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"testing"
)
func assertDataError(t *testing.T, err error, code string) {
if err == nil {
t.Error("Expected an error, but none was returned")
return
}
dataError, ok := err.(*DataError)
if !ok {
t.Errorf("Expected error %#v to be a *DataError", err)
return
}
if code != dataError.Code {
t.Errorf("Expected error code to be %v, but was %v", code, dataError.Code)
}
}

121
src/app/spreed-webrtc-server/config.go

@ -24,47 +24,104 @@ package main
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time"
"github.com/strukturag/phoenix"
) )
type Config struct { type Config struct {
Title string // Title Title string // Title
ver string // Version (not exported to Javascript) ver string // Version (not exported to Javascript)
S string // Static URL prefix with version S string // Static URL prefix with version
B string // Base URL B string // Base URL
Token string // Server token Token string // Server token
StunURIs []string // STUN server URIs StunURIs []string // STUN server URIs
TurnURIs []string // TURN server URIs TurnURIs []string // TURN server URIs
Tokens bool // True when we got a tokens file Tokens bool // True when we got a tokens file
Version string // Server version number Version string // Server version number
UsersEnabled bool // Flag if users are enabled UsersEnabled bool // Flag if users are enabled
UsersAllowRegistration bool // Flag if users can register UsersAllowRegistration bool // Flag if users can register
UsersMode string // Users mode string UsersMode string // Users mode string
DefaultRoomEnabled bool // Flag if default room ("") is enabled DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load Plugin string // Plugin to load
globalRoomid string // Id of the global room (not exported to Javascript) AuthorizeRoomCreation bool // Whether a user account is required to create rooms
globalRoomID string // Id of the global room (not exported to Javascript)
contentSecurityPolicy string // HTML content security policy
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode
} }
func NewConfig(title, ver, runtimeVersion, basePath, serverToken string, stunURIs, turnURIs []string, tokens bool, globalRoomid string, defaultRoomEnabled, usersEnabled, usersAllowRegistration bool, usersMode, plugin string) *Config { func NewConfig(container phoenix.Container, tokens bool) *Config {
sv := fmt.Sprintf("static/ver=%s", ver) ver := container.GetStringDefault("app", "ver", "")
version := container.Version()
if version != "unreleased" {
ver = fmt.Sprintf("%s%s", ver, strings.Replace(version, ".", "", -1))
} else {
ts := fmt.Sprintf("%d", time.Now().Unix())
if ver == "" {
ver = ts
}
version = fmt.Sprintf("unreleased.%s", ts)
}
// Read base path from config and make sure it ends with a slash.
basePath := container.GetStringDefault("http", "basePath", "/")
if !strings.HasSuffix(basePath, "/") {
basePath = fmt.Sprintf("%s/", basePath)
}
if basePath != "/" {
container.Printf("Using '%s' base base path.", basePath)
}
//TODO(longsleep): When we have a database, generate this once from random source and store it.
serverToken := container.GetStringDefault("app", "serverToken", "i-did-not-change-the-public-token-boo")
stunURIsString := container.GetStringDefault("app", "stunURIs", "")
stunURIs := strings.Split(stunURIsString, " ")
trimAndRemoveDuplicates(&stunURIs)
turnURIsString := container.GetStringDefault("app", "turnURIs", "")
turnURIs := strings.Split(turnURIsString, " ")
trimAndRemoveDuplicates(&turnURIs)
return &Config{ return &Config{
Title: title, Title: container.GetStringDefault("app", "title", "Spreed WebRTC"),
ver: ver, ver: ver,
S: sv, S: fmt.Sprintf("static/ver=%s", ver),
B: basePath, B: basePath,
Token: serverToken, Token: serverToken,
StunURIs: stunURIs, StunURIs: stunURIs,
TurnURIs: turnURIs, TurnURIs: turnURIs,
Tokens: tokens, Tokens: tokens,
Version: runtimeVersion, Version: version,
UsersEnabled: usersEnabled, UsersEnabled: container.GetBoolDefault("users", "enabled", false),
UsersAllowRegistration: usersAllowRegistration, UsersAllowRegistration: container.GetBoolDefault("users", "allowRegistration", false),
UsersMode: usersMode, UsersMode: container.GetStringDefault("users", "mode", ""),
DefaultRoomEnabled: defaultRoomEnabled, DefaultRoomEnabled: container.GetBoolDefault("app", "defaultRoomEnabled", true),
Plugin: plugin, Plugin: container.GetStringDefault("app", "plugin", ""),
globalRoomid: globalRoomid, AuthorizeRoomCreation: container.GetBoolDefault("app", "authorizeRoomCreation", false),
globalRoomID: container.GetStringDefault("app", "globalRoom", ""),
contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""),
contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""),
} }
} }
func (config *Config) Get(request *http.Request) (int, interface{}, http.Header) { func (config *Config) Get(request *http.Request) (int, interface{}, http.Header) {
return 200, config, http.Header{"Content-Type": {"application/json; charset=utf-8"}} return 200, config, http.Header{"Content-Type": {"application/json; charset=utf-8"}}
} }
// Helper function to clean up string arrays.
func trimAndRemoveDuplicates(data *[]string) {
found := make(map[string]bool)
j := 0
for i, x := range *data {
x = strings.TrimSpace(x)
if len(x) > 0 && !found[x] {
found[x] = true
(*data)[j] = (*data)[i]
j++
}
}
*data = (*data)[:j]
}

164
src/app/spreed-webrtc-server/connection.go

@ -22,14 +22,13 @@
package main package main
import ( import (
"bytes"
"container/list" "container/list"
"github.com/gorilla/websocket"
"io" "io"
"log" "log"
"net/http"
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket"
) )
const ( const (
@ -54,110 +53,77 @@ const (
maxRatePerSecond = 20 maxRatePerSecond = 20
) )
type Connection struct { type Connection interface {
Index() uint64
Send(Buffer)
Close(runCallbacks bool)
readPump()
writePump()
}
type ConnectionHandler interface {
NewBuffer() Buffer
OnConnect(Connection)
OnText(Buffer)
OnDisconnect()
}
type connection struct {
// References. // References.
h *Hub
ws *websocket.Conn ws *websocket.Conn
request *http.Request handler ConnectionHandler
// Data handling. // Data handling.
condition *sync.Cond condition *sync.Cond
queue list.List queue list.List
mutex sync.Mutex mutex sync.Mutex
isClosed bool isClosed bool
isClosing bool
// Metadata.
Id string
Roomid string // Keep Roomid here for quick acess without locking c.Session.
Idx uint64
Session *Session
IsRegistered bool
Hello bool
Version string
}
func NewConnection(h *Hub, ws *websocket.Conn, request *http.Request) *Connection { // Debugging
Idx uint64
}
c := &Connection{ func NewConnection(index uint64, ws *websocket.Conn, handler ConnectionHandler) Connection {
h: h, c := &connection{
ws: ws, ws: ws,
request: request, handler: handler,
Idx: index,
} }
c.condition = sync.NewCond(&c.mutex) c.condition = sync.NewCond(&c.mutex)
return c return c
} }
func (c *Connection) close() { func (c *connection) Index() uint64 {
return c.Idx
}
if !c.isClosed { func (c *connection) Close(runCallbacks bool) {
c.ws.Close() c.mutex.Lock()
c.Session.Close() if c.isClosed {
c.mutex.Lock()
c.Session = nil
c.isClosed = true
for {
head := c.queue.Front()
if head == nil {
break
}
c.queue.Remove(head)
message := head.Value.(Buffer)
message.Decref()
}
c.condition.Signal()
c.mutex.Unlock() c.mutex.Unlock()
return
} }
if runCallbacks {
} c.handler.OnDisconnect()
func (c *Connection) register() error {
s := c.h.CreateSession(c.request, nil)
c.h.registerHandler(c, s)
return nil
}
func (c *Connection) reregister(token string) error {
if st, err := c.h.DecodeSessionToken(token); err == nil {
s := c.h.CreateSession(c.request, st)
c.h.registerHandler(c, s)
} else {
log.Println("Error while decoding session token", err)
c.register()
} }
return nil c.ws.Close()
c.isClosed = true
} for {
head := c.queue.Front()
func (c *Connection) unregister() { if head == nil {
c.isClosing = true break
c.h.unregisterHandler(c)
}
func (c *Connection) readAll(dest Buffer, r io.Reader) error {
var err error
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
} }
}() c.queue.Remove(head)
message := head.Value.(Buffer)
_, err = dest.ReadFrom(r) message.Decref()
return err }
c.condition.Signal()
c.mutex.Unlock()
} }
// readPump pumps messages from the websocket connection to the hub. // readPump pumps messages from the websocket connection to the hub.
func (c *Connection) readPump() { func (c *connection) readPump() {
c.ws.SetReadLimit(maxMessageSize) c.ws.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait)) c.ws.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error { c.ws.SetPongHandler(func(string) error {
@ -165,6 +131,10 @@ func (c *Connection) readPump() {
return nil return nil
}) })
times := list.New() times := list.New()
// NOTE(lcooper): This more or less assumes that the write pump is started.
c.handler.OnConnect(c)
for { for {
//fmt.Println("readPump wait nextReader", c.Idx) //fmt.Println("readPump wait nextReader", c.Idx)
op, r, err := c.ws.NextReader() op, r, err := c.ws.NextReader()
@ -177,12 +147,6 @@ func (c *Connection) readPump() {
} }
switch op { switch op {
case websocket.TextMessage: case websocket.TextMessage:
message := c.h.buffers.New()
err = c.readAll(message, r)
if err != nil {
message.Decref()
break
}
now := time.Now() now := time.Now()
if times.Len() == maxRatePerSecond { if times.Len() == maxRatePerSecond {
front := times.Front() front := times.Front()
@ -194,18 +158,23 @@ func (c *Connection) readPump() {
} }
} }
times.PushBack(now) times.PushBack(now)
c.h.server.OnText(c, message)
message := c.handler.NewBuffer()
err = readAll(message, r)
if err != nil {
message.Decref()
break
}
c.handler.OnText(message)
message.Decref() message.Decref()
} }
} }
c.unregister() c.Close(true)
c.ws.Close()
} }
// Write message to outbound queue. // Write message to outbound queue.
func (c *Connection) send(message Buffer) { func (c *connection) Send(message Buffer) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
if c.isClosed { if c.isClosed {
@ -223,8 +192,7 @@ func (c *Connection) send(message Buffer) {
} }
// writePump pumps messages from the queue to the websocket connection. // writePump pumps messages from the queue to the websocket connection.
func (c *Connection) writePump() { func (c *connection) writePump() {
var timer *time.Timer var timer *time.Timer
ping := false ping := false
@ -301,16 +269,16 @@ func (c *Connection) writePump() {
cleanup: cleanup:
//fmt.Println("writePump done") //fmt.Println("writePump done")
timer.Stop() timer.Stop()
c.ws.Close() c.Close(true)
} }
// Write ping message. // Write ping message.
func (c *Connection) ping() error { func (c *connection) ping() error {
return c.write(websocket.PingMessage, []byte{}) return c.write(websocket.PingMessage, []byte{})
} }
// Write writes a message with the given opCode and payload. // Write writes a message with the given opCode and payload.
func (c *Connection) write(opCode int, payload []byte) error { func (c *connection) write(opCode int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait)) c.ws.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(opCode, payload) return c.ws.WriteMessage(opCode, payload)
} }

1
src/app/spreed-webrtc-server/context.go

@ -26,6 +26,7 @@ type Context struct {
Cfg *Config Cfg *Config
Host string Host string
Ssl bool Ssl bool
Csp bool
Languages []string Languages []string
Room string `json:"-"` Room string `json:"-"`
Scheme string `json:"-"` Scheme string `json:"-"`

566
src/app/spreed-webrtc-server/hub.go

@ -22,21 +22,16 @@
package main package main
import ( import (
"bytes"
"crypto/aes" "crypto/aes"
"crypto/hmac" "crypto/hmac"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"log" "log"
"net/http"
"strings"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@ -46,85 +41,56 @@ const (
maxUsersLength = 5000 maxUsersLength = 5000
) )
// TODO(longsleep): Get rid of MessageRequest type. type SessionStore interface {
type MessageRequest struct { GetSession(id string) (session *Session, ok bool)
From string
To string
Message Buffer
Id string
} }
type HubStat struct { type Unicaster interface {
Rooms int `json:"rooms"` SessionStore
Connections int `json:"connections"` OnConnect(Client, *Session)
Sessions int `json:"sessions"` Unicast(session *Session, to string, m interface{})
Users int `json:"users"` OnDisconnect(*Session)
Count uint64 `json:"count"`
BroadcastChatMessages uint64 `json:"broadcastchatmessages"`
UnicastChatMessages uint64 `json:"unicastchatmessages"`
IdsInRoom map[string][]string `json:"idsinroom,omitempty"`
SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"`
UsersById map[string]*DataUser `json:"usersbyid,omitempty"`
ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"`
} }
type Hub struct { type ContactManager interface {
server *Server contactrequestHandler(*Session, string, *DataContactRequest) error
connectionTable map[string]*Connection getContactID(*Session, string) (string, error)
sessionTable map[string]*Session
roomTable map[string]*RoomWorker
userTable map[string]*User
fakesessionTable map[string]*Session
version string
config *Config
sessionSecret []byte
encryptionSecret []byte
turnSecret []byte
tickets *securecookie.SecureCookie
attestations *securecookie.SecureCookie
count uint64
mutex sync.RWMutex
buffers BufferCache
broadcastChatMessages uint64
unicastChatMessages uint64
buddyImages ImageCache
realm string
tokenName string
useridRetriever func(*http.Request) (string, error)
contacts *securecookie.SecureCookie
} }
func NewHub(version string, config *Config, sessionSecret, encryptionSecret, turnSecret []byte, realm string) *Hub { type TurnDataCreator interface {
CreateTurnData(*Session) *DataTurn
h := &Hub{ }
connectionTable: make(map[string]*Connection),
sessionTable: make(map[string]*Session), type ClientStats interface {
roomTable: make(map[string]*RoomWorker), ClientInfo(details bool) (int, map[string]*DataSession, map[string]string)
userTable: make(map[string]*User), }
fakesessionTable: make(map[string]*Session),
version: version, type Hub interface {
config: config, ClientStats
sessionSecret: sessionSecret, Unicaster
encryptionSecret: encryptionSecret, TurnDataCreator
turnSecret: turnSecret, ContactManager
realm: realm, }
}
if len(h.sessionSecret) < 32 { type hub struct {
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(h.sessionSecret)) OutgoingEncoder
clients map[string]Client
config *Config
turnSecret []byte
mutex sync.RWMutex
contacts *securecookie.SecureCookie
}
func NewHub(config *Config, sessionSecret, encryptionSecret, turnSecret []byte, encoder OutgoingEncoder) Hub {
h := &hub{
OutgoingEncoder: encoder,
clients: make(map[string]Client),
config: config,
turnSecret: turnSecret,
} }
h.tickets = securecookie.New(h.sessionSecret, h.encryptionSecret) h.contacts = securecookie.New(sessionSecret, encryptionSecret)
h.tickets.MaxAge(86400 * 30) // 30 days
h.tickets.HashFunc(sha256.New)
h.tickets.BlockFunc(aes.NewCipher)
h.attestations = securecookie.New(h.sessionSecret, nil)
h.attestations.MaxAge(300) // 5 minutes
h.tickets.HashFunc(sha256.New)
h.buffers = NewBufferCache(1024, bytes.MinRead)
h.buddyImages = NewImageCache()
h.tokenName = fmt.Sprintf("token@%s", h.realm)
h.contacts = securecookie.New(h.sessionSecret, h.encryptionSecret)
h.contacts.MaxAge(0) // Forever h.contacts.MaxAge(0) // Forever
h.contacts.HashFunc(sha256.New) h.contacts.HashFunc(sha256.New)
h.contacts.BlockFunc(aes.NewCipher) h.contacts.BlockFunc(aes.NewCipher)
@ -132,48 +98,27 @@ func NewHub(version string, config *Config, sessionSecret, encryptionSecret, tur
} }
func (h *Hub) Stat(details bool) *HubStat { func (h *hub) ClientInfo(details bool) (clientCount int, sessions map[string]*DataSession, connections map[string]string) {
h.mutex.RLock() h.mutex.RLock()
defer h.mutex.RUnlock() defer h.mutex.RUnlock()
stat := &HubStat{
Rooms: len(h.roomTable), clientCount = len(h.clients)
Connections: len(h.connectionTable),
Sessions: len(h.sessionTable),
Users: len(h.userTable),
Count: h.count,
BroadcastChatMessages: atomic.LoadUint64(&h.broadcastChatMessages),
UnicastChatMessages: atomic.LoadUint64(&h.unicastChatMessages),
}
if details { if details {
rooms := make(map[string][]string) sessions = make(map[string]*DataSession)
for roomid, room := range h.roomTable { for id, client := range h.clients {
sessions := make([]string, 0, len(room.connections)) sessions[id] = client.Session().Data()
for id := range room.connections {
sessions = append(sessions, id)
}
rooms[roomid] = sessions
}
stat.IdsInRoom = rooms
sessions := make(map[string]*DataSession)
for sessionid, session := range h.sessionTable {
sessions[sessionid] = session.Data()
}
stat.SessionsById = sessions
users := make(map[string]*DataUser)
for userid, user := range h.userTable {
users[userid] = user.Data()
} }
stat.UsersById = users
connections := make(map[string]string) connections = make(map[string]string)
for id, connection := range h.connectionTable { for id, client := range h.clients {
connections[fmt.Sprintf("%d", connection.Idx)] = id connections[fmt.Sprintf("%d", client.Index())] = id
} }
stat.ConnectionsByIdx = connections
} }
return stat
return
} }
func (h *Hub) CreateTurnData(id string) *DataTurn { func (h *hub) CreateTurnData(session *Session) *DataTurn {
// Create turn data credentials for shared secret auth with TURN // Create turn data credentials for shared secret auth with TURN
// server. See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 // server. See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
@ -182,6 +127,7 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
if len(h.turnSecret) == 0 { if len(h.turnSecret) == 0 {
return &DataTurn{} return &DataTurn{}
} }
id := session.Id
bar := sha256.New() bar := sha256.New()
bar.Write([]byte(id)) bar.Write([]byte(id))
id = base64.StdEncoding.EncodeToString(bar.Sum(nil)) id = base64.StdEncoding.EncodeToString(bar.Sum(nil))
@ -194,389 +140,85 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
} }
func (h *Hub) CreateSuserid(session *Session) (suserid string) { func (h *hub) GetSession(id string) (session *Session, ok bool) {
userid := session.Userid() var client Client
if userid != "" { client, ok = h.GetClient(id)
m := hmac.New(sha256.New, h.encryptionSecret) if ok {
m.Write([]byte(userid)) session = client.Session()
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil))
} }
return return
} }
func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session { func (h *hub) OnConnect(client Client, session *Session) {
// Set flags.
var session *Session
var userid string
usersEnabled := h.config.UsersEnabled
if usersEnabled && h.useridRetriever != nil {
userid, _ = h.useridRetriever(request)
}
if st == nil {
sid := NewRandomString(32)
id, _ := h.tickets.Encode("id", sid)
session = NewSession(h, id, sid)
log.Println("Created new session id", id)
} else {
if userid == "" {
userid = st.Userid
}
if !usersEnabled {
userid = ""
}
session = NewSession(h, st.Id, st.Sid)
}
if userid != "" {
h.authenticateHandler(session, st, userid)
}
return session
}
func (h *Hub) CreateFakeSession(userid string) *Session {
h.mutex.Lock()
session, ok := h.fakesessionTable[userid]
if !ok {
sid := fmt.Sprintf("fake-%s", NewRandomString(27))
id, _ := h.tickets.Encode("id", sid)
log.Println("Created new fake session id", id)
session = NewSession(h, id, sid)
session.SetUseridFake(userid)
h.fakesessionTable[userid] = session
}
h.mutex.Unlock()
return session
}
func (h *Hub) ValidateSession(id, sid string) bool {
var decoded string
err := h.tickets.Decode("id", id, &decoded)
if err != nil {
log.Println("Session validation error", err, id, sid)
return false
}
if decoded != sid {
log.Println("Session validation failed", id, sid)
return false
}
return true
}
func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) {
return h.tickets.Encode(h.tokenName, st)
}
func (h *Hub) DecodeSessionToken(token string) (*SessionToken, error) {
st := &SessionToken{}
err := h.tickets.Decode(h.tokenName, token, st)
return st, err
}
func (h *Hub) GetRoom(id string) *RoomWorker {
h.mutex.RLock()
room, ok := h.roomTable[id]
if !ok {
h.mutex.RUnlock()
h.mutex.Lock()
// Need to re-check, another thread might have created the room
// while we waited for the lock.
room, ok = h.roomTable[id]
if !ok {
room = NewRoomWorker(h, id)
h.roomTable[id] = room
h.mutex.Unlock()
go func() {
// Start room, this blocks until room expired.
room.Start()
// Cleanup room when we are done.
h.mutex.Lock()
defer h.mutex.Unlock()
delete(h.roomTable, id)
log.Printf("Cleaned up room '%s'\n", id)
}()
} else {
h.mutex.Unlock()
}
} else {
h.mutex.RUnlock()
}
return room
}
func (h *Hub) GetGlobalConnections() []*Connection {
if h.config.globalRoomid == "" {
return make([]*Connection, 0)
}
h.mutex.RLock()
if room, ok := h.roomTable[h.config.globalRoomid]; ok {
h.mutex.RUnlock()
return room.GetConnections()
}
h.mutex.RUnlock()
return make([]*Connection, 0)
}
func (h *Hub) RunForAllRooms(f func(room *RoomWorker)) {
h.mutex.RLock()
for _, room := range h.roomTable {
f(room)
}
h.mutex.RUnlock()
}
func (h *Hub) isGlobalRoomid(id string) bool {
return id != "" && (id == h.config.globalRoomid)
}
func (h *Hub) isDefaultRoomid(id string) bool {
return id == ""
}
func (h *Hub) registerHandler(c *Connection, s *Session) {
// Apply session to connection.
c.Id = s.Id
c.Session = s
h.mutex.Lock() h.mutex.Lock()
// Set flags. log.Printf("Created client with id %s", session.Id)
h.count++
c.Idx = h.count
c.IsRegistered = true
// Register connection or replace existing one. // Register connection or replace existing one.
if ec, ok := h.connectionTable[c.Id]; ok { if ec, ok := h.clients[session.Id]; ok {
ec.IsRegistered = false ec.Close(false)
ec.close()
//log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id) //log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id)
} }
h.clients[session.Id] = client
h.connectionTable[c.Id] = c
h.sessionTable[c.Id] = s
//fmt.Println("registered", c.Id) //fmt.Println("registered", c.Id)
h.mutex.Unlock() h.mutex.Unlock()
//log.Printf("Register (%d) from %s: %s\n", c.Idx, c.Id) //log.Printf("Register (%d) from %s: %s\n", c.Idx, c.Id)
h.server.OnRegister(c)
} }
func (h *Hub) unregisterHandler(c *Connection) { func (h *hub) OnDisconnect(session *Session) {
h.mutex.Lock() h.mutex.Lock()
if !c.IsRegistered { delete(h.clients, session.Id)
h.mutex.Unlock()
return
}
suserid := c.Session.Userid()
delete(h.connectionTable, c.Id)
delete(h.sessionTable, c.Id)
if suserid != "" {
user, ok := h.userTable[suserid]
if ok {
empty := user.RemoveSession(c.Session)
if empty {
delete(h.userTable, suserid)
}
}
}
h.mutex.Unlock() h.mutex.Unlock()
h.buddyImages.Delete(c.Id)
//log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id)
h.server.OnUnregister(c)
c.close()
} }
func (h *Hub) unicastHandler(m *MessageRequest) { func (h *hub) GetClient(id string) (client Client, ok bool) {
h.mutex.RLock() h.mutex.RLock()
out, ok := h.connectionTable[m.To] client, ok = h.clients[id]
h.mutex.RUnlock() h.mutex.RUnlock()
if !ok { return
log.Println("Unicast To not found", m.To)
return
}
out.send(m.Message)
} }
func (h *Hub) aliveHandler(c *Connection, alive *DataAlive, iid string) { func (h *hub) Unicast(session *Session, to string, m interface{}) {
outgoing := &DataOutgoing{
aliveJson := h.buffers.New() From: session.Id,
encoder := json.NewEncoder(aliveJson) To: to,
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: alive, Iid: iid}) A: session.Attestation(),
if err != nil { Data: m,
log.Println("Alive error while encoding JSON", err)
aliveJson.Decref()
return
} }
c.send(aliveJson) if message, err := h.EncodeOutgoing(outgoing); err == nil {
aliveJson.Decref() client, ok := h.GetClient(to)
}
func (h *Hub) sessionsHandler(c *Connection, srq *DataSessionsRequest, iid string) {
var users []*DataSession
switch srq.Type {
case "contact":
contact := &Contact{}
err := h.contacts.Decode("contact", srq.Token, contact)
if err != nil {
log.Println("Failed to decode incoming contact token", err, srq.Token)
return
}
// Use the userid which is not ours from the contact data.
var userid string
suserid := c.Session.Userid()
if contact.A == suserid {
userid = contact.B
} else if contact.B == suserid {
userid = contact.A
}
if userid == "" {
log.Println("Ignoring foreign contact token", contact.A, contact.B)
return
}
// Find foreign user.
h.mutex.RLock()
user, ok := h.userTable[userid]
h.mutex.RUnlock()
if !ok { if !ok {
// No user. Create fake session. log.Println("Unicast To not found", to)
users = make([]*DataSession, 1, 1)
users[0] = h.CreateFakeSession(userid).Data()
} else {
// Add sessions for forein user.
users = user.SubscribeSessions(c.Session)
}
case "session":
id, err := c.Session.attestation.Decode(srq.Token)
if err != nil {
log.Println("Failed to decode incoming attestation", err, srq.Token)
return return
} }
h.mutex.RLock() client.Send(message)
session, ok := h.sessionTable[id] message.Decref()
h.mutex.RUnlock()
if !ok {
return
}
users = make([]*DataSession, 1, 1)
users[0] = session.Data()
default:
log.Println("Unkown incoming sessions request type", srq.Type)
} }
if users != nil {
sessions := &DataSessions{Type: "Sessions", Users: users, Sessions: srq}
sessionsJson := h.buffers.New()
encoder := json.NewEncoder(sessionsJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions, Iid: iid})
if err != nil {
log.Println("Sessions error while encoding JSON", err)
sessionsJson.Decref()
return
}
c.send(sessionsJson)
sessionsJson.Decref()
}
} }
func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 { func (h *hub) getContactID(session *Session, token string) (userid string, err error) {
contact := &Contact{}
//fmt.Println("Userupdate", u) err = h.contacts.Decode("contact", token, contact)
h.mutex.RLock()
session, ok := h.sessionTable[s.Id]
h.mutex.RUnlock()
var rev uint64
if ok {
if s.Status != nil {
status, ok := s.Status.(map[string]interface{})
if ok && status["buddyPicture"] != nil {
pic := status["buddyPicture"].(string)
if strings.HasPrefix(pic, "data:") {
imageId := h.buddyImages.Update(s.Id, pic[5:])
if imageId != "" {
status["buddyPicture"] = "img:" + imageId
}
}
}
}
rev = session.Update(s)
} else {
log.Printf("Update data for unknown user %s\n", s.Id)
}
return rev
}
func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) {
h.mutex.RLock()
c, ok := h.connectionTable[st.Id]
h.mutex.RUnlock()
if !ok {
return "", errors.New("no such connection")
}
nonce, err := c.Session.Authorize(h.realm, st)
if err != nil { if err != nil {
return "", err err = fmt.Errorf("Failed to decode incoming contact token", err, token)
return
} }
// Use the userid which is not ours from the contact data.
return nonce, nil suserid := session.Userid()
if contact.A == suserid {
} userid = contact.B
} else if contact.B == suserid {
func (h *Hub) authenticateHandler(session *Session, st *SessionToken, userid string) error { userid = contact.A
err := session.Authenticate(h.realm, st, userid)
if err == nil {
// Authentication success.
suserid := session.Userid()
h.mutex.Lock()
user, ok := h.userTable[suserid]
if !ok {
user = NewUser(suserid)
h.userTable[suserid] = user
}
h.mutex.Unlock()
user.AddSession(session)
} }
if userid == "" {
return err err = fmt.Errorf("Ignoring foreign contact token", contact.A, contact.B)
}
return
} }
func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactRequest) error { func (h *hub) contactrequestHandler(session *Session, to string, cr *DataContactRequest) error {
var err error var err error
@ -588,13 +230,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq
if err != nil { if err != nil {
return err return err
} }
suserid := c.Session.Userid() suserid := session.Userid()
if suserid == "" { if suserid == "" {
return errors.New("no userid") return errors.New("no userid")
} }
h.mutex.RLock() session, ok := h.GetSession(to)
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
if !ok { if !ok {
return errors.New("unknown to session for confirm") return errors.New("unknown to session for confirm")
} }
@ -616,13 +256,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq
} else { } else {
// New request. // New request.
// Create Token with flag and c.Session.Userid and the to Session.Userid. // Create Token with flag and c.Session.Userid and the to Session.Userid.
suserid := c.Session.Userid() suserid := session.Userid()
if suserid == "" { if suserid == "" {
return errors.New("no userid") return errors.New("no userid")
} }
h.mutex.RLock() session, ok := h.GetSession(to)
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
if !ok { if !ok {
return errors.New("unknown to session") return errors.New("unknown to session")
} }

69
src/app/spreed-webrtc-server/incoming_codec.go

@ -0,0 +1,69 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"bytes"
"encoding/json"
"log"
)
type IncomingDecoder interface {
DecodeIncoming(Buffer) (*DataIncoming, error)
}
type OutgoingEncoder interface {
EncodeOutgoing(*DataOutgoing) (Buffer, error)
}
type Codec interface {
NewBuffer() Buffer
IncomingDecoder
OutgoingEncoder
}
type incomingCodec struct {
buffers BufferCache
}
func NewCodec() Codec {
return &incomingCodec{NewBufferCache(1024, bytes.MinRead)}
}
func (codec incomingCodec) NewBuffer() Buffer {
return codec.buffers.New()
}
func (codec incomingCodec) DecodeIncoming(b Buffer) (*DataIncoming, error) {
incoming := &DataIncoming{}
return incoming, json.Unmarshal(b.Bytes(), incoming)
}
func (codec incomingCodec) EncodeOutgoing(outgoing *DataOutgoing) (Buffer, error) {
b := codec.NewBuffer()
if err := json.NewEncoder(b).Encode(outgoing); err != nil {
log.Println("Error while encoding JSON", err)
b.Decref()
return nil, err
}
return b, nil
}

181
src/app/spreed-webrtc-server/main.go

@ -40,7 +40,6 @@ import (
"path" "path"
goruntime "runtime" goruntime "runtime"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"time" "time"
) )
@ -63,21 +62,6 @@ func getRequestLanguages(r *http.Request, supportedLanguages []string) []string
} }
// Helper function to clean up string arrays.
func trimAndRemoveDuplicates(data *[]string) {
found := make(map[string]bool)
j := 0
for i, x := range *data {
x = strings.TrimSpace(x)
if len(x) > 0 && !found[x] {
found[x] = true
(*data)[j] = (*data)[i]
j++
}
}
*data = (*data)[:j]
}
func mainHandler(w http.ResponseWriter, r *http.Request) { func mainHandler(w http.ResponseWriter, r *http.Request) {
handleRoomView("", w, r) handleRoomView("", w, r)
@ -91,12 +75,12 @@ func roomHandler(w http.ResponseWriter, r *http.Request) {
} }
func makeImageHandler(hub *Hub, expires time.Duration) http.HandlerFunc { func makeImageHandler(buddyImages ImageCache, expires time.Duration) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
image := hub.buddyImages.Get(vars["imageid"]) image := buddyImages.Get(vars["imageid"])
if image == nil { if image == nil {
http.Error(w, "Unknown image", http.StatusNotFound) http.Error(w, "Unknown image", http.StatusNotFound)
return return
@ -125,6 +109,17 @@ func handleRoomView(room string, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Expires", "-1") w.Header().Set("Expires", "-1")
w.Header().Set("Cache-Control", "private, max-age=0") w.Header().Set("Cache-Control", "private, max-age=0")
csp := false
if config.contentSecurityPolicy != "" {
w.Header().Set("Content-Security-Policy", config.contentSecurityPolicy)
csp = true
}
if config.contentSecurityPolicyReportOnly != "" {
w.Header().Set("Content-Security-Policy-Report-Only", config.contentSecurityPolicyReportOnly)
csp = true
}
scheme := "http" scheme := "http"
// Detect if the request was made with SSL. // Detect if the request was made with SSL.
@ -142,7 +137,7 @@ func handleRoomView(room string, w http.ResponseWriter, r *http.Request) {
} }
// Prepare context to deliver to HTML.. // Prepare context to deliver to HTML..
context := &Context{Cfg: config, App: "main", Host: r.Host, Scheme: scheme, Ssl: ssl, Languages: langs, Room: room} context := &Context{Cfg: config, App: "main", Host: r.Host, Scheme: scheme, Ssl: ssl, Csp: csp, Languages: langs, Room: room}
// Get URL parameters. // Get URL parameters.
r.ParseForm() r.ParseForm()
@ -184,17 +179,6 @@ func runner(runtime phoenix.Runtime) error {
return fmt.Errorf("Unable to find client. Path correct and compiled css?") return fmt.Errorf("Unable to find client. Path correct and compiled css?")
} }
// Read base path from config and make sure it ends with a slash.
basePath, err := runtime.GetString("http", "basePath")
if err != nil {
basePath = "/"
} else {
if !strings.HasSuffix(basePath, "/") {
basePath = fmt.Sprintf("%s/", basePath)
}
log.Printf("Using '%s' base base path.", basePath)
}
statsEnabled, err := runtime.GetBool("http", "stats") statsEnabled, err := runtime.GetBool("http", "stats")
if err != nil { if err != nil {
statsEnabled = false statsEnabled = false
@ -223,6 +207,10 @@ func runner(runtime phoenix.Runtime) error {
} }
} }
if len(sessionSecret) < 32 {
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(sessionSecret))
}
var encryptionSecret []byte var encryptionSecret []byte
encryptionSecretString, err := runtime.GetString("app", "encryptionSecret") encryptionSecretString, err := runtime.GetString("app", "encryptionSecret")
if err != nil { if err != nil {
@ -242,109 +230,33 @@ func runner(runtime phoenix.Runtime) error {
} }
} }
tokenFile, err := runtime.GetString("app", "tokenFile")
if err == nil {
if !httputils.HasFilePath(path.Clean(tokenFile)) {
return fmt.Errorf("Unable to find token file at %s", tokenFile)
}
}
title, err := runtime.GetString("app", "title")
if err != nil {
title = "Spreed WebRTC"
}
ver, err := runtime.GetString("app", "ver")
if err != nil {
ver = ""
}
runtimeVersion := version
if version != "unreleased" {
ver1 := ver
if err != nil {
ver1 = ""
}
ver = fmt.Sprintf("%s%s", ver1, strings.Replace(version, ".", "", -1))
} else {
ts := fmt.Sprintf("%d", time.Now().Unix())
if err != nil {
ver = ts
}
runtimeVersion = fmt.Sprintf("unreleased.%s", ts)
}
turnURIsString, err := runtime.GetString("app", "turnURIs")
if err != nil {
turnURIsString = ""
}
turnURIs := strings.Split(turnURIsString, " ")
trimAndRemoveDuplicates(&turnURIs)
var turnSecret []byte var turnSecret []byte
turnSecretString, err := runtime.GetString("app", "turnSecret") turnSecretString, err := runtime.GetString("app", "turnSecret")
if err == nil { if err == nil {
turnSecret = []byte(turnSecretString) turnSecret = []byte(turnSecretString)
} }
stunURIsString, err := runtime.GetString("app", "stunURIs")
if err != nil {
stunURIsString = ""
}
stunURIs := strings.Split(stunURIsString, " ")
trimAndRemoveDuplicates(&stunURIs)
globalRoomid, err := runtime.GetString("app", "globalRoom")
if err != nil {
// Global room is disabled.
globalRoomid = ""
}
plugin, err := runtime.GetString("app", "plugin")
if err != nil {
plugin = ""
}
defaultRoomEnabled := true
defaultRoomEnabledString, err := runtime.GetString("app", "defaultRoomEnabled")
if err == nil {
defaultRoomEnabled = defaultRoomEnabledString == "true"
}
usersEnabled := false
usersEnabledString, err := runtime.GetString("users", "enabled")
if err == nil {
usersEnabled = usersEnabledString == "true"
}
usersAllowRegistration := false
usersAllowRegistrationString, err := runtime.GetString("users", "allowRegistration")
if err == nil {
usersAllowRegistration = usersAllowRegistrationString == "true"
}
serverToken, err := runtime.GetString("app", "serverToken")
if err != nil {
//TODO(longsleep): When we have a database, generate this once from random source and store it.
serverToken = "i-did-not-change-the-public-token-boo"
}
serverRealm, err := runtime.GetString("app", "serverRealm") serverRealm, err := runtime.GetString("app", "serverRealm")
if err != nil { if err != nil {
serverRealm = "local" serverRealm = "local"
} }
usersMode, _ := runtime.GetString("users", "mode")
// Create token provider. // Create token provider.
tokenFile, err := runtime.GetString("app", "tokenFile")
if err == nil {
if !httputils.HasFilePath(path.Clean(tokenFile)) {
return fmt.Errorf("Unable to find token file at %s", tokenFile)
}
}
var tokenProvider TokenProvider var tokenProvider TokenProvider
if tokenFile != "" { if tokenFile != "" {
log.Printf("Using token authorization from %s\n", tokenFile) log.Printf("Using token authorization from %s\n", tokenFile)
tokenProvider = TokenFileProvider(tokenFile) tokenProvider = TokenFileProvider(tokenFile)
} }
// Create configuration data structure. // Load remaining configuration items.
config = NewConfig(title, ver, runtimeVersion, basePath, serverToken, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, usersEnabled, usersAllowRegistration, usersMode, plugin) config = NewConfig(runtime, tokenProvider != nil)
// Load templates. // Load templates.
tt := template.New("") tt := template.New("")
@ -369,10 +281,7 @@ func runner(runtime phoenix.Runtime) error {
} }
// Create realm string from config. // Create realm string from config.
computedRealm := fmt.Sprintf("%s.%s", serverRealm, serverToken) computedRealm := fmt.Sprintf("%s.%s", serverRealm, config.Token)
// Create our hub instance.
hub := NewHub(runtimeVersion, config, sessionSecret, encryptionSecret, turnSecret, computedRealm)
// Set number of go routines if it is 1 // Set number of go routines if it is 1
if goruntime.GOMAXPROCS(0) == 1 { if goruntime.GOMAXPROCS(0) == 1 {
@ -405,7 +314,7 @@ func runner(runtime phoenix.Runtime) error {
// Create router. // Create router.
router := mux.NewRouter() router := mux.NewRouter()
r := router.PathPrefix(basePath).Subrouter().StrictSlash(true) r := router.PathPrefix(config.B).Subrouter().StrictSlash(true)
// HTTP listener support. // HTTP listener support.
if _, err = runtime.GetString("http", "listen"); err == nil { if _, err = runtime.GetString("http", "listen"); err == nil {
@ -426,12 +335,20 @@ func runner(runtime phoenix.Runtime) error {
} }
// Add handlers. // Add handlers.
buddyImages := NewImageCache()
codec := NewCodec()
roomManager := NewRoomManager(config, codec)
hub := NewHub(config, sessionSecret, encryptionSecret, turnSecret, codec)
tickets := NewTickets(sessionSecret, encryptionSecret, computedRealm)
sessionManager := NewSessionManager(config, tickets, sessionSecret)
statsManager := NewStatsManager(hub, roomManager, sessionManager)
channellingAPI := NewChannellingAPI(config, roomManager, tickets, sessionManager, statsManager, hub, hub, hub, roomManager, buddyImages)
r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler)) r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler))
r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(basePath, makeImageHandler(hub, time.Duration(24)*time.Hour))) r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(config.B, makeImageHandler(buddyImages, time.Duration(24)*time.Hour)))
r.Handle("/static/{path:.*}", http.StripPrefix(basePath, httputils.FileStaticServer(http.Dir(rootFolder)))) r.Handle("/static/{path:.*}", http.StripPrefix(config.B, httputils.FileStaticServer(http.Dir(rootFolder))))
r.Handle("/robots.txt", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static"))))) r.Handle("/robots.txt", http.StripPrefix(config.B, http.FileServer(http.Dir(path.Join(rootFolder, "static")))))
r.Handle("/favicon.ico", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static", "img"))))) r.Handle("/favicon.ico", http.StripPrefix(config.B, http.FileServer(http.Dir(path.Join(rootFolder, "static", "img")))))
r.Handle("/ws", makeWsHubHandler(hub)) r.Handle("/ws", makeWSHandler(statsManager, sessionManager, codec, channellingAPI))
r.HandleFunc("/{room}", httputils.MakeGzipHandler(roomHandler)) r.HandleFunc("/{room}", httputils.MakeGzipHandler(roomHandler))
// Add API end points. // Add API end points.
@ -440,16 +357,16 @@ func runner(runtime phoenix.Runtime) error {
api.AddResource(&Rooms{}, "/rooms") api.AddResource(&Rooms{}, "/rooms")
api.AddResource(config, "/config") api.AddResource(config, "/config")
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens") api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens")
if usersEnabled { if config.UsersEnabled {
// Create Users handler. // Create Users handler.
users := NewUsers(hub, usersMode, serverRealm, runtime) users := NewUsers(hub, tickets, sessionManager, config.UsersMode, serverRealm, runtime)
api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/") api.AddResource(&Sessions{tickets, hub, users}, "/sessions/{id}/")
if usersAllowRegistration { if config.UsersAllowRegistration {
api.AddResource(users, "/users") api.AddResource(users, "/users")
} }
} }
if statsEnabled { if statsEnabled {
api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats") api.AddResourceWithWrapper(&Stats{statsManager}, httputils.MakeGzipHandler, "/stats")
log.Println("Stats are enabled!") log.Println("Stats are enabled!")
} }
@ -457,7 +374,7 @@ func runner(runtime phoenix.Runtime) error {
if extraFolder != "" { if extraFolder != "" {
extraFolderStatic := path.Join(extraFolder, "static") extraFolderStatic := path.Join(extraFolder, "static")
if _, err = os.Stat(extraFolderStatic); err == nil { if _, err = os.Stat(extraFolderStatic); err == nil {
r.Handle("/extra/static/{path:.*}", http.StripPrefix(fmt.Sprintf("%sextra", basePath), httputils.FileStaticServer(http.Dir(extraFolder)))) r.Handle("/extra/static/{path:.*}", http.StripPrefix(fmt.Sprintf("%sextra", config.B), httputils.FileStaticServer(http.Dir(extraFolder))))
log.Printf("Added URL handler /extra/static/... for static files in %s/...\n", extraFolderStatic) log.Printf("Added URL handler /extra/static/... for static files in %s/...\n", extraFolderStatic)
} }
} }
@ -482,7 +399,7 @@ func boot() error {
return nil return nil
} }
return phoenix.NewServer("server", ""). return phoenix.NewServer("server", version).
Config(configPath). Config(configPath).
Log(logPath). Log(logPath).
CpuProfile(cpuprofile). CpuProfile(cpuprofile).

201
src/app/spreed-webrtc-server/room_manager.go

@ -0,0 +1,201 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"log"
"sync"
)
type RoomStatusManager interface {
RoomUsers(*Session) []*DataSession
JoinRoom(string, *DataRoomCredentials, *Session, Sender) (*DataRoom, error)
LeaveRoom(*Session)
UpdateRoom(*Session, *DataRoom) (*DataRoom, error)
}
type Broadcaster interface {
Broadcast(*Session, interface{})
}
type RoomStats interface {
RoomInfo(includeSessions bool) (count int, sessionInfo map[string][]string)
}
type RoomManager interface {
RoomStatusManager
Broadcaster
RoomStats
}
type roomManager struct {
sync.RWMutex
*Config
OutgoingEncoder
roomTable map[string]RoomWorker
}
func NewRoomManager(config *Config, encoder OutgoingEncoder) RoomManager {
return &roomManager{
sync.RWMutex{},
config,
encoder,
make(map[string]RoomWorker),
}
}
func (rooms *roomManager) RoomUsers(session *Session) []*DataSession {
if room, ok := rooms.Get(session.Roomid); ok {
return room.GetUsers()
}
// TODO(lcooper): This should return an error.
return []*DataSession{}
}
func (rooms *roomManager) JoinRoom(id string, credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) {
if id == "" && !rooms.DefaultRoomEnabled {
return nil, NewDataError("default_room_disabled", "The default room is not enabled")
}
roomWorker, err := rooms.GetOrCreate(id, credentials, session)
if err != nil {
return nil, err
}
return roomWorker.Join(credentials, session, sender)
}
func (rooms *roomManager) LeaveRoom(session *Session) {
if room, ok := rooms.Get(session.Roomid); ok {
room.Leave(session)
}
}
func (rooms *roomManager) UpdateRoom(session *Session, room *DataRoom) (*DataRoom, error) {
if !session.Hello || session.Roomid != room.Name {
return nil, NewDataError("not_in_room", "Cannot update other rooms")
}
// XXX(lcooper): We'll process and send documents without this field
// correctly, however clients cannot not handle it currently.
room.Type = "Room"
if roomWorker, ok := rooms.Get(session.Roomid); ok {
return room, roomWorker.Update(room)
}
// TODO(lcooper): We should almost certainly return an error in this case.
return room, nil
}
func (rooms *roomManager) Broadcast(session *Session, m interface{}) {
outgoing := &DataOutgoing{
From: session.Id,
A: session.Attestation(),
Data: m,
}
message, err := rooms.EncodeOutgoing(outgoing)
if err != nil {
return
}
id := session.Roomid
if id != "" && id == rooms.globalRoomID {
rooms.RLock()
for _, room := range rooms.roomTable {
room.Broadcast(session, message)
}
rooms.RUnlock()
} else if room, ok := rooms.Get(id); ok {
room.Broadcast(session, message)
} else {
log.Printf("No room named %s found for broadcast message %#v", id, m)
}
message.Decref()
}
func (rooms *roomManager) RoomInfo(includeSessions bool) (count int, sessionInfo map[string][]string) {
rooms.RLock()
defer rooms.RUnlock()
count = len(rooms.roomTable)
if includeSessions {
sessionInfo := make(map[string][]string)
for roomid, room := range rooms.roomTable {
sessionInfo[roomid] = room.SessionIDs()
}
}
return
}
func (rooms *roomManager) Get(id string) (room RoomWorker, ok bool) {
rooms.RLock()
room, ok = rooms.roomTable[id]
rooms.RUnlock()
return
}
func (rooms *roomManager) GetOrCreate(id string, credentials *DataRoomCredentials, session *Session) (RoomWorker, error) {
if room, ok := rooms.Get(id); ok {
return room, nil
}
rooms.Lock()
// Need to re-check, another thread might have created the room
// while we waited for the lock.
if room, ok := rooms.roomTable[id]; ok {
rooms.Unlock()
return room, nil
}
if rooms.UsersEnabled && rooms.AuthorizeRoomCreation && !session.Authenticated() {
rooms.Unlock()
return nil, NewDataError("room_join_requires_account", "Room creation requires a user account")
}
room := NewRoomWorker(rooms, id, credentials)
rooms.roomTable[id] = room
rooms.Unlock()
go func() {
// Start room, this blocks until room expired.
room.Start()
// Cleanup room when we are done.
rooms.Lock()
defer rooms.Unlock()
delete(rooms.roomTable, id)
log.Printf("Cleaned up room '%s'\n", id)
}()
return room, nil
}
func (rooms *roomManager) GlobalUsers() []*roomUser {
if rooms.globalRoomID == "" {
return make([]*roomUser, 0)
}
rooms.RLock()
if room, ok := rooms.roomTable[rooms.globalRoomID]; ok {
rooms.RUnlock()
return room.Users()
}
rooms.RUnlock()
return make([]*roomUser, 0)
}

79
src/app/spreed-webrtc-server/room_manager_test.go

@ -0,0 +1,79 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"testing"
)
func NewTestRoomManager() (RoomManager, *Config) {
config := &Config{}
return NewRoomManager(config, nil), config
}
func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenCreationRequiresAnAccount(t *testing.T) {
roomManager, config := NewTestRoomManager()
config.UsersEnabled = true
config.AuthorizeRoomCreation = true
unauthenticatedSession := &Session{}
_, err := roomManager.JoinRoom("foo", nil, unauthenticatedSession, nil)
assertDataError(t, err, "room_join_requires_account")
authenticatedSession := &Session{userid: "9870457"}
_, err = roomManager.JoinRoom("foo", nil, authenticatedSession, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while authenticated", err)
}
_, err = roomManager.JoinRoom("foo", nil, unauthenticatedSession, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while unauthenticated", err)
}
}
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.T) {
roomManager, _ := NewTestRoomManager()
_, err := roomManager.UpdateRoom(&Session{}, nil)
assertDataError(t, err, "not_in_room")
}
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfUpdatingAnUnjoinedRoom(t *testing.T) {
roomManager, _ := NewTestRoomManager()
session := &Session{Hello: true, Roomid: "foo"}
_, err := roomManager.UpdateRoom(session, &DataRoom{Name: "bar"})
assertDataError(t, err, "not_in_room")
}
func Test_RoomManager_UpdateRoom_ReturnsACorrectlyTypedDocument(t *testing.T) {
roomManager, _ := NewTestRoomManager()
session := &Session{Hello: true, Roomid: "foo"}
room, err := roomManager.UpdateRoom(session, &DataRoom{Name: session.Roomid})
if err != nil {
t.Fatalf("Unexpected error %v updating room", err)
}
if room.Type != "Room" {
t.Errorf("Expected document type to be Room, but was %v", room.Type)
}
}

189
src/app/spreed-webrtc-server/roomworker.go

@ -22,7 +22,7 @@
package main package main
import ( import (
"encoding/json" "crypto/subtle"
"log" "log"
"sync" "sync"
"time" "time"
@ -33,39 +33,53 @@ const (
roomExpiryDuration = 60 * time.Second roomExpiryDuration = 60 * time.Second
) )
type RoomConnectionUpdate struct { type RoomWorker interface {
Id string Start()
Sessionid string SessionIDs() []string
Status bool Users() []*roomUser
Connection *Connection Update(*DataRoom) error
GetUsers() []*DataSession
Broadcast(*Session, Buffer)
Join(*DataRoomCredentials, *Session, Sender) (*DataRoom, error)
Leave(*Session)
} }
type RoomWorker struct { type roomWorker struct {
// References. // References.
h *Hub manager *roomManager
// Data handling. // Data handling.
workers chan (func()) workers chan (func())
expired chan (bool) expired chan (bool)
connections map[string]*Connection users map[string]*roomUser
timer *time.Timer timer *time.Timer
mutex sync.RWMutex mutex sync.RWMutex
// Metadata. // Metadata.
Id string Id string
credentials *DataRoomCredentials
} }
func NewRoomWorker(h *Hub, id string) *RoomWorker { type roomUser struct {
*Session
Sender
}
func NewRoomWorker(manager *roomManager, id string, credentials *DataRoomCredentials) RoomWorker {
log.Printf("Creating worker for room '%s'\n", id) log.Printf("Creating worker for room '%s'\n", id)
r := &RoomWorker{ r := &roomWorker{
h: h, manager: manager,
Id: id, Id: id,
workers: make(chan func(), roomMaxWorkers),
expired: make(chan bool),
users: make(map[string]*roomUser),
}
if credentials != nil && len(credentials.PIN) > 0 {
r.credentials = credentials
} }
r.workers = make(chan func(), roomMaxWorkers)
r.expired = make(chan bool)
r.connections = make(map[string]*Connection)
// Create expire timer. // Create expire timer.
r.timer = time.AfterFunc(roomExpiryDuration, func() { r.timer = time.AfterFunc(roomExpiryDuration, func() {
@ -76,7 +90,7 @@ func NewRoomWorker(h *Hub, id string) *RoomWorker {
} }
func (r *RoomWorker) Start() { func (r *roomWorker) Start() {
// Main blocking worker. // Main blocking worker.
L: L:
@ -90,7 +104,7 @@ L:
//fmt.Println("Work room expired", r.Id) //fmt.Println("Work room expired", r.Id)
//fmt.Println("Work room expired", r.Id, len(r.connections)) //fmt.Println("Work room expired", r.Id, len(r.connections))
r.mutex.RLock() r.mutex.RLock()
if len(r.connections) == 0 { if len(r.users) == 0 {
// Cleanup room when it is empty. // Cleanup room when it is empty.
r.mutex.RUnlock() r.mutex.RUnlock()
log.Printf("Room worker not in use - cleaning up '%s'\n", r.Id) log.Printf("Room worker not in use - cleaning up '%s'\n", r.Id)
@ -107,19 +121,29 @@ L:
} }
func (r *RoomWorker) GetConnections() []*Connection { func (r *roomWorker) SessionIDs() []string {
r.mutex.RLock()
defer r.mutex.RUnlock()
sessions := make([]string, 0, len(r.users))
for id := range r.users {
sessions = append(sessions, id)
}
return sessions
}
func (r *roomWorker) Users() []*roomUser {
r.mutex.RLock() r.mutex.RLock()
defer r.mutex.RUnlock() defer r.mutex.RUnlock()
connections := make([]*Connection, 0, len(r.connections)) users := make([]*roomUser, 0, len(r.users))
for _, connection := range r.connections { for _, user := range r.users {
connections = append(connections, connection) users = append(users, user)
} }
return connections return users
} }
func (r *RoomWorker) Run(f func()) bool { func (r *roomWorker) Run(f func()) bool {
select { select {
case r.workers <- f: case r.workers <- f:
@ -131,13 +155,30 @@ func (r *RoomWorker) Run(f func()) bool {
} }
func (r *RoomWorker) usersHandler(c *Connection) { func (r *roomWorker) Update(room *DataRoom) error {
fault := make(chan error, 1)
worker := func() {
r.mutex.Lock()
if room.Credentials != nil {
if len(room.Credentials.PIN) > 0 {
r.credentials = room.Credentials
} else {
r.credentials = nil
}
}
r.mutex.Unlock()
fault <- nil
}
r.Run(worker)
return <-fault
}
func (r *roomWorker) GetUsers() []*DataSession {
out := make(chan []*DataSession, 1)
worker := func() { worker := func() {
sessions := &DataSessions{Type: "Users"}
var sl []*DataSession var sl []*DataSession
appender := func(ec *Connection) bool { appender := func(user *roomUser) bool {
ecsession := ec.Session ecsession := user.Session
if ecsession != nil { if ecsession != nil {
session := ecsession.Data() session := ecsession.Data()
session.Type = "Online" session.Type = "Online"
@ -150,73 +191,95 @@ func (r *RoomWorker) usersHandler(c *Connection) {
return true return true
} }
r.mutex.RLock() r.mutex.RLock()
sl = make([]*DataSession, 0, len(r.connections)) sl = make([]*DataSession, 0, len(r.users))
// Include connections in this room. // Include connections in this room.
for _, ec := range r.connections { for _, user := range r.users {
if !appender(ec) { if !appender(user) {
break break
} }
} }
r.mutex.RUnlock() r.mutex.RUnlock()
// Include connections to global room. // Include connections to global room.
for _, ec := range c.h.GetGlobalConnections() { for _, ec := range r.manager.GlobalUsers() {
if !appender(ec) { if !appender(ec) {
break break
} }
} }
sessions.Users = sl
sessionsJson := c.h.buffers.New()
encoder := json.NewEncoder(sessionsJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions})
if err != nil {
log.Println("Users error while encoding JSON", err)
sessionsJson.Decref()
return
}
c.send(sessionsJson)
sessionsJson.Decref()
out <- sl
} }
r.Run(worker) r.Run(worker)
return <-out
} }
func (r *RoomWorker) broadcastHandler(m *MessageRequest) { func (r *roomWorker) Broadcast(session *Session, message Buffer) {
worker := func() { worker := func() {
r.mutex.RLock() r.mutex.RLock()
defer r.mutex.RUnlock() defer r.mutex.RUnlock()
for id, ec := range r.connections { for id, user := range r.users {
if id == m.From { if id == session.Id {
// Skip broadcast to self. // Skip broadcast to self.
continue continue
} }
//fmt.Printf("%s\n", m.Message) //fmt.Printf("%s\n", m.Message)
ec.send(m.Message) user.Send(message)
} }
m.Message.Decref() message.Decref()
} }
m.Message.Incref() message.Incref()
r.Run(worker) r.Run(worker)
} }
func (r *RoomWorker) connectionHandler(rcu *RoomConnectionUpdate) { type joinResult struct {
*DataRoom
error
}
func (r *roomWorker) Join(credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) {
results := make(chan joinResult, 1)
worker := func() { worker := func() {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() if r.credentials == nil && credentials != nil {
if rcu.Status { results <- joinResult{nil, NewDataError("authorization_not_required", "No credentials may be provided for this room")}
r.connections[rcu.Sessionid] = rcu.Connection r.mutex.Unlock()
} else { return
if _, ok := r.connections[rcu.Sessionid]; ok { } else if r.credentials != nil {
delete(r.connections, rcu.Sessionid) if credentials == nil {
results <- joinResult{nil, NewDataError("authorization_required", "Valid credentials are required to join this room")}
r.mutex.Unlock()
return
}
if len(r.credentials.PIN) != len(credentials.PIN) || subtle.ConstantTimeCompare([]byte(r.credentials.PIN), []byte(credentials.PIN)) != 1 {
results <- joinResult{nil, NewDataError("invalid_credentials", "The provided credentials are incorrect")}
r.mutex.Unlock()
return
} }
} }
}
r.users[session.Id] = &roomUser{session, sender}
// NOTE(lcooper): Needs to be a copy, else we risk races with
// a subsequent modification of room properties.
result := joinResult{&DataRoom{Name: r.Id}, nil}
r.mutex.Unlock()
results <- result
}
r.Run(worker) r.Run(worker)
result := <-results
return result.DataRoom, result.error
}
func (r *roomWorker) Leave(session *Session) {
worker := func() {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, ok := r.users[session.Id]; ok {
delete(r.users, session.Id)
}
}
r.Run(worker)
} }

124
src/app/spreed-webrtc-server/roomworker_test.go

@ -0,0 +1,124 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"testing"
)
const (
testRoomName string = "a-room-name"
)
func NewTestRoomWorker() RoomWorker {
worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomName, nil)
go worker.Start()
return worker
}
func NewTestRoomWorkerWithPIN(t *testing.T) (RoomWorker, string) {
pin := "asdf"
worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomName, &DataRoomCredentials{PIN: pin})
go worker.Start()
return worker, pin
}
func Test_RoomWorker_Join_SucceedsWhenNoCredentialsAreRequired(t *testing.T) {
worker := NewTestRoomWorker()
_, err := worker.Join(nil, &Session{}, nil)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if userCount := len(worker.GetUsers()); userCount != 1 {
t.Errorf("Expected join to have been accepted but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfCredentialsAreGivenWhenUnneeded(t *testing.T) {
worker := NewTestRoomWorker()
_, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil)
assertDataError(t, err, "authorization_not_required")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfNoCredentialsAreGiven(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
_, err := worker.Join(nil, &Session{}, nil)
assertDataError(t, err, "authorization_required")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfIncorrectCredentialsAreGiven(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
_, err := worker.Join(&DataRoomCredentials{PIN: "adfs"}, &Session{}, nil)
assertDataError(t, err, "invalid_credentials")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_SucceedsWhenTheCorrectPINIsGiven(t *testing.T) {
worker, pin := NewTestRoomWorkerWithPIN(t)
if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil {
t.Fatalf("Unexpected error %v", err)
}
if len(worker.GetUsers()) < 1 {
t.Error("Expected join to have been accepted but room contains no users")
}
}
func Test_RoomWorker_Update_AllowsClearingCredentials(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
if err := worker.Update(&DataRoom{Credentials: &DataRoomCredentials{PIN: ""}}); err != nil {
t.Fatalf("Failed to update room: %v", err)
}
_, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil)
assertDataError(t, err, "authorization_not_required")
}
func Test_RoomWorker_Update_RetainsCredentialsWhenOtherPropertiesAreUpdated(t *testing.T) {
worker, pin := NewTestRoomWorkerWithPIN(t)
if err := worker.Update(&DataRoom{}); err != nil {
t.Fatalf("Failed to update room: %v", err)
}
if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil {
t.Fatalf("Unexpected error joining room %v", err)
}
}

280
src/app/spreed-webrtc-server/server.go

@ -1,280 +0,0 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"encoding/json"
"log"
"sync/atomic"
"time"
)
const (
maxConferenceSize = 100
)
type Server struct {
}
func (s *Server) OnRegister(c *Connection) {
//log.Println("OnRegister", c.id)
if token, err := c.h.EncodeSessionToken(c.Session.Token()); err == nil {
log.Println("Created new session token", len(token), token)
// Send stuff back.
s.Unicast(c, c.Id, &DataSelf{
Type: "Self",
Id: c.Id,
Sid: c.Session.Sid,
Userid: c.Session.Userid(),
Suserid: c.h.CreateSuserid(c.Session),
Token: token,
Version: c.h.version,
Turn: c.h.CreateTurnData(c.Id),
Stun: c.h.config.StunURIs,
})
} else {
log.Println("Error in OnRegister", c.Idx, err)
}
}
func (s *Server) OnUnregister(c *Connection) {
//log.Println("OnUnregister", c.id)
dsl := c.Session.DataSessionLeft("hard")
if c.Hello {
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid})
s.Broadcast(c, dsl)
}
c.Session.RunForAllSubscribers(func(session *Session) {
log.Println("Notifying subscriber that we are gone", c.Id, session.Id)
s.Unicast(c, session.Id, dsl)
})
}
func (s *Server) OnText(c *Connection, b Buffer) {
//log.Printf("OnText from %d: %s\n", c.Id, b)
var msg DataIncoming
err := json.Unmarshal(b.Bytes(), &msg)
if err != nil {
log.Println("OnText error while decoding JSON", err)
log.Printf("JSON:\n%s\n", b)
return
}
switch msg.Type {
case "Self":
s.OnRegister(c)
case "Hello":
//log.Println("Hello", msg.Hello, c.Idx)
// TODO(longsleep): Filter room id and user agent.
s.UpdateSession(c, &SessionUpdate{Types: []string{"Roomid", "Ua"}, Roomid: msg.Hello.Id, Ua: msg.Hello.Ua})
if c.Hello && c.Roomid != msg.Hello.Id {
// Room changed.
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid})
s.Broadcast(c, c.Session.DataSessionLeft("soft"))
}
c.Roomid = msg.Hello.Id
if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
c.Hello = true
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true})
s.Broadcast(c, c.Session.DataSessionJoined())
} else {
c.Hello = false
}
case "Offer":
// TODO(longsleep): Validate offer
s.Unicast(c, msg.Offer.To, msg.Offer)
case "Candidate":
// TODO(longsleep): Validate candidate
s.Unicast(c, msg.Candidate.To, msg.Candidate)
case "Answer":
// TODO(longsleep): Validate Answer
s.Unicast(c, msg.Answer.To, msg.Answer)
case "Users":
if c.Hello {
s.Users(c)
}
case "Authentication":
if msg.Authentication.Authentication != nil && s.Authenticate(c, msg.Authentication.Authentication) {
s.OnRegister(c)
if c.Hello {
s.Broadcast(c, c.Session.DataSessionStatus())
}
}
case "Bye":
s.Unicast(c, msg.Bye.To, msg.Bye)
case "Status":
//log.Println("Status", msg.Status)
s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
if c.Hello {
s.Broadcast(c, c.Session.DataSessionStatus())
}
case "Chat":
// TODO(longsleep): Limit sent chat messages per incoming connection.
if !msg.Chat.Chat.NoEcho {
s.Unicast(c, c.Id, msg.Chat)
}
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339)
if msg.Chat.To == "" {
// TODO(longsleep): Check if chat broadcast is allowed.
if c.Hello {
atomic.AddUint64(&c.h.broadcastChatMessages, 1)
s.Broadcast(c, msg.Chat)
}
} else {
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil {
err = s.ContactRequest(c, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest)
if err != nil {
log.Println("Ignoring invalid contact request.", err)
return
}
msg.Chat.Chat.Status.ContactRequest.Userid = c.Session.Userid()
}
atomic.AddUint64(&c.h.unicastChatMessages, 1)
s.Unicast(c, msg.Chat.To, msg.Chat)
if msg.Chat.Chat.Mid != "" {
// Send out delivery confirmation status chat message.
s.Unicast(c, c.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}})
}
}
case "Conference":
// Check conference maximum size.
if len(msg.Conference.Conference) > maxConferenceSize {
log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference))
} else {
// Send conference update to anyone.
for _, id := range msg.Conference.Conference {
if id != c.Id {
//log.Println("participant", id)
s.Unicast(c, id, msg.Conference)
}
}
}
case "Alive":
s.Alive(c, msg.Alive, msg.Iid)
case "Sessions":
s.Sessions(c, msg.Sessions.Sessions, msg.Iid)
default:
log.Println("OnText unhandled message type", msg.Type)
}
}
func (s *Server) Unicast(c *Connection, to string, m interface{}) {
outgoing := &DataOutgoing{From: c.Id, To: to, Data: m}
if !c.isClosing && c.Id != to {
outgoing.A = c.Session.Attestation()
}
b := c.h.buffers.New()
encoder := json.NewEncoder(b)
err := encoder.Encode(outgoing)
if err != nil {
b.Decref()
log.Println("Unicast error while encoding JSON", err)
return
}
//log.Println("Unicast", b)
var msg = &MessageRequest{From: c.Id, To: to, Message: b}
c.h.unicastHandler(msg)
b.Decref()
}
func (s *Server) Broadcast(c *Connection, m interface{}) {
b := c.h.buffers.New()
encoder := json.NewEncoder(b)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: m, A: c.Session.Attestation()})
if err != nil {
b.Decref()
log.Println("Broadcast error while encoding JSON", err)
return
}
if c.h.isGlobalRoomid(c.Roomid) {
c.h.RunForAllRooms(func(room *RoomWorker) {
var msg = &MessageRequest{From: c.Id, Message: b, Id: room.Id}
room.broadcastHandler(msg)
})
} else {
var msg = &MessageRequest{From: c.Id, Message: b, Id: c.Roomid}
room := c.h.GetRoom(c.Roomid)
room.broadcastHandler(msg)
}
b.Decref()
}
func (s *Server) Alive(c *Connection, alive *DataAlive, iid string) {
c.h.aliveHandler(c, alive, iid)
}
func (s *Server) Sessions(c *Connection, srq *DataSessionsRequest, iid string) {
c.h.sessionsHandler(c, srq, iid)
}
func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 {
su.Id = c.Id
return c.h.sessionupdateHandler(su)
}
func (s *Server) ContactRequest(c *Connection, to string, cr *DataContactRequest) (err error) {
return c.h.contactrequestHandler(c, to, cr)
}
func (s *Server) Users(c *Connection) {
room := c.h.GetRoom(c.Roomid)
room.usersHandler(c)
}
func (s *Server) Authenticate(c *Connection, st *SessionToken) bool {
err := c.h.authenticateHandler(c.Session, st, "")
if err == nil {
log.Println("Authentication success", c.Id, c.Idx, c.Session.Userid)
return true
} else {
log.Println("Authentication failed", err, c.Id, c.Idx, st.Userid, st.Nonce)
return false
}
}
func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) {
rcu.Sessionid = c.Id
rcu.Connection = c
room := c.h.GetRoom(c.Roomid)
room.connectionHandler(rcu)
}

35
src/app/spreed-webrtc-server/session.go

@ -39,26 +39,28 @@ type Session struct {
Status interface{} Status interface{}
Nonce string Nonce string
Prio int Prio int
Hello bool
Roomid string
mutex sync.RWMutex mutex sync.RWMutex
userid string userid string
fake bool fake bool
stamp int64 stamp int64
attestation *SessionAttestation attestation *SessionAttestation
attestations *securecookie.SecureCookie
subscriptions map[string]*Session subscriptions map[string]*Session
subscribers map[string]*Session subscribers map[string]*Session
h *Hub
} }
func NewSession(h *Hub, id, sid string) *Session { func NewSession(attestations *securecookie.SecureCookie, id, sid string) *Session {
session := &Session{ session := &Session{
Id: id, Id: id,
Sid: sid, Sid: sid,
Prio: 100, Prio: 100,
stamp: time.Now().Unix(), stamp: time.Now().Unix(),
attestations: attestations,
subscriptions: make(map[string]*Session), subscriptions: make(map[string]*Session),
subscribers: make(map[string]*Session), subscribers: make(map[string]*Session),
h: h,
} }
session.NewAttestation() session.NewAttestation()
return session return session
@ -171,6 +173,13 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
} }
func (s *Session) Authenticated() (authenticated bool) {
s.mutex.Lock()
authenticated = s.userid != ""
s.mutex.Unlock()
return
}
func (s *Session) Authenticate(realm string, st *SessionToken, userid string) error { func (s *Session) Authenticate(realm string, st *SessionToken, userid string) error {
s.mutex.Lock() s.mutex.Lock()
@ -288,35 +297,27 @@ func (s *Session) DataSessionStatus() *DataSession {
} }
func (s *Session) NewAttestation() { func (s *Session) NewAttestation() {
s.attestation = &SessionAttestation{ s.attestation = &SessionAttestation{
s: s, s: s,
} }
s.attestation.Update() s.attestation.Update()
} }
func (s *Session) Attestation() (attestation string) { func (s *Session) Attestation() (attestation string) {
s.mutex.RLock() s.mutex.RLock()
attestation = s.attestation.Token() attestation = s.attestation.Token()
s.mutex.RUnlock() s.mutex.RUnlock()
return return
} }
func (s *Session) UpdateAttestation() { func (s *Session) UpdateAttestation() {
s.mutex.Lock() s.mutex.Lock()
s.attestation.Update() s.attestation.Update()
s.mutex.Unlock() s.mutex.Unlock()
} }
type SessionUpdate struct { type SessionUpdate struct {
Id string
Types []string Types []string
Roomid string
Ua string Ua string
Prio int Prio int
Status interface{} Status interface{}
@ -336,39 +337,31 @@ type SessionAttestation struct {
} }
func (sa *SessionAttestation) Update() (string, error) { func (sa *SessionAttestation) Update() (string, error) {
token, err := sa.Encode() token, err := sa.Encode()
if err == nil { if err == nil {
sa.token = token sa.token = token
sa.refresh = time.Now().Unix() + 180 // expires after 3 minutes sa.refresh = time.Now().Unix() + 180 // expires after 3 minutes
} }
return token, err return token, err
} }
func (sa *SessionAttestation) Token() (token string) { func (sa *SessionAttestation) Token() (token string) {
if sa.refresh < time.Now().Unix() { if sa.refresh < time.Now().Unix() {
token, _ = sa.Update() token, _ = sa.Update()
} else { } else {
token = sa.token token = sa.token
} }
return return
} }
func (sa *SessionAttestation) Encode() (string, error) { func (sa *SessionAttestation) Encode() (string, error) {
return sa.s.attestations.Encode("attestation", sa.s.Id)
return sa.s.h.attestations.Encode("attestation", sa.s.Id)
} }
func (sa *SessionAttestation) Decode(token string) (string, error) { func (sa *SessionAttestation) Decode(token string) (string, error) {
var id string var id string
err := sa.s.h.attestations.Decode("attestation", token, &id) err := sa.s.attestations.Decode("attestation", token, &id)
return id, err return id, err
} }
func init() { func init() {

173
src/app/spreed-webrtc-server/session_manager.go

@ -0,0 +1,173 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"crypto/sha256"
"net/http"
"sync"
"github.com/gorilla/securecookie"
)
type UserStats interface {
UserInfo(bool) (int, map[string]*DataUser)
}
type SessionManager interface {
UserStats
RetrieveUsersWith(func(*http.Request) (string, error))
CreateSession(*http.Request) *Session
DestroySession(*Session)
Authenticate(*Session, *SessionToken, string) error
GetUserSessions(session *Session, id string) []*DataSession
}
type sessionManager struct {
Tickets
sync.RWMutex
config *Config
userTable map[string]*User
fakesessionTable map[string]*Session
useridRetriever func(*http.Request) (string, error)
attestations *securecookie.SecureCookie
}
func NewSessionManager(config *Config, tickets Tickets, sessionSecret []byte) SessionManager {
sessionManager := &sessionManager{
tickets,
sync.RWMutex{},
config,
make(map[string]*User),
make(map[string]*Session),
nil,
nil,
}
sessionManager.attestations = securecookie.New(sessionSecret, nil)
sessionManager.attestations.MaxAge(300) // 5 minutes
sessionManager.attestations.HashFunc(sha256.New)
return sessionManager
}
func (sessionManager *sessionManager) UserInfo(details bool) (userCount int, users map[string]*DataUser) {
sessionManager.RLock()
defer sessionManager.RUnlock()
userCount = len(sessionManager.userTable)
if details {
users := make(map[string]*DataUser)
for userid, user := range sessionManager.userTable {
users[userid] = user.Data()
}
}
return
}
func (sessionManager *sessionManager) RetrieveUsersWith(retriever func(*http.Request) (string, error)) {
sessionManager.useridRetriever = retriever
}
func (sessionManager *sessionManager) CreateSession(request *http.Request) *Session {
request.ParseForm()
token := request.FormValue("t")
st := sessionManager.DecodeSessionToken(token)
var userid string
if sessionManager.config.UsersEnabled {
if sessionManager.useridRetriever != nil {
userid, _ = sessionManager.useridRetriever(request)
if userid == "" {
userid = st.Userid
}
}
}
session := NewSession(sessionManager.attestations, st.Id, st.Sid)
if userid != "" {
// XXX(lcooper): Should errors be handled here?
sessionManager.Authenticate(session, st, userid)
}
return session
}
func (sessionManager *sessionManager) DestroySession(session *Session) {
session.Close()
sessionManager.Lock()
if suserid := session.Userid(); suserid != "" {
user, ok := sessionManager.userTable[suserid]
if ok && user.RemoveSession(session) {
delete(sessionManager.userTable, suserid)
}
}
sessionManager.Unlock()
}
func (sessionManager *sessionManager) Authenticate(session *Session, st *SessionToken, userid string) error {
if err := session.Authenticate(sessionManager.Realm(), st, userid); err != nil {
return err
}
// Authentication success.
suserid := session.Userid()
sessionManager.Lock()
user, ok := sessionManager.userTable[suserid]
if !ok {
user = NewUser(suserid)
sessionManager.userTable[suserid] = user
}
sessionManager.Unlock()
user.AddSession(session)
return nil
}
func (sessionManager *sessionManager) GetUserSessions(session *Session, userid string) (users []*DataSession) {
var (
user *User
ok bool
)
sessionManager.RLock()
user, ok = sessionManager.userTable[userid]
sessionManager.RUnlock()
if !ok {
// No user. Create fake session.
sessionManager.Lock()
session, ok := sessionManager.fakesessionTable[userid]
if !ok {
st := sessionManager.FakeSessionToken(userid)
session = NewSession(sessionManager.attestations, st.Id, st.Sid)
session.SetUseridFake(st.Userid)
sessionManager.fakesessionTable[userid] = session
}
sessionManager.Unlock()
users = make([]*DataSession, 1, 1)
users[0] = session.Data()
} else {
// Add sessions for foreign user.
users = user.SubscribeSessions(session)
}
return
}

13
src/app/spreed-webrtc-server/sessions.go

@ -23,6 +23,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"log" "log"
"net/http" "net/http"
@ -42,7 +43,8 @@ type SessionNonceRequest struct {
} }
type Sessions struct { type Sessions struct {
hub *Hub SessionValidator
SessionStore
users *Users users *Users
} }
@ -78,7 +80,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H
} }
// Make sure Sid matches session and is valid. // Make sure Sid matches session and is valid.
if !sessions.hub.ValidateSession(snr.Id, snr.Sid) { if !sessions.ValidateSession(snr.Id, snr.Sid) {
log.Println("Session patch failed - validation failed.") log.Println("Session patch failed - validation failed.")
error = true error = true
} }
@ -104,7 +106,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H
var nonce string var nonce string
if !error { if !error {
// FIXME(longsleep): Not running this might reveal error state with a timing attack. // FIXME(longsleep): Not running this might reveal error state with a timing attack.
nonce, err = sessions.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid}) if session, ok := sessions.GetSession(snr.Id); ok {
nonce, err = session.Authorize(sessions.Realm(), &SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
} else {
err = errors.New("no such session")
}
if err != nil { if err != nil {
log.Println("Session patch failed - handle failed.", err) log.Println("Session patch failed - handle failed.", err)
error = true error = true

8
src/app/spreed-webrtc-server/stats.go

@ -33,11 +33,11 @@ type Stat struct {
Hub *HubStat `json:"hub"` Hub *HubStat `json:"hub"`
} }
func NewStat(details bool, h *Hub) *Stat { func NewStat(details bool, statsGenerator StatsGenerator) *Stat {
stat := &Stat{ stat := &Stat{
details: details, details: details,
Runtime: &RuntimeStat{}, Runtime: &RuntimeStat{},
Hub: h.Stat(details), Hub: statsGenerator.Stat(details),
} }
stat.Runtime.Read() stat.Runtime.Read()
return stat return stat
@ -69,12 +69,12 @@ func (stat *RuntimeStat) Read() {
} }
type Stats struct { type Stats struct {
hub *Hub StatsGenerator
} }
func (stats *Stats) Get(request *http.Request) (int, interface{}, http.Header) { func (stats *Stats) Get(request *http.Request) (int, interface{}, http.Header) {
details := request.Form.Get("details") == "1" details := request.Form.Get("details") == "1"
return 200, NewStat(details, stats.hub), http.Header{"Content-Type": {"application/json; charset=utf-8"}, "Access-Control-Allow-Origin": {"*"}} return 200, NewStat(details, stats), http.Header{"Content-Type": {"application/json; charset=utf-8"}, "Access-Control-Allow-Origin": {"*"}}
} }

104
src/app/spreed-webrtc-server/stats_manager.go

@ -0,0 +1,104 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"sync/atomic"
)
type HubStat struct {
Rooms int `json:"rooms"`
Connections int `json:"connections"`
Sessions int `json:"sessions"`
Users int `json:"users"`
Count uint64 `json:"count"`
BroadcastChatMessages uint64 `json:"broadcastchatmessages"`
UnicastChatMessages uint64 `json:"unicastchatmessages"`
IdsInRoom map[string][]string `json:"idsinroom,omitempty"`
SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"`
UsersById map[string]*DataUser `json:"usersbyid,omitempty"`
ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"`
}
type ConnectionCounter interface {
CountConnection() uint64
}
type StatsCounter interface {
CountBroadcastChat()
CountUnicastChat()
}
type StatsGenerator interface {
Stat(details bool) *HubStat
}
type StatsManager interface {
ConnectionCounter
StatsCounter
StatsGenerator
}
type statsManager struct {
ClientStats
RoomStats
UserStats
connectionCount uint64
broadcastChatMessages uint64
unicastChatMessages uint64
}
func NewStatsManager(clientStats ClientStats, roomStats RoomStats, userStats UserStats) StatsManager {
return &statsManager{clientStats, roomStats, userStats, 0, 0, 0}
}
func (stats *statsManager) CountConnection() uint64 {
return atomic.AddUint64(&stats.connectionCount, 1)
}
func (stats *statsManager) CountBroadcastChat() {
atomic.AddUint64(&stats.broadcastChatMessages, 1)
}
func (stats *statsManager) CountUnicastChat() {
atomic.AddUint64(&stats.unicastChatMessages, 1)
}
func (stats *statsManager) Stat(details bool) *HubStat {
roomCount, roomSessionInfo := stats.RoomInfo(details)
clientCount, sessions, connections := stats.ClientInfo(details)
userCount, users := stats.UserInfo(details)
return &HubStat{
Rooms: roomCount,
Connections: clientCount,
Sessions: clientCount,
Users: userCount,
Count: atomic.LoadUint64(&stats.connectionCount),
BroadcastChatMessages: atomic.LoadUint64(&stats.broadcastChatMessages),
UnicastChatMessages: atomic.LoadUint64(&stats.unicastChatMessages),
IdsInRoom: roomSessionInfo,
SessionsById: sessions,
UsersById: users,
ConnectionsByIdx: connections,
}
}

130
src/app/spreed-webrtc-server/tickets.go

@ -0,0 +1,130 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"log"
"github.com/gorilla/securecookie"
)
type SessionValidator interface {
Realm() string
ValidateSession(string, string) bool
}
type SessionEncoder interface {
EncodeSessionToken(*Session) (string, error)
EncodeSessionUserID(*Session) string
}
type Tickets interface {
SessionValidator
SessionEncoder
DecodeSessionToken(token string) (st *SessionToken)
FakeSessionToken(userid string) *SessionToken
}
type tickets struct {
*securecookie.SecureCookie
realm string
tokenName string
encryptionSecret []byte
}
func NewTickets(sessionSecret, encryptionSecret []byte, realm string) Tickets {
tickets := &tickets{
nil,
realm,
fmt.Sprintf("token@%s", realm),
encryptionSecret,
}
tickets.SecureCookie = securecookie.New(sessionSecret, encryptionSecret)
tickets.MaxAge(86400 * 30) // 30 days
tickets.HashFunc(sha256.New)
tickets.BlockFunc(aes.NewCipher)
return tickets
}
func (tickets *tickets) Realm() string {
return tickets.realm
}
func (tickets *tickets) DecodeSessionToken(token string) (st *SessionToken) {
var err error
if token != "" {
st = &SessionToken{}
err = tickets.Decode(tickets.tokenName, token, st)
if err != nil {
log.Println("Error while decoding session token", err)
}
}
if st == nil || err != nil {
sid := NewRandomString(32)
id, _ := tickets.Encode("id", sid)
st = &SessionToken{Id: id, Sid: sid}
log.Println("Created new session id", id)
}
return
}
func (tickets *tickets) FakeSessionToken(userid string) *SessionToken {
st := &SessionToken{}
st.Sid = fmt.Sprintf("fake-%s", NewRandomString(27))
st.Id, _ = tickets.Encode("id", st.Sid)
st.Userid = userid
log.Println("Created new fake session id", st.Id)
return st
}
func (tickets *tickets) ValidateSession(id, sid string) bool {
var decoded string
if err := tickets.Decode("id", id, &decoded); err != nil {
log.Println("Session validation error", err, id, sid)
return false
}
if decoded != sid {
log.Println("Session validation failed", id, sid)
return false
}
return true
}
func (tickets *tickets) EncodeSessionToken(session *Session) (string, error) {
return tickets.Encode(tickets.tokenName, session.Token())
}
func (tickets *tickets) EncodeSessionUserID(session *Session) (suserid string) {
if userid := session.Userid(); userid != "" {
m := hmac.New(sha256.New, tickets.encryptionSecret)
m.Write([]byte(userid))
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil))
}
return
}

32
src/app/spreed-webrtc-server/users.go

@ -291,16 +291,21 @@ func (un *UserNonce) Response() (int, interface{}, http.Header) {
} }
type Users struct { type Users struct {
hub *Hub SessionValidator
SessionManager
SessionStore
realm string realm string
handler UsersHandler handler UsersHandler
} }
func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users { func NewUsers(sessionStore SessionStore, sessionValidator SessionValidator, sessionManager SessionManager, mode, realm string, runtime phoenix.Runtime) *Users {
var users = &Users{ var users = &Users{
hub: hub, sessionValidator,
realm: realm, sessionManager,
sessionStore,
realm,
nil,
} }
var handler UsersHandler var handler UsersHandler
@ -309,8 +314,8 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
// Create handler based on mode. // Create handler based on mode.
if handler, err = users.createHandler(mode, runtime); handler != nil && err == nil { if handler, err = users.createHandler(mode, runtime); handler != nil && err == nil {
users.handler = handler users.handler = handler
// Register handler Get at the hub. // Register handler Get.
users.hub.useridRetriever = func(request *http.Request) (userid string, err error) { sessionManager.RetrieveUsersWith(func(request *http.Request) (userid string, err error) {
userid, err = handler.Get(request) userid, err = handler.Get(request)
if err != nil { if err != nil {
log.Printf("Failed to get userid from handler: %s", err) log.Printf("Failed to get userid from handler: %s", err)
@ -320,7 +325,7 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
} }
} }
return return
} })
log.Printf("Enabled users handler '%s'\n", mode) log.Printf("Enabled users handler '%s'\n", mode)
} else if err != nil { } else if err != nil {
log.Printf("Failed to enable handler '%s': %s\n", mode, err) log.Printf("Failed to enable handler '%s': %s\n", mode, err)
@ -450,11 +455,20 @@ func (users *Users) Post(request *http.Request) (int, interface{}, http.Header)
userid := fmt.Sprintf("%s@%s", uuid.NewV4().String(), users.realm) userid := fmt.Sprintf("%s@%s", uuid.NewV4().String(), users.realm)
// Make sure Sid matches session and is valid. // Make sure Sid matches session and is valid.
if !users.hub.ValidateSession(snr.Id, snr.Sid) { if !users.ValidateSession(snr.Id, snr.Sid) {
return 403, NewApiError("users_invalid_session", "Invalid session"), http.Header{"Content-Type": {"application/json"}} return 403, NewApiError("users_invalid_session", "Invalid session"), http.Header{"Content-Type": {"application/json"}}
} }
nonce, err := users.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid}) var (
nonce string
err error
)
if session, ok := users.GetSession(snr.Id); ok {
nonce, err = session.Authorize(users.Realm(), &SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
} else {
err = errors.New("no such session")
}
if err != nil { if err != nil {
return 400, NewApiError("users_request_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}} return 400, NewApiError("users_request_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}}
} }

35
src/app/spreed-webrtc-server/ws.go

@ -22,9 +22,10 @@
package main package main
import ( import (
"github.com/gorilla/websocket"
"log" "log"
"net/http" "net/http"
"github.com/gorilla/websocket"
) )
const ( const (
@ -49,10 +50,8 @@ var (
} }
) )
func makeWsHubHandler(h *Hub) http.HandlerFunc { func makeWSHandler(connectionCounter ConnectionCounter, sessionManager SessionManager, codec Codec, channellingAPI ChannellingAPI) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// Validate incoming request. // Validate incoming request.
if r.Method != "GET" { if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
@ -68,30 +67,14 @@ func makeWsHubHandler(h *Hub) http.HandlerFunc {
return return
} }
// Read request details.
r.ParseForm()
token := r.FormValue("t")
// Create a new connection instance. // Create a new connection instance.
c := NewConnection(h, ws, r) session := sessionManager.CreateSession(r)
if token != "" { defer sessionManager.DestroySession(session)
if err := c.reregister(token); err != nil { client := NewClient(codec, channellingAPI, session)
log.Println(err) conn := NewConnection(connectionCounter.CountConnection(), ws, client)
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
if err := c.register(); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
// Start pumps (readPump blocks). // Start pumps (readPump blocks).
go c.writePump() go conn.writePump()
c.readPump() conn.readPump()
} }
} }

1
src/i18n/helpers/languages.py

@ -16,6 +16,7 @@ import json
def main(): def main():
print """// This file is auto generated, do not modify. print """// This file is auto generated, do not modify.
"use strict";
define([], function() { define([], function() {
return %s; return %s;
});""" % json.dumps(LANGUAGES) });""" % json.dumps(LANGUAGES)

127
src/i18n/messages-de.po

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n" "POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-10-23 11:28+0100\n" "PO-Revision-Date: 2014-12-08 16:48+0100\n"
"Last-Translator: Simon Eisenmann <simon@struktur.de>\n" "Last-Translator: Simon Eisenmann <simon@struktur.de>\n"
"Language-Team: struktur AG <opensource@struktur.de>\n" "Language-Team: struktur AG <opensource@struktur.de>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n"
@ -54,8 +54,8 @@ msgstr "Große Videos"
msgid "Kiosk view" msgid "Kiosk view"
msgstr "Kiosk-Ansicht" msgstr "Kiosk-Ansicht"
msgid "Classroom" msgid "Auditorium"
msgstr "Klassenzimmer" msgstr "Auditorium"
msgid "Start chat" msgid "Start chat"
msgstr "Chat starten" msgstr "Chat starten"
@ -66,8 +66,8 @@ msgstr "Video-Anruf starten"
msgid "Start audio conference" msgid "Start audio conference"
msgstr "Audio-Konferenz starten" msgstr "Audio-Konferenz starten"
msgid "No other users online" msgid "No one else here"
msgstr "Niemand sonst online" msgstr "Niemand sonst hier"
msgid "Take" msgid "Take"
msgstr "Los" msgstr "Los"
@ -155,6 +155,9 @@ msgstr ""
"Betreten Sie einen Raum und klicken dann auf das Stern-Symbol eines " "Betreten Sie einen Raum und klicken dann auf das Stern-Symbol eines "
"anderen Nutzers um eine Kontaktanfrage zu starten." "anderen Nutzers um eine Kontaktanfrage zu starten."
msgid "Edit contact"
msgstr "Kontakt bearbeiten"
msgid "Edit" msgid "Edit"
msgstr "Bearbeiten" msgstr "Bearbeiten"
@ -246,6 +249,25 @@ msgstr "Bildschirm einpassen."
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
msgid "Your name"
msgstr "Ihr Name"
msgid "Your picture"
msgstr "Ihr Bild"
msgid "Status message"
msgstr "Status Nachricht"
msgid "What's on your mind?"
msgstr "Was machen Sie gerade?"
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
"Ihr Bild, Name und Status Nachricht repräsentiert Sie in Anrufen, Chats "
"und Räumen."
msgid "Your ID" msgid "Your ID"
msgstr "Ihre ID" msgstr "Ihre ID"
@ -271,28 +293,6 @@ msgstr "Abmelden"
msgid "Manage account" msgid "Manage account"
msgstr "Konto verwalten" msgstr "Konto verwalten"
msgid "Your picture"
msgstr "Ihr Bild"
msgid "Your name"
msgstr "Ihr Name"
msgid "Status message"
msgstr "Status Nachricht"
msgid "What's on your mind?"
msgstr "Was machen Sie gerade?"
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
"Ihr Bild, Name und Status Nachricht repräsentiert Sie in Anrufen, Chats "
"und Räumen."
msgid "Manage contacts"
msgstr "Kontakte verwalten"
msgid "Media" msgid "Media"
msgstr "Kamera / Mikrofon" msgstr "Kamera / Mikrofon"
@ -396,20 +396,23 @@ msgstr ""
"Ihre ID bleibt dennoch gespeichert. Klicken Sie Ausloggen weiter oben um " "Ihre ID bleibt dennoch gespeichert. Klicken Sie Ausloggen weiter oben um "
"die ID zu löschen." "die ID zu löschen."
msgid "Share by Email" msgid "Room link"
msgstr "Per E-Mail teilen" msgstr "Raum-Link"
msgid "Share on Facebook" msgid "Invite by Email"
msgstr "Auf Facebook teilen" msgstr "Per E-Mail einladen"
msgid "Share on Twitter" msgid "Invite with Facebook"
msgstr "Auf Twitter teilen" msgstr "Mit Facebook einladen"
msgid "Share on Google Plus" msgid "Invite with Twitter"
msgstr "Auf Google Plus teilen" msgstr "Mit Twitter einladen"
msgid "Share on XING" msgid "Invite with Google Plus"
msgstr "Auf XING teilen" msgstr "Mit Google Plus einladen"
msgid "Invite with XING"
msgstr "Mit XING einladen"
msgid "Initializing" msgid "Initializing"
msgstr "Initialisiere" msgstr "Initialisiere"
@ -478,14 +481,24 @@ msgstr "Hier klicken für weitere Infos (Google Chrome)."
msgid "Please set your user details and settings." msgid "Please set your user details and settings."
msgstr "Bitte vervollständigen Sie Ihre Daten und Einstellungen." msgstr "Bitte vervollständigen Sie Ihre Daten und Einstellungen."
msgid "Create a room and talk together" msgid "Enter a room name"
msgstr "Erstellen Sie Ihren Raum" msgstr "Raum eingeben"
msgid "Random room name"
msgstr "Zufälliger Raum"
msgid "Creating room ..." msgid "Enter room"
msgstr "Raum wird erstellt ..." msgstr "Raum betreten"
msgid "Create" msgid ""
msgstr "Erstellen" "Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
"Geben Sie den Namen eines existierenden Raums ein. Melden Sie sich an um "
"eigene Räume zu erstellen."
msgid "Room history"
msgstr "Raum-Verlauf"
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt." msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt."
@ -595,11 +608,11 @@ msgstr " hat Ihren Anruf abgelehnt."
msgid " does not pick up." msgid " does not pick up."
msgstr " nimmt nicht ab." msgstr " nimmt nicht ab."
msgid " tried to call you." msgid " tried to call you"
msgstr " hat versucht Sie anzurufen." msgstr " hat versucht Sie anzurufen"
msgid " called you." msgid " called you"
msgstr " hat Sie angerufen." msgstr " hat Sie angerufen"
msgid "Your browser is not supported. Please upgrade to a current version." msgid "Your browser is not supported. Please upgrade to a current version."
msgstr "" msgstr ""
@ -682,6 +695,9 @@ msgstr "Browsereinstellung"
msgid "Meet with me here:" msgid "Meet with me here:"
msgstr "Meeting:" msgstr "Meeting:"
msgid "Room name"
msgstr "Raum-Name"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr "Unbekanntes URL-Format. Bitte geben Sie eine gültige YouTube URL ein." msgstr "Unbekanntes URL-Format. Bitte geben Sie eine gültige YouTube URL ein."
@ -723,6 +739,21 @@ msgstr ""
"Der Zugriffscode konnte nicht überprueft werden. Bitte prüfen Sie Ihre " "Der Zugriffscode konnte nicht überprueft werden. Bitte prüfen Sie Ihre "
"Internetverbindung." "Internetverbindung."
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr "PIN für Raum %s ist jetzt '%s'."
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr "Raum %s ist nicht mehr PIN-geschützt."
#, python-format
msgid "Enter the PIN for room %s"
msgstr "Geben Sie die PIN für Raum %s ein"
msgid "Please sign in to create rooms."
msgstr "Bitte melden Sie sich an um Räume zu erstellen."
#, python-format #, python-format
msgid "and %s" msgid "and %s"
msgstr "und %s" msgstr "und %s"

111
src/i18n/messages-ja.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n" "POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-04-23 22:25+0100\n" "PO-Revision-Date: 2014-04-23 22:25+0100\n"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n" "Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view" msgid "Kiosk view"
msgstr "" msgstr ""
msgid "Classroom" msgid "Auditorium"
msgstr "" msgstr ""
msgid "Start chat" msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "テレビ電話を始める"
msgid "Start audio conference" msgid "Start audio conference"
msgstr "音声会議を始める" msgstr "音声会議を始める"
msgid "No other users online" msgid "No one else here"
msgstr "オンラインのユーザーはいません" msgstr ""
msgid "Take" msgid "Take"
msgstr "" msgstr ""
@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry." "clicking on the star icon next to a user entry."
msgstr "" msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -246,6 +249,23 @@ msgstr "画面に合わせる"
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
msgid "Your name"
msgstr "あなたの名前"
msgid "Your picture"
msgstr "あなたの写真"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID" msgid "Your ID"
msgstr "" msgstr ""
@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account" msgid "Manage account"
msgstr "" msgstr ""
msgid "Your picture"
msgstr "あなたの写真"
msgid "Your name"
msgstr "あなたの名前"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media" msgid "Media"
msgstr "" msgstr ""
@ -390,20 +390,24 @@ msgid ""
" ID." " ID."
msgstr "" msgstr ""
msgid "Share by Email" msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "Eメールでシェア" msgstr "Eメールでシェア"
msgid "Share on Facebook" msgid "Invite with Facebook"
msgstr "フェイスブックでシェア" msgstr ""
msgid "Share on Twitter" msgid "Invite with Twitter"
msgstr "ツィッターでシェア" msgstr ""
msgid "Share on Google Plus" msgid "Invite with Google Plus"
msgstr "Google+でシェア" msgstr ""
msgid "Share on XING" msgid "Invite with XING"
msgstr "XINGでシェア" msgstr ""
msgid "Initializing" msgid "Initializing"
msgstr "初期化中" msgstr "初期化中"
@ -470,14 +474,22 @@ msgstr "ここをクリックしてヘルプ表示(Google Chrome)"
msgid "Please set your user details and settings." msgid "Please set your user details and settings."
msgstr "あなたのプロフィールとアプリの動作を設定してください." msgstr "あなたのプロフィールとアプリの動作を設定してください."
msgid "Create a room and talk together" msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Creating room ..." msgid "Enter room"
msgstr "ルームへのリンクを作る..." msgstr "現在のルーム"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create" msgid "Room history"
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr "着信拒否されました."
msgid " does not pick up." msgid " does not pick up."
msgstr "は電話にでません." msgstr "は電話にでません."
msgid " tried to call you." #, fuzzy
msgid " tried to call you"
msgstr "は電話しようとしました." msgstr "は電話しようとしました."
msgid " called you." #, fuzzy
msgid " called you"
msgstr "から電話がありました." msgstr "から電話がありました."
#, fuzzy #, fuzzy
@ -664,6 +678,10 @@ msgstr "ブラウザの言語を使用"
msgid "Meet with me here:" msgid "Meet with me here:"
msgstr "ここで私と会う:" msgstr "ここで私と会う:"
#, fuzzy
msgid "Room name"
msgstr "あなたの名前"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr "" msgstr ""
@ -701,6 +719,21 @@ msgid ""
"again." "again."
msgstr "アクセスコードの確認に失敗しました.インターネット接続を確認してリトライしてください." msgstr "アクセスコードの確認に失敗しました.インターネット接続を確認してリトライしてください."
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, fuzzy, python-format #, fuzzy, python-format
msgid "and %s" msgid "and %s"
msgstr "と %2" msgstr "と %2"

111
src/i18n/messages-ko.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n" "POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-04-13 20:30+0900\n" "PO-Revision-Date: 2014-04-13 20:30+0900\n"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n" "Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view" msgid "Kiosk view"
msgstr "" msgstr ""
msgid "Classroom" msgid "Auditorium"
msgstr "" msgstr ""
msgid "Start chat" msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "화상회의 시작"
msgid "Start audio conference" msgid "Start audio conference"
msgstr "음성회의 시작" msgstr "음성회의 시작"
msgid "No other users online" msgid "No one else here"
msgstr "온라인에 다른 대화상대 없음" msgstr ""
msgid "Take" msgid "Take"
msgstr "" msgstr ""
@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry." "clicking on the star icon next to a user entry."
msgstr "" msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -246,6 +249,23 @@ msgstr "화면에 맟춤"
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
msgid "Your name"
msgstr "사용자 이름"
msgid "Your picture"
msgstr "사용자 사진"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID" msgid "Your ID"
msgstr "" msgstr ""
@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account" msgid "Manage account"
msgstr "" msgstr ""
msgid "Your picture"
msgstr "사용자 사진"
msgid "Your name"
msgstr "사용자 이름"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media" msgid "Media"
msgstr "" msgstr ""
@ -390,20 +390,24 @@ msgid ""
" ID." " ID."
msgstr "" msgstr ""
msgid "Share by Email" msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "이메일로 공유" msgstr "이메일로 공유"
msgid "Share on Facebook" msgid "Invite with Facebook"
msgstr "Facebook에서 공유" msgstr ""
msgid "Share on Twitter" msgid "Invite with Twitter"
msgstr "Twitter에서 공유" msgstr ""
msgid "Share on Google Plus" msgid "Invite with Google Plus"
msgstr "구글 플러스에서 공유" msgstr ""
msgid "Share on XING" msgid "Invite with XING"
msgstr "Xing에서 공유" msgstr ""
msgid "Initializing" msgid "Initializing"
msgstr "초기화" msgstr "초기화"
@ -470,14 +474,22 @@ msgstr "도움말을 원하면 여기를 클릭 하세요 (구글 크롬)"
msgid "Please set your user details and settings." msgid "Please set your user details and settings."
msgstr "사용자의 세부상세와 설정을 지정하세요 " msgstr "사용자의 세부상세와 설정을 지정하세요 "
msgid "Create a room and talk together" msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Creating room ..." msgid "Enter room"
msgstr "방 링크 만들기..." msgstr "현재 방"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create" msgid "Room history"
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr "전화가 거부 되었습니다."
msgid " does not pick up." msgid " does not pick up."
msgstr "전화를 받지 않습니다." msgstr "전화를 받지 않습니다."
msgid " tried to call you." #, fuzzy
msgid " tried to call you"
msgstr "연결을 시도 중입니다" msgstr "연결을 시도 중입니다"
msgid " called you." #, fuzzy
msgid " called you"
msgstr "전화 드렸습니다." msgstr "전화 드렸습니다."
#, fuzzy #, fuzzy
@ -664,6 +678,10 @@ msgstr "브라우저 언어 사용"
msgid "Meet with me here:" msgid "Meet with me here:"
msgstr "나를 여기서 만납니다:" msgstr "나를 여기서 만납니다:"
#, fuzzy
msgid "Room name"
msgstr "사용자 이름"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr "" msgstr ""
@ -701,6 +719,21 @@ msgid ""
"again." "again."
msgstr "접속코드 확인이 실패 했습니다. 인터넷 연결을 확인하고 다시 시도해 주십시오. " msgstr "접속코드 확인이 실패 했습니다. 인터넷 연결을 확인하고 다시 시도해 주십시오. "
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, fuzzy, python-format #, fuzzy, python-format
msgid "and %s" msgid "and %s"
msgstr "그리고 %2$s" msgstr "그리고 %2$s"

111
src/i18n/messages-zh-cn.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n" "POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-05-21 09:54+0800\n" "PO-Revision-Date: 2014-05-21 09:54+0800\n"
"Last-Translator: Michael P.\n" "Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view" msgid "Kiosk view"
msgstr "" msgstr ""
msgid "Classroom" msgid "Auditorium"
msgstr "" msgstr ""
msgid "Start chat" msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "开始视频通话"
msgid "Start audio conference" msgid "Start audio conference"
msgstr "开始语音会议" msgstr "开始语音会议"
msgid "No other users online" msgid "No one else here"
msgstr "无其他联系人在线" msgstr ""
msgid "Take" msgid "Take"
msgstr "" msgstr ""
@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry." "clicking on the star icon next to a user entry."
msgstr "" msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -246,6 +249,23 @@ msgstr "适合屏幕"
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
msgid "Your name"
msgstr "您的名字"
msgid "Your picture"
msgstr "您的图片"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID" msgid "Your ID"
msgstr "" msgstr ""
@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account" msgid "Manage account"
msgstr "" msgstr ""
msgid "Your picture"
msgstr "您的图片"
msgid "Your name"
msgstr "您的名字"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media" msgid "Media"
msgstr "" msgstr ""
@ -390,20 +390,24 @@ msgid ""
" ID." " ID."
msgstr "" msgstr ""
msgid "Share by Email" msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "电子邮件共享" msgstr "电子邮件共享"
msgid "Share on Facebook" msgid "Invite with Facebook"
msgstr "Facebook共享" msgstr ""
msgid "Share on Twitter" msgid "Invite with Twitter"
msgstr "Twitter共享" msgstr ""
msgid "Share on Google Plus" msgid "Invite with Google Plus"
msgstr "Google Plus共享" msgstr ""
msgid "Share on XING" msgid "Invite with XING"
msgstr "XING共享" msgstr ""
msgid "Initializing" msgid "Initializing"
msgstr "初始化" msgstr "初始化"
@ -470,14 +474,22 @@ msgstr "点击这里获取帮助 (Google Chrome)"
msgid "Please set your user details and settings." msgid "Please set your user details and settings."
msgstr "请设定您的用户信息及设置" msgstr "请设定您的用户信息及设置"
msgid "Create a room and talk together" msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Creating room ..." msgid "Enter room"
msgstr "创建房间链接" msgstr "當前房间"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create" msgid "Room history"
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr " 拒绝了您的呼叫。"
msgid " does not pick up." msgid " does not pick up."
msgstr " 无人接听。" msgstr " 无人接听。"
msgid " tried to call you." #, fuzzy
msgid " tried to call you"
msgstr " 曾呼叫您。" msgstr " 曾呼叫您。"
msgid " called you." #, fuzzy
msgid " called you"
msgstr " 曾与您通话。" msgstr " 曾与您通话。"
#, fuzzy #, fuzzy
@ -663,6 +677,10 @@ msgstr "使用浏览器语言"
msgid "Meet with me here:" msgid "Meet with me here:"
msgstr "我们这里见:" msgstr "我们这里见:"
#, fuzzy
msgid "Room name"
msgstr "您的名字"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr "" msgstr ""
@ -700,6 +718,21 @@ msgid ""
"again." "again."
msgstr "接入码认证失败。请检查您的网络连接并重试。" msgstr "接入码认证失败。请检查您的网络连接并重试。"
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, python-format #, python-format
msgid "and %s" msgid "and %s"
msgstr "" msgstr ""

111
src/i18n/messages-zh-tw.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n" "POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-05-21 09:55+0800\n" "PO-Revision-Date: 2014-05-21 09:55+0800\n"
"Last-Translator: Michael P.\n" "Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view" msgid "Kiosk view"
msgstr "" msgstr ""
msgid "Classroom" msgid "Auditorium"
msgstr "" msgstr ""
msgid "Start chat" msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "開始視頻通話"
msgid "Start audio conference" msgid "Start audio conference"
msgstr "開始語音會議" msgstr "開始語音會議"
msgid "No other users online" msgid "No one else here"
msgstr "無其他聯繫人在線" msgstr ""
msgid "Take" msgid "Take"
msgstr "" msgstr ""
@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry." "clicking on the star icon next to a user entry."
msgstr "" msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -246,6 +249,23 @@ msgstr "適合屏幕"
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
msgid "Your name"
msgstr "您的名字"
msgid "Your picture"
msgstr "您的圖片"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID" msgid "Your ID"
msgstr "" msgstr ""
@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account" msgid "Manage account"
msgstr "" msgstr ""
msgid "Your picture"
msgstr "您的圖片"
msgid "Your name"
msgstr "您的名字"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media" msgid "Media"
msgstr "" msgstr ""
@ -390,20 +390,24 @@ msgid ""
" ID." " ID."
msgstr "" msgstr ""
msgid "Share by Email" msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "電子郵件共享" msgstr "電子郵件共享"
msgid "Share on Facebook" msgid "Invite with Facebook"
msgstr "Facebook共享" msgstr ""
msgid "Share on Twitter" msgid "Invite with Twitter"
msgstr "Twitter共享" msgstr ""
msgid "Share on Google Plus" msgid "Invite with Google Plus"
msgstr "Google Plus共享" msgstr ""
msgid "Share on XING" msgid "Invite with XING"
msgstr "XING共享" msgstr ""
msgid "Initializing" msgid "Initializing"
msgstr "初始化" msgstr "初始化"
@ -470,14 +474,22 @@ msgstr "點擊這裡獲取幫助 (Google Chrome)"
msgid "Please set your user details and settings." msgid "Please set your user details and settings."
msgstr "請設定您的用戶信息及設置" msgstr "請設定您的用戶信息及設置"
msgid "Create a room and talk together" msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
msgid "Creating room ..." msgid "Enter room"
msgstr "創建房間連接 ..." msgstr "當前房間"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create" msgid "Room history"
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr " 拒絕了您的呼叫"
msgid " does not pick up." msgid " does not pick up."
msgstr " 無人接聽。" msgstr " 無人接聽。"
msgid " tried to call you." #, fuzzy
msgid " tried to call you"
msgstr " 曾呼叫您" msgstr " 曾呼叫您"
msgid " called you." #, fuzzy
msgid " called you"
msgstr " 曾與您通話" msgstr " 曾與您通話"
#, fuzzy #, fuzzy
@ -663,6 +677,10 @@ msgstr "使用瀏覽器語言"
msgid "Meet with me here:" msgid "Meet with me here:"
msgstr "我們這裡見:" msgstr "我們這裡見:"
#, fuzzy
msgid "Room name"
msgstr "您的名字"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr "" msgstr ""
@ -700,6 +718,21 @@ msgid ""
"again." "again."
msgstr "接入碼認證錯誤。請檢查您的網絡連接并重試。" msgstr "接入碼認證錯誤。請檢查您的網絡連接并重試。"
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, python-format #, python-format
msgid "and %s" msgid "and %s"
msgstr "" msgstr ""

93
src/i18n/messages.pot

@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n" "POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view" msgid "Kiosk view"
msgstr "" msgstr ""
msgid "Classroom" msgid "Auditorium"
msgstr "" msgstr ""
msgid "Start chat" msgid "Start chat"
@ -66,7 +66,7 @@ msgstr ""
msgid "Start audio conference" msgid "Start audio conference"
msgstr "" msgstr ""
msgid "No other users online" msgid "No one else here"
msgstr "" msgstr ""
msgid "Take" msgid "Take"
@ -153,6 +153,9 @@ msgid ""
"clicking on the star icon next to a user entry." "clicking on the star icon next to a user entry."
msgstr "" msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -242,47 +245,44 @@ msgstr ""
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
msgid "Your ID" msgid "Your name"
msgstr ""
msgid "Register"
msgstr "" msgstr ""
msgid "" msgid "Your picture"
"Authenticated by certificate. To log out you have to remove your "
"certificate from the browser."
msgstr "" msgstr ""
msgid "Sign in" msgid "Status message"
msgstr "" msgstr ""
msgid "Create an account" msgid "What's on your mind?"
msgstr "" msgstr ""
msgid "Sign out" msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr "" msgstr ""
msgid "Manage account" msgid "Your ID"
msgstr "" msgstr ""
msgid "Your picture" msgid "Register"
msgstr "" msgstr ""
msgid "Your name" msgid ""
"Authenticated by certificate. To log out you have to remove your "
"certificate from the browser."
msgstr "" msgstr ""
msgid "Status message" msgid "Sign in"
msgstr "" msgstr ""
msgid "What's on your mind?" msgid "Create an account"
msgstr "" msgstr ""
msgid "" msgid "Sign out"
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr "" msgstr ""
msgid "Manage contacts" msgid "Manage account"
msgstr "" msgstr ""
msgid "Media" msgid "Media"
@ -386,19 +386,22 @@ msgid ""
" ID." " ID."
msgstr "" msgstr ""
msgid "Share by Email" msgid "Room link"
msgstr ""
msgid "Invite by Email"
msgstr "" msgstr ""
msgid "Share on Facebook" msgid "Invite with Facebook"
msgstr "" msgstr ""
msgid "Share on Twitter" msgid "Invite with Twitter"
msgstr "" msgstr ""
msgid "Share on Google Plus" msgid "Invite with Google Plus"
msgstr "" msgstr ""
msgid "Share on XING" msgid "Invite with XING"
msgstr "" msgstr ""
msgid "Initializing" msgid "Initializing"
@ -466,13 +469,21 @@ msgstr ""
msgid "Please set your user details and settings." msgid "Please set your user details and settings."
msgstr "" msgstr ""
msgid "Create a room and talk together" msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr ""
msgid "Enter room"
msgstr "" msgstr ""
msgid "Creating room ..." msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr "" msgstr ""
msgid "Create" msgid "Room history"
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
@ -579,10 +590,10 @@ msgstr ""
msgid " does not pick up." msgid " does not pick up."
msgstr "" msgstr ""
msgid " tried to call you." msgid " tried to call you"
msgstr "" msgstr ""
msgid " called you." msgid " called you"
msgstr "" msgstr ""
msgid "Your browser is not supported. Please upgrade to a current version." msgid "Your browser is not supported. Please upgrade to a current version."
@ -654,6 +665,9 @@ msgstr ""
msgid "Meet with me here:" msgid "Meet with me here:"
msgstr "" msgstr ""
msgid "Room name"
msgstr ""
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr "" msgstr ""
@ -691,6 +705,21 @@ msgid ""
"again." "again."
msgstr "" msgstr ""
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, python-format #, python-format
msgid "and %s" msgid "and %s"
msgstr "" msgstr ""

2
src/styles/Makefile.am

@ -36,6 +36,8 @@ styles:
$(CURDIR)/bootstrap.scss:$(STATIC)/css/bootstrap.min.css $(CURDIR)/bootstrap.scss:$(STATIC)/css/bootstrap.min.css
$(SASS) --compass --scss $(SASSFLAGS) \ $(SASS) --compass --scss $(SASSFLAGS) \
$(CURDIR)/font-awesome.scss:$(STATIC)/css/font-awesome.min.css $(CURDIR)/font-awesome.scss:$(STATIC)/css/font-awesome.min.css
$(SASS) --compass --scss $(SASSFLAGS) \
$(CURDIR)/csp.scss:$(STATIC)/css/csp.min.css
styleshint: styleshint:
@if [ "$(SASS)" = "" ]; then echo "Command 'sass' not found, required when checking styles"; exit 1; fi @if [ "$(SASS)" = "" ]; then echo "Command 'sass' not found, required when checking styles"; exit 1; fi

49
src/styles/_shame.scss

@ -22,4 +22,53 @@
// Remove boostrap 3 modal scroll bar. // Remove boostrap 3 modal scroll bar.
.modal { .modal {
overflow-y: auto; overflow-y: auto;
}
// Toastr fontawesome support.
#toast-container > .toast {
background-image: none !important;
}
#toast-container > .toast:before {
position: fixed;
font-family: FontAwesome;
font-size: 20px;
line-height: 20px;
float: left;
color: #fff;
padding-right: 0.5em;
margin: auto 0.5em auto -1.5em;
}
#toast-container > .toast-warning:before {
content: "\f05a";
}
#toast-container > .toast-error:before {
content: "\f05a";
}
#toast-container > .toast-info:before {
content: "\f05a";
}
#toast-container > .toast-success:before {
content: "\f05a";
}
// No shadows for toastr.
#toast-container > :hover, #toast-container > div {
box-shadow: none !important;
}
// Update colors for toaster.
.toast-info {
background-color: #5bc0de;
}
// Update position of toastr close icon.
.toast-close-button {
top: -0.6em;
font-size: 1em;
}
// No opacity for toastr.
#toast-container > div {
opacity: 1;
filter: alpha(opacity=100);
} }

35
src/styles/components/_audiovideo.scss

@ -169,14 +169,14 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
vertical-align: bottom; vertical-align: bottom;
visibility: hidden; //visibility: hidden;
width: 100%; width: 100%;
&.withvideo { &.withvideo {
visibility: visible; //visibility: visible;
} }
&.onlyaudio { &.onlyaudio {
background: $video-onlyaudio-background; background: $video-onlyaudio-background;
visibility: visible; //visibility: visible;
} }
.onlyaudio { .onlyaudio {
color: $video-onlyaudio; color: $video-onlyaudio;
@ -190,12 +190,15 @@
text-align: center; text-align: center;
top: 45%; top: 45%;
} }
&.onlyaudio video { &.onlyaudio video, &.dummy video {
visibility: hidden; visibility: hidden;
} }
&.onlyaudio .onlyaudio { &.onlyaudio .onlyaudio {
display: block; display: block;
} }
&.dummy .onlyaudio {
display: block;
}
.peerActions { .peerActions {
bottom: 5%; bottom: 5%;
left: 40px; left: 40px;
@ -251,6 +254,25 @@
text-shadow: 0 0 5px black; text-shadow: 0 0 5px black;
width: 40px; width: 40px;
} }
button.renderer-auditorium {
position: relative;
span:before {
position:absolute;
left:50%;
top:50%;
margin-left: -0.8em;
margin-top: -0.5em;
content: "\f183";
}
span:after {
position:absolute;
top:50%;
right:50%;
margin-right: -0.9em;
margin-top: -0.5em;
content: "\f183";
}
}
} }
.remoteVideo { .remoteVideo {
@ -384,7 +406,7 @@
} }
} }
.renderer-classroom { .renderer-auditorium {
.remoteContainer { .remoteContainer {
border-left: 40px solid black; border-left: 40px solid black;
} }
@ -442,4 +464,5 @@
bottom: 8%; bottom: 8%;
} }
} }
} }

2
src/styles/components/_buddylist.scss

@ -240,7 +240,7 @@
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
line-height: 40px; line-height: 40px;
font-size: 1.6em; font-size: $buddylist-action-font-size;
} }
} }

5
src/styles/components/_contactsmanager.scss

@ -47,9 +47,12 @@
overflow-y: auto; overflow-y: auto;
} }
.picture { .picture {
display: table-cell;
border-bottom: 0; border-bottom: 0;
cursor: auto; cursor: auto;
min-height: 46px; min-height: 46px;
position: static;
width: auto;
.buddyPicture { .buddyPicture {
margin: 0 0 0 10px; margin: 0 0 0 10px;
} }
@ -60,7 +63,7 @@
border-top: none; border-top: none;
} }
.name { .name {
width: 70%; width: 40%;
text-align: left; text-align: left;
vertical-align: middle; vertical-align: middle;
} }

3
src/styles/components/_social.scss

@ -20,6 +20,9 @@
*/ */
.#{$fa-css-prefix} { .#{$fa-css-prefix} {
&.link {
color: $social-email-color;
}
&.email { &.email {
color: $social-email-color; color: $social-email-color;
} }

5
src/styles/global/skins/_light.scss → src/styles/csp.scss

@ -1,4 +1,4 @@
/* /*!
* Spreed WebRTC. * Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG * Copyright (C) 2013-2014 struktur AG
* *
@ -19,5 +19,4 @@
* *
*/ */
$skin-name: "Light Theme"; @import "libs/angular/angular-csp.scss";
$skin-version: "0.0.1";

4
src/styles/global/_overlaybar.scss

@ -28,6 +28,8 @@
text-shadow: 0 0 5px black; text-shadow: 0 0 5px black;
user-select: none; user-select: none;
vertical-align: middle; vertical-align: middle;
border-bottom: 1px solid #222;
border-top: 1px solid #222;
} }
.overlaybar { .overlaybar {
@ -57,6 +59,8 @@
&.notvisible { &.notvisible {
background: transparent; background: transparent;
pointer-events: none; pointer-events: none;
border-bottom: 1px solid transparent;
border-top: 1px solid transparent;
&:hover { &:hover {
background: transparent; background: transparent;
} }

40
src/styles/global/_pages.scss

@ -35,16 +35,17 @@
text-shadow: 0 0 5px black; text-shadow: 0 0 5px black;
max-width:600px; max-width:600px;
min-height: 160px; min-height: 160px;
padding-left:160px; padding-left:105px;
padding-right: 0px; padding-right: 0px;
position: relative; position: relative;
@include breakpt($breakpoint-medium) { @include breakpt($breakpoint-medium) {
padding-left:0px; padding-left:10px;
margin: 0 10px; padding-right:20px;
margin: 0 auto;
} }
h1 { h1 {
text-align: center; margin-top: 0px;
white-space: nowrap; white-space: nowrap;
@include breakpt($breakpoint-medium) { @include breakpt($breakpoint-medium) {
white-space: normal; white-space: normal;
@ -52,24 +53,23 @@
} }
.welcome-container { .welcome-container {
max-width: 450px;
margin: 0 auto; margin: 0 auto;
} }
.welcome-logo { .welcome-logo {
position: absolute; position: absolute;
left: 0px; left: 0px;
top: 0px; top: 1px;
bottom: 0px; bottom: 0px;
width: 140px; width: 90px;
background: $scalable-logo no-repeat left; background: $scalable-logo no-repeat left top;
background-size: contain; background-size: contain;
@include breakpt($breakpoint-medium) { @include breakpt($breakpoint-medium) {
position: relative; position: relative;
margin: 0 auto;
height: 70px; height: 70px;
width: 70px; width: 70px;
margin-top: 30px; margin-top: 30px;
margin-bottom: 20px;
} }
} }
@ -88,6 +88,28 @@
a { a {
padding-right: .5em; padding-right: .5em;
color: black; color: black;
user-select: none;
}
}
.room-link {
margin-top: -10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
a {
color: $welcome;
}
}
.rooms-history {
margin-top: 3em;
a {
&:hover {
text-decoration: none;
}
margin-right: .5em;
display: inline-block;
} }
} }

3
src/styles/global/_variables.scss

@ -20,8 +20,6 @@
*/ */
@import "compass/utilities/color/contrast"; @import "compass/utilities/color/contrast";
@import "branding";
@import "skins/light";
// general // general
$background: #e5e5e5 !default; $background: #e5e5e5 !default;
@ -105,6 +103,7 @@ $buddylist-tab-background: $componentbg !default;
$buddylist-action-background: rgba(255,255,255,0.5) !default; $buddylist-action-background: rgba(255,255,255,0.5) !default;
$buddylist-buddy1: $componentfg1 !default; $buddylist-buddy1: $componentfg1 !default;
$buddylist-buddy2: $componentfg2 !default; $buddylist-buddy2: $componentfg2 !default;
$buddylist-action-font-size: 1.6em;
// chat // chat
$chat-width: $pane-width !default; $chat-width: $pane-width !default;

91
src/styles/global/skins/_dark.scss

@ -1,91 +0,0 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
$skin-name: "Dark Theme";
$skin-version: "0.0.1";
$base-skin-color: #212121;
$contrasted-dark-default: #262625; // compass color utility
$contrasted-light-default: #FCFCFC; // compass color utility
// background
$main-background: url("../img/bg-tiles.jpg");
$main-background-retina: url("../img/bg-tiles_x2.jpg");
// menu background color
$menu-background-color: $base-skin-color;
$menu-background-color-accent: lighten($menu-background-color, 5%);
$componentfg1: contrast-color($menu-background-color);
$componentbg: $menu-background-color;
$buddylist-tab-background: $menu-background-color;
$chat-background: darken($menu-background-color, 5%);
$chat-msg-self-background: lighten($chat-background, 10%);
$chat-msg-remote-background: $chat-msg-self-background;
$fileinfo-background: darken($chat-msg-self-background, 10%);
$fileinfo-background-remote: darken($chat-msg-remote-background, 10%);
$buddylist-action-background: transparent;
$sidepanebg: $menu-background-color-accent;
$settings-background: $menu-background-color;
$fileinfo-icon-background-color: $chat-msg-self-background;
$fileinfo-icon-background-color-remote: $chat-msg-remote-background;
// font
$font-color: contrast-color($menu-background-color);
$font-color-accent: darken($font-color, 20%);
$chat-ctrl: $font-color;
$bar-btn-action-color: $font-color;
$chat-meta: $font-color-accent;
$chat-timestamp: $font-color-accent;
$form-help-text: $font-color-accent;
$bar-logo-text-desc: $font-color-accent;
$buddylist-browser: $font-color-accent;
$buddylist-tab-color: $font-color-accent;
$bar-btn-action-hover: darken($font-color, 20%);
$bar-btn-action-border: darken($font-color, 50%);
$bordercolor: lighten($menu-background-color, 10%);
$chat-msg-border: lighten($chat-background, 5%);
$fileinfo-border-remote: $chat-msg-remote-background;
$fileinfo-border: $chat-msg-self-background;
$overlaybar-btn: $font-color-accent;
$overlaybar-color: $font-color-accent;
// bootstrap vars
$text-color: $font-color;
$legend-color: $font-color;
$legend-border-color: $bordercolor;
$hr-border: $bordercolor;
$list-group-bg: $menu-background-color-accent;
$list-group-border: $bordercolor;
$list-group-hover-bg: $menu-background-color;
$list-group-link-color: $font-color-accent;
$input-bg: lighten($menu-background-color, 20%);
$input-color: $font-color;
$input-border: $bar-btn-action-border;
$progress-bg: lighten($input-bg, 30%);
$modal-content-bg: $menu-background-color-accent;
$dialog-header-neutral: $menu-background-color-accent !default;
$close-color: white;
$modal-header-border-color: $bordercolor;

6
src/styles/libs/_dialogs.scss

@ -26,7 +26,7 @@
.dialog-header-wait span, .dialog-header-wait h4 { color: #fff; } .dialog-header-wait span, .dialog-header-wait h4 { color: #fff; }
.modal-content { .modal-content {
overflow:hidden; overflow:hidden;
.modal-body {
min-height: 160px;
}
} }
.modal-backdrop.in {
opacity:0.2;
}

1
src/styles/libs/_libs.scss

@ -2,3 +2,4 @@
@import "hidpi"; @import "hidpi";
@import "toastr/toastr.scss"; @import "toastr/toastr.scss";
@import "dialogs"; @import "dialogs";
@import "angular/angular.scss";

41
src/styles/libs/angular/angular-csp.scss

@ -0,0 +1,41 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
.ng-cloak, .x-ng-cloak,
.ng-hide {
display: none !important;
}
ng\:form {
display: block;
}
.ng-animate-block-transitions {
transition:0s all!important;
-webkit-transition:0s all!important;
}
/* show the element during a show/hide animation when the
* animation is ongoing, but the .ng-hide class is active */
.ng-hide-add-active, .ng-hide-remove {
display: block!important;
}

0
src/styles/global/_angular.scss → src/styles/libs/angular/angular.scss

44
src/styles/libs/toastr/toastr.scss

@ -1,12 +1,13 @@
/* /*
* Toastr * Toastr
* Version 2.0.1 * Copyright 2012-2014
* Copyright 2012 John Papa and Hans Fjällemark. * Authors: John Papa, Hans Fjällemark, and Tim Ferrell.
* All Rights Reserved. * All Rights Reserved.
* Use, reproduction, distribution, and modification of this code is subject to the terms and * Use, reproduction, distribution, and modification of this code is subject to the terms and
* conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
* *
* Author: John Papa and Hans Fjällemark * ARIA Support: Greta Krafsig
*
* Project: https://github.com/CodeSeven/toastr * Project: https://github.com/CodeSeven/toastr
*/ */
.toast-title { .toast-title {
@ -24,7 +25,6 @@
color: #cccccc; color: #cccccc;
text-decoration: none; text-decoration: none;
} }
.toast-close-button { .toast-close-button {
position: relative; position: relative;
right: -0.3em; right: -0.3em;
@ -48,7 +48,6 @@
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40); filter: alpha(opacity=40);
} }
/*Additional properties for button version /*Additional properties for button version
iOS requires the button element instead of an anchor tag. iOS requires the button element instead of an anchor tag.
If you want the anchor version, it requires `href="#"`.*/ If you want the anchor version, it requires `href="#"`.*/
@ -59,6 +58,16 @@ button.toast-close-button {
border: 0; border: 0;
-webkit-appearance: none; -webkit-appearance: none;
} }
.toast-top-center {
top: 0;
right: 0;
width: 100%;
}
.toast-bottom-center {
bottom: 0;
right: 0;
width: 100%;
}
.toast-top-full-width { .toast-top-full-width {
top: 0; top: 0;
right: 0; right: 0;
@ -97,6 +106,8 @@ button.toast-close-button {
box-sizing: border-box; box-sizing: border-box;
} }
#toast-container > div { #toast-container > div {
position: relative;
overflow: hidden;
margin: 0 0 6px; margin: 0 0 6px;
padding: 15px 15px 15px 50px; padding: 15px 15px 15px 50px;
width: 300px; width: 300px;
@ -134,6 +145,11 @@ button.toast-close-button {
#toast-container > .toast-warning { #toast-container > .toast-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
} }
#toast-container.toast-top-center > div,
#toast-container.toast-bottom-center > div {
width: 300px;
margin: auto;
}
#toast-container.toast-top-full-width > div, #toast-container.toast-top-full-width > div,
#toast-container.toast-bottom-full-width > div { #toast-container.toast-bottom-full-width > div {
width: 96%; width: 96%;
@ -154,8 +170,20 @@ button.toast-close-button {
.toast-warning { .toast-warning {
background-color: #f89406; background-color: #f89406;
} }
.toast-progress {
position: absolute;
left: 0;
bottom: 0;
height: 4px;
background-color: #000000;
opacity: 0.4;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40);
}
/*Responsive Design*/ /*Responsive Design*/
@media all and (max-width: 239px) { @media all and (max-width: 240px) {
#toast-container > div { #toast-container > div {
padding: 8px 8px 8px 50px; padding: 8px 8px 8px 50px;
width: 11em; width: 11em;
@ -165,7 +193,7 @@ button.toast-close-button {
top: -0.2em; top: -0.2em;
} }
} }
@media all and (min-width: 240px) and (max-width: 479px) { @media all and (min-width: 241px) and (max-width: 480px) {
#toast-container > div { #toast-container > div {
padding: 8px 8px 8px 50px; padding: 8px 8px 8px 50px;
width: 18em; width: 18em;
@ -175,7 +203,7 @@ button.toast-close-button {
top: -0.2em; top: -0.2em;
} }
} }
@media all and (min-width: 480px) and (max-width: 767px) { @media all and (min-width: 481px) and (max-width: 768px) {
#toast-container > div { #toast-container > div {
padding: 15px 15px 15px 50px; padding: 15px 15px 15px 50px;
width: 25em; width: 25em;

1
src/styles/main.scss

@ -30,7 +30,6 @@
@import "global/loader"; @import "global/loader";
@import "global/views"; @import "global/views";
@import "global/pages"; @import "global/pages";
@import "global/angular";
@import "global/nicescroll"; @import "global/nicescroll";
@import "global/animations"; @import "global/animations";
@import "global/overlaybar"; @import "global/overlaybar";

20
static/css/csp.min.css vendored

@ -0,0 +1,20 @@
/*!
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important}ng\:form{display:block}.ng-animate-block-transitions{transition:0s all !important;-webkit-transition:0s all !important}.ng-hide-add-active,.ng-hide-remove{display:block !important}

2
static/css/main.min.css vendored

File diff suppressed because one or more lines are too long

59
static/js/app.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([ define([
'require', 'require',
'jquery', 'jquery',
@ -38,6 +40,7 @@ define([
'angular-animate', 'angular-animate',
'angular-humanize', 'angular-humanize',
'angular-route', 'angular-route',
'mobile-events' 'mobile-events'
], function(require, $, _, angular, modernizr, moment, services, directives, filters, controllers, languages) { ], function(require, $, _, angular, modernizr, moment, services, directives, filters, controllers, languages) {
@ -65,12 +68,13 @@ define([
var appConfig = {}; var appConfig = {};
// Implement translation store. // Implement translation store.
var TranslationData = function(default_language) { var TranslationData = function(default_language, launcher) {
// Create data structure. // Create data structure.
this.data = { this.data = {
locale_data: {} locale_data: {}
}; };
this.lang = this.default_lang = default_language; this.lang = this.default_lang = default_language;
this.getHTTP = launcher.$http.get;
}; };
TranslationData.prototype.language = function() { TranslationData.prototype.language = function() {
// Return language. // Return language.
@ -97,25 +101,24 @@ define([
}; };
TranslationData.prototype.load = function(domain, url) { TranslationData.prototype.load = function(domain, url) {
var that = this; var that = this;
return $.ajax({ return this.getHTTP(url).
dataType: "json", success(function(data) {
url: url,
success: function(data) {
//console.log("loaded translation data", data); //console.log("loaded translation data", data);
that.add(domain, data); that.add(domain, data);
}, }).
error: function(err, textStatus, errorThrown) { error(function(data, status) {
console.warn("Failed to load translation data: " + errorThrown); console.warn("Failed to load translation data: " + status);
that.add(domain, null); that.add(domain, null);
} });
});
}; };
TranslationData.prototype.get = function() { TranslationData.prototype.get = function() {
return this.data; return this.data;
}; };
var translationData = new TranslationData("en");
var create = function(ms) { var create = function(ms, launcher) {
// Create translation data instance.
var translationData = launcher.translationData = new TranslationData("en", launcher);
var modules = ['ui.bootstrap', 'ngSanitize', 'ngAnimate', 'ngHumanize', 'ngRoute']; var modules = ['ui.bootstrap', 'ngSanitize', 'ngAnimate', 'ngHumanize', 'ngRoute'];
if (ms && ms.length) { if (ms && ms.length) {
@ -179,11 +182,18 @@ define([
}; };
var initialize = function(app) { // Our API version as float. This value is incremented on
// breaking changes to plugins can check on it.
var apiversion = 1.1;
var deferred = $.Deferred(); var initialize = function(app, launcher) {
var globalContext = JSON.parse($("#globalcontext").text()); var deferred = launcher.$q.defer();
var globalContext = JSON.parse(document.getElementById("globalcontext").innerHTML);
if (!globalContext.Cfg.Version) {
globalContext.Cfg.Version = "unknown";
}
app.constant("globalContext", globalContext); app.constant("globalContext", globalContext);
// Configure language. // Configure language.
@ -233,16 +243,19 @@ define([
console.info("Selected language: "+lang); console.info("Selected language: "+lang);
// Set language and load default translations. // Set language and load default translations.
translationData.lang = lang; launcher.translationData.lang = lang;
var domain = "messages"; var translationDomain = "messages";
if (lang === translationData.default_lang) { if (lang === launcher.translationData.default_lang) {
// No need to load default language as it is built in. // No need to load default language as it is built in.
translationData.add(domain, null); launcher.translationData.add(translationDomain, null);
deferred.resolve(); deferred.resolve();
} else { } else {
// Load default translation catalog. // Load default translation catalog.
var url = require.toUrl('translation/' + domain + "-" + lang + '.json'); var url = require.toUrl('translation/'+translationDomain+"-"+lang+'.json');
$.when(translationData.load(domain, url)).always(function() { launcher.translationData.load(translationDomain, url).then(function() {
deferred.resolve();
}, function() {
// Ignore errors.
deferred.resolve(); deferred.resolve();
}); });
} }
@ -250,7 +263,7 @@ define([
// Set momemt language. // Set momemt language.
moment.lang(lang); moment.lang(lang);
return deferred.promise(); return deferred.promise;
}; };
@ -259,7 +272,7 @@ define([
initialize: initialize, initialize: initialize,
query: urlQuery, query: urlQuery,
config: appConfig, config: appConfig,
translationData: translationData apiversion: apiversion
}; };
}); });

5
static/js/base.js

@ -18,8 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
// Helper module to put non dependency base libraries together.
define([ "use strict";
define([ // Helper module to put non dependency base libraries together.
'modernizr', 'modernizr',
'moment', 'moment',
'Howler', 'Howler',

14
static/js/controllers/chatroomcontroller.js

@ -18,15 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
define(['underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html', 'text!partials/geolocation.html'], function(_, moment, templateFileInfo, templateContactRequest, templateGeolocation) {
"use strict";
define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html', 'text!partials/geolocation.html'], function($, _, moment, templateFileInfo, templateContactRequest, templateGeolocation) {
// ChatroomController // ChatroomController
return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) { return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) {
$scope.outputElement = $(".output", $element); $scope.outputElement = $element.find(".output");
$scope.inputElement = $(".input", $element); $scope.inputElement = $element.find(".input");
$scope.bodyElement = $(".chatbody", $element); $scope.bodyElement = $element.find(".chatbody");
$scope.menuElement = $(".chatmenu", $element); $scope.menuElement = $element.find(".chatmenu");
var lastSender = null; var lastSender = null;
var lastDate = null; var lastDate = null;
var lastMessageContainer = null; var lastMessageContainer = null;
@ -159,7 +161,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/co
$scope.canScroll = function() { $scope.canScroll = function() {
var o = $scope.outputElement.get(0); var o = $scope.outputElement[0];
if ((o.clientHeight - 20) < o.scrollHeight) { if ((o.clientHeight - 20) < o.scrollHeight) {
if (!scrollAfterInput && (o.clientHeight + 20) < (o.scrollHeight - o.scrollTop)) { if (!scrollAfterInput && (o.clientHeight + 20) < (o.scrollHeight - o.scrollTop)) {
// Manually scrolled -> do nothing. // Manually scrolled -> do nothing.

25
static/js/controllers/contactsmanagercontroller.js

@ -18,10 +18,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// ContactsmanagerController // ContactsmanagerController
return ["$scope", "$modalInstance", "contactData", "data", "contacts", 'dialogs', 'translation', function($scope, $modalInstance, contactData, data, contacts, dialogs, translation) { return ["$scope", "$modalInstance", "contactData", "data", "contacts", "dialogs", "translation", "mediaStream", "buddyData", function($scope, $modalInstance, contactData, data, contacts, dialogs, translation, mediaStream, buddyData) {
var getContactSessionId = function(userid) {
var session = null;
var scope = buddyData.lookup(userid, false, false);
if (scope) {
session = scope.session.get();
}
return session && session.Id ? session.Id : null;
};
$scope.header = data.header; $scope.header = data.header;
$scope.contacts = []; $scope.contacts = [];
$scope.openContactsManagerEdit = function(contact) { $scope.openContactsManagerEdit = function(contact) {
@ -36,7 +46,6 @@ define([], function() {
} }
); );
}; };
var updateContacts = function() { var updateContacts = function() {
$scope.contacts = contactData.getAll(); $scope.contacts = contactData.getAll();
}; };
@ -50,7 +59,17 @@ define([], function() {
contacts.e.on('contactremoved', function() { contacts.e.on('contactremoved', function() {
updateContacts(); updateContacts();
}); });
$scope.doCall = function(contact) {
mediaStream.webrtc.doCall(getContactSessionId(contact.Userid));
$modalInstance.close();
};
$scope.startChat = function(contact) {
$scope.$emit("startchat", getContactSessionId(contact.Userid), {
autofocus: true,
restore: true
});
$modalInstance.close();
};
}]; }];
}); });

2
static/js/controllers/contactsmanagereditcontroller.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// ContactsmanagereditController // ContactsmanagereditController

6
static/js/controllers/controllers.js

@ -18,22 +18,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([ define([
'underscore', 'underscore',
'controllers/mediastreamcontroller', 'controllers/mediastreamcontroller',
'controllers/statusmessagecontroller', 'controllers/statusmessagecontroller',
'controllers/chatroomcontroller', 'controllers/chatroomcontroller',
'controllers/roomchangecontroller',
'controllers/usersettingscontroller', 'controllers/usersettingscontroller',
'controllers/contactsmanagercontroller', 'controllers/contactsmanagercontroller',
'controllers/contactsmanagereditcontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController, UsersettingsController, ContactsmanagerController, ContactsmanagereditController) { 'controllers/contactsmanagereditcontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, UsersettingsController, ContactsmanagerController, ContactsmanagereditController) {
var controllers = { var controllers = {
MediastreamController: MediastreamController, MediastreamController: MediastreamController,
StatusmessageController: StatusmessageController, StatusmessageController: StatusmessageController,
ChatroomController: ChatroomController, ChatroomController: ChatroomController,
RoomchangeController: RoomchangeController,
UsersettingsController: UsersettingsController, UsersettingsController: UsersettingsController,
ContactsmanagerController: ContactsmanagerController, ContactsmanagerController: ContactsmanagerController,
ContactsmanagereditController: ContactsmanagereditController ContactsmanagereditController: ContactsmanagereditController

257
static/js/controllers/mediastreamcontroller.js

@ -18,9 +18,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function(_, BigScreen, moment, sjcl, Modernizr) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "userSettingsData", "localStatus", "dialogs", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, userSettingsData, localStatus, dialogs) { "use strict";
define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function($, _, angular, BigScreen, moment, sjcl, Modernizr) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "userSettingsData", "localStatus", "dialogs", "rooms", "constraints", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, userSettingsData, localStatus, dialogs, rooms, constraints) {
/*console.log("route", $route, $routeParams, $location);*/ /*console.log("route", $route, $routeParams, $location);*/
@ -86,29 +88,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
appData.set($scope); appData.set($scope);
var videoQualityMap = {
tiny: {
maxWidth: 80,
maxHeight: 45
},
low: {
maxWidth: 320,
maxHeight: 180
},
high: {
maxWidth: 640,
maxHeight: 360
},
hd: {
minWidth: 1280,
minHeight: 720
},
fullhd: {
minWidth: 1920,
minHeight: 1080
}
}
var displayName = safeDisplayName; var displayName = safeDisplayName;
// Init STUN and TURN servers. // Init STUN and TURN servers.
@ -147,6 +126,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
$scope.chatMessagesUnseen = 0; $scope.chatMessagesUnseen = 0;
$scope.autoAccept = null; $scope.autoAccept = null;
$scope.isCollapsed = true; $scope.isCollapsed = true;
$scope.roomsHistory = [];
$scope.defaults = { $scope.defaults = {
displayName: null, displayName: null,
buddyPicture: null, buddyPicture: null,
@ -219,156 +199,31 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
console.warn("This is not a WebRTC capable browser."); console.warn("This is not a WebRTC capable browser.");
return; return;
} }
var settings = $scope.master.settings; var settings = $scope.master.settings;
// Create iceServers from scope settings. // Create iceServers from scope.
var iceServers = []; var iceServers = [];
var iceServer; var iceServer;
if ($scope.stun.length) { if ($scope.stun.length) {
iceServer = createIceServers($scope.stun); iceServer = $window.createIceServers($scope.stun);
if (iceServer.length) { if (iceServer.length) {
iceServers.push.apply(iceServers, iceServer); iceServers.push.apply(iceServers, iceServer);
} }
} }
if ($scope.turn.urls && $scope.turn.urls.length) { if ($scope.turn.urls && $scope.turn.urls.length) {
iceServer = createIceServers($scope.turn.urls, $scope.turn.username, $scope.turn.password); iceServer = $window.createIceServers($scope.turn.urls, $scope.turn.username, $scope.turn.password);
if (iceServer.length) { if (iceServer.length) {
iceServers.push.apply(iceServers, iceServer); iceServers.push.apply(iceServers, iceServer);
} }
} }
mediaStream.webrtc.settings.pcConfig.iceServers = iceServers;
var audioConstraints = []; // Stereo.
var videoConstraints = [];
var videoConstraintsMandatory = {};
var screensharingConstraints = [];
var pushmulti = function(arrays, data) {
_.each(arrays, function(a) {
a.push(data);
});
};
// Chrome only constraints.
if ($scope.isChrome) {
// Audio settings.
// For defaults in Chromium see https://code.google.com/p/webrtc/source/browse/trunk/talk/media/webrtc/webrtcvoiceengine.cc#225
// Experimental audio settings.
if (settings.experimental.enabled) {
audioConstraints.push({
googEchoCancellation: true // defaults to true
});
audioConstraints.push({
googEchoCancellation2: settings.experimental.audioEchoCancellation2 && true // defaults to false in Chrome
});
audioConstraints.push({
googAutoGainControl: true // defaults to true
});
audioConstraints.push({
googAutoGainControl2: settings.experimental.audioAutoGainControl2 && true // defaults to false in Chrome
});
audioConstraints.push({
googNoiseSuppression: true // defaults to true
});
audioConstraints.push({
googgNoiseSuppression2: settings.experimental.audioNoiseSuppression2 && true // defaults to false in Chrome
});
audioConstraints.push({
googHighpassFilter: true // defaults to true
});
audioConstraints.push({
googTypingNoiseDetection: settings.experimental.audioTypingNoiseDetection && true // defaults to true in Chrome
});
}
if ($scope.supported.renderToAssociatedSink) {
audioConstraints.push({
// When true uses the default communications device on Windows.
// https://codereview.chromium.org/155863003
googDucking: true // defaults to true on Windows.
});
audioConstraints.push({
// Chrome will start rendering mediastream output to an output device that's associated with
// the input stream that was opened via getUserMedia.
// https://chromiumcodereview.appspot.com/23558010
chromeRenderToAssociatedSink: settings.audioRenderToAssociatedSkin && true // defaults to false in Chrome
});
}
// Select microphone device by id.
if (settings.microphoneId) {
audioConstraints.push({
sourceId: settings.microphoneId
});
}
// Select camera by device id.
if (settings.cameraId) {
videoConstraints.push({
sourceId: settings.cameraId
});
}
// Video settings.
if (settings.experimental.enabled) {
// Experimental video settings.
pushmulti([videoConstraints, screensharingConstraints], {
// Changes the way the video encoding adapts to the available bandwidth.
// https://code.google.com/p/webrtc/issues/detail?id=3351
googLeakyBucket: settings.experimental.videoLeakyBucket && true // defaults to false in Chrome
});
pushmulti([videoConstraints, screensharingConstraints], {
// Removes the noise in the captured video stream at the expense of CPU.
googNoiseReduction: settings.experimental.videoNoiseReduction && true // defaults to false in Chrome
});
pushmulti([videoConstraints, screensharingConstraints], {
googCpuOveruseDetection: settings.experimental.videoCpuOveruseDetection && true // defaults to true in Chrome
});
}
// Video.
videoConstraintsMandatory = $.extend(videoConstraintsMandatory, videoQualityMap[settings.videoQuality]);
// Not supported as of Firefox 27.
if (settings.maxFrameRate && settings.maxFrameRate != "auto") {
videoConstraintsMandatory.maxFrameRate = parseInt(settings.maxFrameRate, 10);
}
}
// Apply the shit.
mediaStream.webrtc.settings.stereo = settings.stereo; mediaStream.webrtc.settings.stereo = settings.stereo;
mediaStream.webrtc.settings.mediaConstraints.video.mandatory = videoConstraintsMandatory;
mediaStream.webrtc.settings.mediaConstraints.video.optional = videoConstraints;
mediaStream.webrtc.settings.mediaConstraints.audio = {
optional: audioConstraints
};
mediaStream.webrtc.settings.pcConfig.iceServers = iceServers;
mediaStream.webrtc.settings.screensharing.mediaConstraints.video.optional = screensharingConstraints;
// Inject optional stuff.
var optionalPcConstraints = mediaStream.webrtc.settings.pcConstraints.optional = [];
if ($window.webrtcDetectedBrowser === "chrome") {
// NOTE(longsleep): We can always enable SCTP data channels, as we have a workaround
// using the "active" event for Firefox < 27.
// SCTP does not work correctly with Chrome 31. Require M32.
if ($window.webrtcDetectedVersion >= 32) {
// SCTP is supported from Chrome M31.
// No need to pass DTLS constraint as it is on by default in Chrome M31.
// For SCTP, reliable and ordered is true by default.
} else {
// Chrome < M32 does not yet do DTLS-SRTP by default whereas Firefox only
// does DTLS-SRTP. In order to get interop, you must supply Chrome
// with a PC constructor constraint to enable DTLS.
console.warn("Turning on SCTP combatibility - please update your Chrome.");
optionalPcConstraints.push({
DtlsSrtpKeyAgreement: true
});
}
}
//console.log("WebRTC settings", mediaStream.webrtc.settings); // Refresh constraints.
constraints.refresh($scope.master.settings);
}; };
$scope.refreshWebrtcSettings(); // Call once for bootstrap. $scope.refreshWebrtcSettings(); // Call once for bootstrap.
@ -421,6 +276,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
} else { } else {
$scope.loadedUser = false; $scope.loadedUser = false;
} }
$scope.roomsHistory = [];
appData.e.triggerHandler("userSettingsLoaded", [$scope.loadedUser, $scope.user]);
$scope.reset(); $scope.reset();
}; };
@ -544,20 +401,18 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
// Unmark authorization process. // Unmark authorization process.
if (data.Userid) { if (data.Userid) {
mediaStream.users.authorizing(false); appData.authorizing(false);
} else if (!mediaStream.users.authorizing()) { } else if (!appData.authorizing()) {
// Trigger user data load when not in authorizing phase. // Trigger user data load when not in authorizing phase.
$scope.loadUserSettings(); $scope.loadUserSettings();
} }
if (!$rootScope.roomid && $scope.master.settings.defaultRoom) { // Select room if settings have an alternative default room.
if (rooms.inDefaultRoom() && $scope.master.settings.defaultRoom) {
console.log("Selecting default room from settings:", [$scope.master.settings.defaultRoom]); console.log("Selecting default room from settings:", [$scope.master.settings.defaultRoom]);
mediaStream.changeRoom($scope.master.settings.defaultRoom, true); rooms.joinByName($scope.master.settings.defaultRoom, true);
} }
// Always apply room after self received to avoid double stuff.
mediaStream.applyRoom();
}); });
mediaStream.webrtc.e.on("peercall", function(event, peercall) { mediaStream.webrtc.e.on("peercall", function(event, peercall) {
@ -660,36 +515,36 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
} }
}; };
mediaStream.connector.e.on("open error close", function(event, options) { $scope.$on("room.joined", function(ev) {
var t = event.type; // TODO(lcooper): Is it really needful to do this stuff?
var opts = $.extend({}, options);
$timeout.cancel(ttlTimeout); $timeout.cancel(ttlTimeout);
if (!opts.soft) { connected = true;
// Reset login information for anything not soft. reconnecting = false;
$scope.userid = $scope.suserid = null; $scope.updateStatus(true);
} });
switch (t) {
mediaStream.connector.e.on("open error close", function(event) {
$timeout.cancel(ttlTimeout);
$scope.userid = $scope.suserid = null;
switch (event.type) {
case "open": case "open":
t = "waiting";
connected = true; connected = true;
reconnecting = false; reconnecting = false;
$scope.updateStatus(true); $scope.updateStatus(true);
if (opts.soft) { $scope.setStatus("waiting");
return;
}
break; break;
case "error": case "error":
if (reconnecting || connected) { if (reconnecting || connected) {
reconnecting = false; reconnecting = false;
reconnect(); reconnect();
return; } else {
$scope.setStatus(event.type);
} }
break; break;
case "close": case "close":
reconnect(); reconnect();
return; break;
} }
$scope.setStatus(t);
}); });
mediaStream.webrtc.e.on("waitforusermedia connecting", function(event, currentcall) { mediaStream.webrtc.e.on("waitforusermedia connecting", function(event, currentcall) {
@ -797,23 +652,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
if (mediaStream.connector.connected) { if (mediaStream.connector.connected) {
$scope.setStatus("waiting"); $scope.setStatus("waiting");
} }
if ($scope.roomstatus) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
});
$scope.$watch("roomstatus", function(roomstatus) {
if (roomstatus && !$scope.peer) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else if (!$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
}); });
mediaStream.webrtc.e.on("busy", function(event, from) { mediaStream.webrtc.e.on("busy", function(event, from) {
@ -825,6 +663,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
}); });
mediaStream.webrtc.e.on("bye", function(event, reason, from) { mediaStream.webrtc.e.on("bye", function(event, reason, from) {
console.log("received bye", pickupTimeout, reason);
switch (reason) { switch (reason) {
case "busy": case "busy":
console.log("User is busy", reason, from); console.log("User is busy", reason, from);
@ -851,6 +690,13 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
console.log("User cannot accept call because of error"); console.log("User cannot accept call because of error");
alertify.dialog.alert(translation._("Oops") + "<br/>" + translation._("User hung up because of error.")); alertify.dialog.alert(translation._("Oops") + "<br/>" + translation._("User hung up because of error."));
break; break;
case "abort":
console.log("Remote call was aborted before we did pick up");
$scope.$emit("notification", "abortbeforepickup", {
reason: reason,
from: from
});
break;
} }
}); });
@ -885,10 +731,12 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
message = displayName(details.from) + translation._(" does not pick up."); message = displayName(details.from) + translation._(" does not pick up.");
break; break;
case "incomingbusy": case "incomingbusy":
toastr.info(moment().format("llll"), displayName(details.from) + translation._(" tried to call you.")); toastr.info(moment().format("lll"), displayName(details.from) + translation._(" tried to call you"));
break; break;
case "abortbeforepickup":
// Fall through
case "incomingpickuptimeout": case "incomingpickuptimeout":
toastr.info(moment().format("llll"), displayName(details.from) + translation._(" called you.")); toastr.info(moment().format("lll"), displayName(details.from) + translation._(" called you"));
break; break;
} }
if (message) { if (message) {
@ -919,6 +767,17 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
$scope.chatMessagesUnseen = $scope.chatMessagesUnseen - count; $scope.chatMessagesUnseen = $scope.chatMessagesUnseen - count;
}); });
$scope.$on("room.joined", function(event, roomName) {
if (roomName) {
_.pull($scope.roomsHistory, roomName);
$scope.roomsHistory.unshift(roomName);
if ($scope.roomsHistory.length > 15) {
// Limit the history.
$scope.roomsHistory = $scope.roomsHistory.splice(0, 15);
}
}
});
_.defer(function() { _.defer(function() {
if (!Modernizr.websockets) { if (!Modernizr.websockets) {
alertify.dialog.alert(translation._("Your browser is not supported. Please upgrade to a current version.")); alertify.dialog.alert(translation._("Your browser is not supported. Please upgrade to a current version."));

90
static/js/controllers/roomchangecontroller.js

@ -1,90 +0,0 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define([], function() {
// RoomchangeController
return ["$scope", "$element", "$window", "mediaStream", "$http", "$timeout", function($scope, $element, $window, mediaStream, $http, $timeout) {
//console.log("Room change controller", $element, $scope.roomdata);
var url = mediaStream.url.api("rooms");
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({});
});
};
$scope.changeRoomToId = function(id) {
return mediaStream.changeRoom(id);
};
$scope.refreshRoom = function() {
if (ctrl.enabled) {
ctrl.getRoom(function(roomdata) {
console.info("Retrieved room data", roomdata);
$scope.roomdata = roomdata;
$element.find(".btn-roomcreate").get(0).focus();
});
}
};
$scope.$on("$destroy", function() {
//console.log("Room change controller destroyed");
ctrl.enabled = false;
});
$scope.roomdata = {};
$scope.$watch("roomdata.name", function(n) {
//console.log("roomdata.name changed", n);
if (!n) {
n = "";
}
var u = encodeURIComponent(n);
$scope.roomdata.url = "/" + u;
$scope.roomdata.link = mediaStream.url.room(n);
});
var roomDataLinkInput = $element.find(".roomdata-link-input");
if (roomDataLinkInput.length) {
$timeout(function() {
$scope.refreshRoom();
}, 100);
}
}];
});

2
static/js/controllers/statusmessagecontroller.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// StatusmessageController // StatusmessageController

11
static/js/controllers/usersettingscontroller.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// UsersettingsController // UsersettingsController
@ -25,7 +27,7 @@ define([], function() {
$scope.withUsersForget = true; $scope.withUsersForget = true;
this.registerUserid = function(btn) { this.registerUserid = function(event) {
var successHandler = function(data) { var successHandler = function(data) {
console.info("Created new userid:", data.userid); console.info("Created new userid:", data.userid);
@ -38,8 +40,13 @@ define([], function() {
delete data.nonce; delete data.nonce;
}; };
var form = null;
if (event && event.target) {
form = event.target.form;
}
console.log("No userid - creating one ..."); console.log("No userid - creating one ...");
mediaStream.users.register(btn.form, function(data) { mediaStream.users.register(form, function(data) {
if (data.nonce) { if (data.nonce) {
successHandler(data); successHandler(data);
} else { } else {

2
static/js/directives/audiolevel.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore'], function($, _) { define(['jquery', 'underscore'], function($, _) {
return ["$window", "mediaStream", "safeApply", "animationFrame", function($window, mediaStream, safeApply, animationFrame) { return ["$window", "mediaStream", "safeApply", "animationFrame", function($window, mediaStream, safeApply, animationFrame) {

283
static/js/directives/audiovideo.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) { define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) {
return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", "animationFrame", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout, animationFrame) { return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", "animationFrame", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout, animationFrame) {
@ -26,19 +28,33 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
var peers = {}; var streams = {};
var events = $({}); var calls = {};
var getStreamId = function(stream, currentcall) {
var id = currentcall.id + "-" + stream.id;
console.log("Created stream ID", id);
return id;
};
// Dummy stream.
var dummy = {
id: "defaultDummyStream"
};
$scope.container = $element.get(0); $scope.container = $element[0];
$scope.layoutparent = $element.parent(); $scope.layoutparent = $element.parent();
$scope.remoteVideos = $element.find(".remoteVideos").get(0); $scope.remoteVideos = $element.find(".remoteVideos")[0];
$scope.localVideo = $element.find(".localVideo").get(0); $scope.localVideo = $element.find(".localVideo")[0];
$scope.miniVideo = $element.find(".miniVideo").get(0); $scope.miniVideo = $element.find(".miniVideo")[0];
$scope.mini = $element.find(".miniContainer").get(0); $scope.mini = $element.find(".miniContainer")[0];
$scope.hasUsermedia = false; $scope.hasUsermedia = false;
$scope.isActive = false; $scope.isActive = false;
$scope.haveStreams = false;
$scope.peersTalking = {};
$scope.rendererName = $scope.defaultRendererName = "democrazy"; $scope.rendererName = $scope.defaultRendererName = "democrazy";
@ -46,86 +62,125 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
$scope.addRemoteStream = function(stream, currentcall) { $scope.addRemoteStream = function(stream, currentcall) {
//console.log("Add remote stream to scope", pc.id, stream); var id = getStreamId(stream, currentcall);
// Create scope.
var subscope = $scope.$new(true); if (streams.hasOwnProperty(id)) {
console.warn("Cowardly refusing to add stream id twice", id, currentcall);
return;
}
var subscope;
// Dummy replacement support.
if (calls.hasOwnProperty(currentcall.id)) {
subscope = calls[currentcall.id];
if (stream === dummy) {
return;
}
if (subscope.dummy) {
subscope.$apply(function() {
subscope.attachStream(stream);
});
return;
}
} else {
// Create scope.
subscope = $scope.$new();
calls[currentcall.id] = subscope;
}
//console.log("Add remote stream to scope", stream.id, stream, currentcall);
var peerid = subscope.peerid = currentcall.id; var peerid = subscope.peerid = currentcall.id;
buddyData.push(peerid); buddyData.push(peerid);
subscope.unattached = true;
subscope.withvideo = false; subscope.withvideo = false;
subscope.onlyaudio = false; subscope.onlyaudio = false;
subscope.talking = false;
subscope.destroyed = false; subscope.destroyed = false;
subscope.applyTalking = function(talking) {
subscope.talking = !! talking;
safeApply(subscope);
};
subscope.$on("active", function() { subscope.$on("active", function() {
console.log("Stream scope is now active", peerid); console.log("Stream scope is now active", id, peerid);
events.triggerHandler("active." + peerid, [subscope, currentcall, stream]);
}); });
subscope.$on("$destroy", function() { subscope.$on("$destroy", function() {
console.log("Destroyed scope for audiovideo", subscope); console.log("Destroyed scope for stream", id, peerid);
subscope.destroyed = true; subscope.destroyed = true;
}); });
console.log("Created stream scope", peerid); console.log("Created stream scope", id, peerid);
// Add created scope. // Add created scope.
peers[peerid] = subscope; if (stream === dummy) {
subscope.dummy = true;
} else {
streams[id] = subscope;
}
// Render template. // Render template.
peerTemplate(subscope, function(clonedElement, scope) { peerTemplate(subscope, function(clonedElement, scope) {
$($scope.remoteVideos).append(clonedElement); $($scope.remoteVideos).append(clonedElement);
clonedElement.data("peerid", scope.peerid); clonedElement.data("peerid", scope.peerid);
scope.element = clonedElement; scope.element = clonedElement;
var video = clonedElement.find("video").get(0); scope.attachStream = function(stream) {
$window.attachMediaStream(video, stream); if (stream === dummy) {
// Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly.
videoWaiter.wait(video, stream, function(withvideo) {
if (scope.destroyed) {
console.log("Abort wait for video on destroyed scope.");
return; return;
} }
if (withvideo) { var video = clonedElement.find("video")[0];
scope.$apply(function($scope) { $window.attachMediaStream(video, stream);
$scope.withvideo = true; // Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly.
}); videoWaiter.wait(video, stream, function(withvideo) {
} else { if (scope.destroyed) {
console.info("Incoming stream has no video tracks."); console.log("Abort wait for video on destroyed scope.");
scope.$apply(function($scope) { return;
$scope.onlyaudio = true; }
}); if (withvideo) {
} scope.$apply(function($scope) {
scope.$emit("active", currentcall); $scope.withvideo = true;
$scope.redraw(); });
}, function() { } else {
if (scope.destroyed) { console.info("Incoming stream has no video tracks.");
console.log("No longer wait for video on destroyed scope."); scope.$apply(function($scope) {
return; $scope.onlyaudio = true;
} });
console.warn("We did not receive video data for remote stream", currentcall, stream, video); }
scope.$emit("active", currentcall); scope.$emit("active", currentcall);
$scope.redraw(); $scope.redraw();
}); }, function() {
if (scope.destroyed) {
console.log("No longer wait for video on destroyed scope.");
return;
}
console.warn("We did not receive video data for remote stream", currentcall, stream, video);
scope.$emit("active", currentcall);
$scope.redraw();
});
scope.unattached = false;
scope.dummy = false;
};
scope.doChat = function() { scope.doChat = function() {
$scope.$emit("startchat", currentcall.id, { $scope.$emit("startchat", currentcall.id, {
autofocus: true, autofocus: true,
restore: true restore: true
}); });
}; };
scope.attachStream(stream);
}); });
}; };
$scope.removeRemoteStream = function(stream, currentcall) { $scope.removeRemoteStream = function(stream, currentcall) {
var subscope = peers[currentcall.id]; //console.log("remove stream", stream, stream.id, currentcall);
var id = getStreamId(stream, currentcall);
var subscope = streams[id];
if (subscope) { if (subscope) {
buddyData.pop(currentcall.id); buddyData.pop(currentcall.id);
delete peers[currentcall.id]; delete streams[id];
//console.log("remove scope", subscope); //console.log("remove scope", subscope);
if (subscope.element) { if (subscope.element) {
subscope.element.remove(); subscope.element.remove();
} }
var callscope = calls[currentcall.id];
if (subscope === callscope) {
delete calls[currentcall.id];
}
subscope.$destroy(); subscope.$destroy();
$scope.redraw(); $scope.redraw();
} }
@ -134,17 +189,9 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
// Talking updates receiver. // Talking updates receiver.
mediaStream.api.e.on("received.talking", function(event, id, from, talking) { mediaStream.api.e.on("received.talking", function(event, id, from, talking) {
var scope = peers[from]; $scope.$apply(function(scope) {
//console.log("received.talking", talking, scope); scope.peersTalking[from] = !!talking;
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) { $scope.$on("active", function(currentcall) {
@ -171,69 +218,83 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
//console.log("Toggle full screen", BigScreen.enabled, $scope.isActive, $scope.hasUsermedia); //console.log("Toggle full screen", BigScreen.enabled, $scope.isActive, $scope.hasUsermedia);
if (BigScreen.enabled && ($scope.isActive || $scope.hasUsermedia)) { if (BigScreen.enabled && ($scope.isActive || $scope.hasUsermedia)) {
$scope.layoutparent.toggleClass("fullscreen"); $scope.layoutparent.toggleClass("fullscreen");
BigScreen.toggle($scope.layoutparent.get(0)); BigScreen.toggle($scope.layoutparent[0]);
} }
}; };
mediaStream.webrtc.e.on("usermedia", function(event, usermedia) { mediaStream.webrtc.e.on("usermedia", function(event, usermedia) {
//console.log("XXXXXXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia); //console.log("XXXX XXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia);
$scope.hasUsermedia = true; if ($scope.haveStreams) {
usermedia.attachMediaStream($scope.localVideo);
var count = 0; usermedia.attachMediaStream($scope.miniVideo);
var waitForLocalVideo = function() { $scope.redraw();
if (!$scope.hasUsermedia) {
return; } else {
}
if ($scope.localVideo.videoWidth > 0) { $scope.hasUsermedia = true;
$scope.localVideo.style.opacity = 1; usermedia.attachMediaStream($scope.localVideo);
$scope.redraw(); var count = 0;
} else { var waitForLocalVideo = function() {
count++; if (!$scope.hasUsermedia || $scope.isActive) {
if (count < 100) { return;
setTimeout(waitForLocalVideo, 100); }
if ($scope.localVideo.videoWidth > 0) {
$scope.localVideo.style.opacity = 1;
$scope.redraw();
} else { } else {
console.warn("Timeout while waiting for local video.") count++;
if (count < 100) {
setTimeout(waitForLocalVideo, 100);
} else {
console.warn("Timeout while waiting for local video.")
}
} }
} };
}; waitForLocalVideo();
waitForLocalVideo();
}
}); });
mediaStream.webrtc.e.on("done", function() { mediaStream.webrtc.e.on("done", function() {
$scope.hasUsermedia = false; $scope.$apply(function() {
$scope.isActive = false; $scope.hasUsermedia = false;
if (BigScreen.enabled) { $scope.isActive = false;
BigScreen.exit(); $scope.peersTalking = {};
} if (BigScreen.enabled) {
_.delay(function() { BigScreen.exit();
if ($scope.isActive) {
return;
} }
$scope.localVideo.src = ''; _.delay(function() {
$scope.miniVideo.src = ''; if ($scope.isActive) {
$($scope.remoteVideos).children(".remoteVideo").remove(); return;
}, 1500); }
$($scope.mini).removeClass("visible"); $scope.localVideo.src = '';
$scope.localVideo.style.opacity = 0; $scope.miniVideo.src = '';
$scope.remoteVideos.style.opacity = 0; $($scope.remoteVideos).children(".remoteVideo").remove();
$element.removeClass('active'); }, 1500);
_.each(peers, function(scope, k) { $($scope.mini).removeClass("visible");
scope.$destroy(); $scope.localVideo.style.opacity = 0;
delete peers[k]; $scope.remoteVideos.style.opacity = 0;
$element.removeClass('active');
_.each(streams, function(scope, k) {
scope.$destroy();
delete streams[k];
});
$scope.rendererName = $scope.defaultRendererName;
$scope.haveStreams = false;
}); });
$scope.rendererName = $scope.defaultRendererName;
}); });
mediaStream.webrtc.e.on("streamadded", function(event, stream, currentcall) { mediaStream.webrtc.e.on("streamadded", function(event, stream, currentcall) {
console.log("Remote stream added.", stream, currentcall); console.log("Remote stream added.", stream, currentcall);
if (_.isEmpty(peers)) { if (!$scope.haveStreams) {
//console.log("First stream"); //console.log("First stream");
$window.reattachMediaStream($scope.miniVideo, $scope.localVideo); $window.reattachMediaStream($scope.miniVideo, $scope.localVideo);
$scope.haveStreams = true;
} }
$scope.addRemoteStream(stream, currentcall); $scope.addRemoteStream(stream, currentcall);
@ -246,8 +307,26 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
}); });
mediaStream.webrtc.e.on("statechange", function(event, iceConnectionState, currentcall) {
if (!$scope.haveStreams) {
return;
}
switch (iceConnectionState) {
case "new":
case "checking":
case "connected":
case "completed":
case "failed":
$scope.addRemoteStream(dummy, currentcall);
break;
}
});
return { return {
peers: peers streams: streams
}; };
}]; }];

2
static/js/directives/bfi.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(["bootstrap-file-input"], function() { define(["bootstrap-file-input"], function() {
// bfi // bfi

65
static/js/directives/buddylist.js

@ -18,10 +18,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['underscore', 'text!partials/buddylist.html'], function(_, template) { define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
// buddyList // buddyList
return ["$compile", "buddyList", "mediaStream", "contacts", function($compile, buddyList, mediaStream, contacts) { return ["buddyList", "api", "webrtc", "contacts", function(buddyList, api, webrtc, contacts) {
//console.log("buddyList directive"); //console.log("buddyList directive");
@ -30,10 +32,34 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
$scope.layout.buddylist = false; $scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true; $scope.layout.buddylistAutoHide = true;
$scope.doCall = function(id) { var inRoom = false;
var updateBuddyListVisibility = function() {
if (inRoom && !$scope.peer) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else if (!$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
};
mediaStream.webrtc.doCall(id); webrtc.e.on("done", function() {
$scope.$apply(updateBuddyListVisibility);
});
$scope.$on("room.joined", function(ev) {
inRoom = true;
updateBuddyListVisibility();
});
$scope.$on("room.left", function(ev) {
inRoom = false;
buddylist.onClosed();
updateBuddyListVisibility();
});
$scope.doCall = function(id) {
webrtc.doCall(id);
}; };
$scope.doChat = function(id) { $scope.doChat = function(id) {
@ -61,23 +87,6 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
}; };
/*
$scope.doAudioConference = function(id) {
$scope.updateAutoAccept(id);
mediaStream.api.sendChat(id, null, {
AutoCall: {
Type: "conference",
Id: mediaStream.connector.roomid
}
})
};*/
$scope.setRoomStatus = function(status) {
$scope.$emit("roomStatus", status);
};
var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {}); var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {});
var onJoined = _.bind(buddylist.onJoined, buddylist); var onJoined = _.bind(buddylist.onJoined, buddylist);
var onLeft = _.bind(buddylist.onLeft, buddylist); var onLeft = _.bind(buddylist.onLeft, buddylist);
@ -85,15 +94,14 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
var onContactAdded = _.bind(buddylist.onContactAdded, buddylist); var onContactAdded = _.bind(buddylist.onContactAdded, buddylist);
var onContactRemoved = _.bind(buddylist.onContactRemoved, buddylist); var onContactRemoved = _.bind(buddylist.onContactRemoved, buddylist);
var onContactUpdated = _.bind(buddylist.onContactUpdated, buddylist); var onContactUpdated = _.bind(buddylist.onContactUpdated, buddylist);
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) { api.e.on("received.userleftorjoined", function(event, dataType, data) {
if (dataType === "Left") { if (dataType === "Left") {
onLeft(data); onLeft(data);
} else { } else {
onJoined(data); onJoined(data);
} }
}); });
mediaStream.api.e.on("received.users", function(event, data) { api.e.on("received.users", function(event, data) {
$scope.setRoomStatus(true);
var selfId = $scope.id; var selfId = $scope.id;
_.each(data, function(p) { _.each(data, function(p) {
if (p.Id !== selfId) { if (p.Id !== selfId) {
@ -102,17 +110,10 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
}); });
$scope.$apply(); $scope.$apply();
}); });
mediaStream.api.e.on("received.status", function(event, data) { api.e.on("received.status", function(event, data) {
onStatus(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();
});
// Contacts. // Contacts.
contacts.e.on("contactadded", function(event, data) { contacts.e.on("contactadded", function(event, data) {
onContactAdded(data); onContactAdded(data);

14
static/js/directives/buddypicturecapture.js

@ -18,10 +18,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], function($, _, template) { define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], function($, _, template) {
// buddyPictureCapture // buddyPictureCapture
return ["$compile", function($compile) { return ["$compile", "$window", function($compile, $window) {
var controller = ['$scope', 'safeApply', '$timeout', '$q', function($scope, safeApply, $timeout, $q) { var controller = ['$scope', 'safeApply', '$timeout', '$q', function($scope, safeApply, $timeout, $q) {
@ -124,13 +126,13 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct
}] }]
}; };
} }
getUserMedia({ $window.getUserMedia({
video: videoConstraints video: videoConstraints
}, function(stream) { }, function(stream) {
$scope.showTakePicture = true; $scope.showTakePicture = true;
localStream = stream; localStream = stream;
$scope.waitingForPermission = false; $scope.waitingForPermission = false;
attachMediaStream($scope.video, stream); $window.attachMediaStream($scope.video, stream);
safeApply($scope); safeApply($scope);
videoAllowed.resolve(true); videoAllowed.resolve(true);
}, function(error) { }, function(error) {
@ -176,10 +178,10 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct
var link = function($scope, $element, $attrs, modelController) { var link = function($scope, $element, $attrs, modelController) {
$scope.video = $element.find("video").get(0); $scope.video = $element.find("video")[0];
$scope.flash = $element.find(".videoFlash"); $scope.flash = $element.find(".videoFlash");
$scope.canvasPic = $element.find("canvas.videoPic").get(0); $scope.canvasPic = $element.find("canvas.videoPic")[0];
$scope.canvasPrev = $element.find("canvas.videoPrev").get(0); $scope.canvasPrev = $element.find("canvas.videoPrev")[0];
$($scope.canvasPic).attr($scope.captureSize); $($scope.canvasPic).attr($scope.captureSize);
$scope.save = function() { $scope.save = function() {

46
static/js/directives/buddypictureupload.js

@ -18,11 +18,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], function($, _, template) { define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], function($, _, template) {
// buddyPictureUpload // buddyPictureUpload
return ["$compile", function($compile) { return ["$compile", function($compile) {
var getNumFromPx = function(px) {
var num = px.match(/[\-0-9]+/);
if (num) {
num = Number(num[0]);
} else {
num = 0;
}
return num;
};
var controller = ['$scope', 'safeApply', '$timeout', '$q', 'translation', function($scope, safeApply, $timeout, $q, translation) { var controller = ['$scope', 'safeApply', '$timeout', '$q', 'translation', function($scope, safeApply, $timeout, $q, translation) {
var previewWidth = 205; var previewWidth = 205;
@ -40,6 +52,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.upload = { $scope.upload = {
status: 0 status: 0
}; };
$scope.aspectRatio = 1;
var completedUpload = function() { var completedUpload = function() {
$scope.upload.status = 100; $scope.upload.status = 100;
@ -49,6 +62,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.prevImage.onload = function() { $scope.prevImage.onload = function() {
// clear old dimensions // clear old dimensions
this.style.cssText = null; this.style.cssText = null;
$scope.aspectRatio = this.width/this.height;
// get new dimensions // get new dimensions
var dim = getAutoFitDimensions(this, {width: previewWidth, height: previewHeight}); var dim = getAutoFitDimensions(this, {width: previewWidth, height: previewHeight});
this.style.width = dim.width + 'px'; this.style.width = dim.width + 'px';
@ -61,10 +75,6 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.prevImage.src = data; $scope.prevImage.src = data;
}; };
var getNumFromPx = function(px) {
return px.match(/[\-0-9]+/) ? Number(px.match(/[\-0-9]+/)[0]) : 0;
};
// Auto fit by smallest dimension // Auto fit by smallest dimension
var getAutoFitDimensions = function(from, to) { var getAutoFitDimensions = function(from, to) {
if (!from.width && !from.height && !to.width && !to.height) { if (!from.width && !from.height && !to.width && !to.height) {
@ -149,7 +159,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
//console.log('file progress', event); //console.log('file progress', event);
var percentComplete = event.loaded/event.total * 100; var percentComplete = event.loaded/event.total * 100;
// show complete only when src is loaded in image element // show complete only when src is loaded in image element
if(percentComplete != 100) { if (percentComplete !== 100) {
$scope.$apply(function(scope) { $scope.$apply(function(scope) {
$scope.upload.status = percentComplete; $scope.upload.status = percentComplete;
}); });
@ -203,7 +213,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
var link = function($scope, $element) { var link = function($scope, $element) {
$scope.prevImage = $element.find("img.preview").get(0); $scope.prevImage = $element.find("img.preview")[0];
$scope.clearInput = function() { $scope.clearInput = function() {
$element.find("input[type=file]")[0].value = ""; $element.find("input[type=file]")[0].value = "";
}; };
@ -216,17 +226,21 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
}; };
var pxDefaultMovementSpeed = 5; var pxDefaultMovementSpeed = 5;
// Return the correct image height based on changes to the image width.
var calcHeight = function(width) {
return (getNumFromPx(width) / $scope.aspectRatio) + 'px';
};
var incrementPx = function(num, pxToMove) { var incrementPx = function(num, pxToMove) {
if(pxToMove === undefined) { if (pxToMove === undefined) {
pxToMove = pxDefaultMovementSpeed; pxToMove = pxDefaultMovementSpeed;
} }
return ((Number(num.match(/[\-0-9]+/)) + pxToMove) + 'px'); return (getNumFromPx(num) + pxToMove) + 'px';
}; };
var decrementPx = function(num, pxToMove) { var decrementPx = function(num, pxToMove) {
if(pxToMove === undefined) { if (pxToMove === undefined) {
pxToMove = pxDefaultMovementSpeed; pxToMove = pxDefaultMovementSpeed;
} }
return ((Number(num.match(/[\-0-9]+/)) - pxToMove) + 'px'); return (getNumFromPx(num) - pxToMove) + 'px';
}; };
var moveImageUp = function(pxMove) { var moveImageUp = function(pxMove) {
$scope.prevImage.style.top = decrementPx($scope.prevImage.style.top, pxMove); $scope.prevImage.style.top = decrementPx($scope.prevImage.style.top, pxMove);
@ -241,15 +255,15 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.prevImage.style.left = incrementPx($scope.prevImage.style.left, pxMove); $scope.prevImage.style.left = incrementPx($scope.prevImage.style.left, pxMove);
}; };
var makeImageLarger = function() { var makeImageLarger = function() {
$scope.prevImage.style.height = incrementPx($scope.prevImage.style.height);
moveImageLeft(1);
$scope.prevImage.style.width = incrementPx($scope.prevImage.style.width); $scope.prevImage.style.width = incrementPx($scope.prevImage.style.width);
$scope.prevImage.style.height = calcHeight($scope.prevImage.style.width);
moveImageLeft(1);
moveImageUp(2); moveImageUp(2);
}; };
var makeImageSmaller = function() { var makeImageSmaller = function() {
$scope.prevImage.style.height = decrementPx($scope.prevImage.style.height);
moveImageRight(1);
$scope.prevImage.style.width = decrementPx($scope.prevImage.style.width); $scope.prevImage.style.width = decrementPx($scope.prevImage.style.width);
$scope.prevImage.style.height = calcHeight($scope.prevImage.style.width);
moveImageRight(1);
moveImageDown(2); moveImageDown(2);
}; };
var changeImage = function(evt) { var changeImage = function(evt) {
@ -263,8 +277,8 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
} }
}; };
$element.find(".fa-long-arrow-up").on('mousedown', null, {intervalNum: intervalNum, action: moveImageUp}, changeImage); $element.find(".fa-long-arrow-up").on('mousedown', null, {intervalNum: intervalNum, action: moveImageDown}, changeImage);
$element.find(".fa-long-arrow-down").on('mousedown', null, {intervalNum: intervalNum, action: moveImageDown}, changeImage); $element.find(".fa-long-arrow-down").on('mousedown', null, {intervalNum: intervalNum, action: moveImageUp}, changeImage);
$element.find(".fa-long-arrow-up").on('mouseup', null, {intervalNum: intervalNum}, changeImage); $element.find(".fa-long-arrow-up").on('mouseup', null, {intervalNum: intervalNum}, changeImage);
$element.find(".fa-long-arrow-down").on('mouseup', null, {intervalNum: intervalNum}, changeImage); $element.find(".fa-long-arrow-down").on('mouseup', null, {intervalNum: intervalNum}, changeImage);

40
static/js/directives/chat.js

@ -18,7 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function(_, templateChat, templateChatroom) {
"use strict";
define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function($, _, templateChat, templateChatroom) {
return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "appData", "$timeout", "geolocation", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, appData, $timeout, geolocation) { return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "appData", "$timeout", "geolocation", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, appData, $timeout, geolocation) {
@ -44,16 +46,13 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
var res = []; var res = [];
for (var i = 0; i < ctrl.visibleRooms.length; i++) { for (var i = 0; i < ctrl.visibleRooms.length; i++) {
var r = rooms[ctrl.visibleRooms[i]]; var r = rooms[ctrl.visibleRooms[i]];
if (!r || r.id === ctrl.group) { if (!r) {
continue; continue;
} }
res.push(r); res.push(r);
} }
return res; return res;
}; };
$scope.getGroupRoom = function() {
return rooms[ctrl.group];
};
mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p) { mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p) {
@ -182,22 +181,31 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.showGroupRoom = function(settings, options) { scope.showGroupRoom = function(settings, options) {
var stngs = $.extend({ var stngs = $.extend({
title: translation._("Room chat") title: translation._("Room chat"),
group: true
}, settings); }, settings);
return scope.showRoom(controller.group, stngs, options); return scope.showRoom(controller.group, stngs, options);
}; };
scope.hideGroupRoom = function(settings, options) {
return scope.hideRoom(controller.group);
};
scope.showRoom = function(id, settings, opts) { scope.showRoom = function(id, settings, opts) {
var options = $.extend({}, opts); var options = $.extend({}, opts);
var subscope = controller.rooms[id]; var subscope = controller.rooms[id];
var index = controller.visibleRooms.length; var index = controller.visibleRooms.length;
if (!subscope) { if (!subscope) {
console.log("Create new chatroom", [id]); console.log("Create new chatroom", [id]);
controller.visibleRooms.push(id); if (settings.group) {
controller.visibleRooms.unshift(id);
} else {
controller.visibleRooms.push(id);
}
subscope = controller.rooms[id] = scope.$new(); subscope = controller.rooms[id] = scope.$new();
translation.inject(subscope); translation.inject(subscope);
subscope.id = id; subscope.id = id;
subscope.isgroupchat = id === controller.group ? true : false; subscope.isgroupchat = !!settings.group;
subscope.index = index; subscope.index = index;
subscope.settings = settings; subscope.settings = settings;
subscope.visible = false; subscope.visible = false;
@ -474,11 +482,6 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.currentRoomActive = false; scope.currentRoomActive = false;
} }
if (!controller.visibleRooms.length) { if (!controller.visibleRooms.length) {
scope.showGroupRoom(null, {
restore: true,
noenable: true,
noactivate: true
});
// If last visible room was removed, hide chat. // If last visible room was removed, hide chat.
scope.layout.chat = false; scope.layout.chat = false;
} }
@ -544,18 +547,23 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.layout.chatMaximized = false; scope.layout.chatMaximized = false;
}); });
scope.$on("room", function(event, room) { scope.$on("room.updated", function(event, room) {
var subscope = scope.showGroupRoom(null, { var subscope = scope.showGroupRoom(null, {
restore: true, restore: true,
noenable: true, noenable: true,
noactivate: true noactivate: true
}); });
if (room) { if (scope.currentRoomName != room.Name) {
var msg = $("<span>").text(translation._("You are now in room %s ...", room)); var msg = $("<span>").text(translation._("You are now in room %s ...", room.Name));
subscope.$broadcast("display", null, $("<i>").append(msg)); subscope.$broadcast("display", null, $("<i>").append(msg));
scope.currentRoomName = room.Name;
} }
}); });
scope.$on("room.left", function(event) {
scope.hideGroupRoom();
scope.currentRoomName = null;
});
}; };
}; };

2
static/js/directives/contactrequest.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore'], function($, _) { define(['jquery', 'underscore'], function($, _) {
return ["translation", "buddyData", "contacts", function(translation, buddyData, contacts) { return ["translation", "buddyData", "contacts", function(translation, buddyData, contacts) {

2
static/js/directives/defaultdialog.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['text!partials/defaultdialog.html'], function(defaultDialogTemplate) { define(['text!partials/defaultdialog.html'], function(defaultDialogTemplate) {
// defautlDialog // defautlDialog

12
static/js/directives/directives.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([ define([
'underscore', 'underscore',
@ -43,7 +45,9 @@ define([
'directives/odfcanvas', 'directives/odfcanvas',
'directives/presentation', 'directives/presentation',
'directives/youtubevideo', 'directives/youtubevideo',
'directives/bfi'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi) { 'directives/bfi',
'directives/title',
'directives/welcome'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi, title, welcome) {
var directives = { var directives = {
onEnter: onEnter, onEnter: onEnter,
@ -68,13 +72,15 @@ define([
odfcanvas: odfcanvas, odfcanvas: odfcanvas,
presentation: presentation, presentation: presentation,
youtubevideo: youtubevideo, youtubevideo: youtubevideo,
bfi: bfi bfi: bfi,
title: title,
welcome: welcome
}; };
var initialize = function(angModule) { var initialize = function(angModule) {
_.each(directives, function(directive, name) { _.each(directives, function(directive, name) {
angModule.directive(name, directive); angModule.directive(name, directive);
}) });
}; };
return { return {

4
static/js/directives/fileinfo.js

@ -18,7 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
define(['jquery', 'underscore'], function($, _) {
"use strict";
define(['jquery', 'underscore', 'moment'], function($, _, moment) {
return ["fileData", "fileUpload", "fileDownload", "mediaStream", "$window", "alertify", "translation", function(fileData, fileUpload, fileDownload, mediaStream, $window, alertify, translation) { return ["fileData", "fileUpload", "fileDownload", "mediaStream", "$window", "alertify", "translation", function(fileData, fileUpload, fileDownload, mediaStream, $window, alertify, translation) {

2
static/js/directives/odfcanvas.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['require', 'underscore', 'jquery'], function(require, _, $) { define(['require', 'underscore', 'jquery'], function(require, _, $) {
return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) { return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) {

2
static/js/directives/onenter.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// onEnter // onEnter

2
static/js/directives/onescape.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// onEscape // onEscape

52
static/js/directives/page.js

@ -18,48 +18,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['text!partials/page.html', 'text!partials/page/welcome.html'], function(template, welcome) { define(['text!partials/page.html', 'text!partials/page/welcome.html'], function(template, welcome) {
return ["$templateCache", "mediaStream", function($templateCache, mediaStream) { return ["$templateCache", "$timeout", "rooms", function($templateCache, $timeout, rooms) {
$templateCache.put('page/welcome.html', welcome); $templateCache.put('page/welcome.html', welcome);
var link = function(scope, element, attrs) { var link = function($scope, $element, attrs) {
$scope.$on("room.joined", function(event) {
scope.room = false; // Show no page when joined a room.
scope.page = null; $scope.page = null;
});
if (mediaStream.config.DefaultRoomEnabled !== true) { $scope.$on("room.random", function(ev, roomdata) {
// Show welcome page on room random events.
scope.$on("welcome", function() { $timeout(function() {
if (!scope.initialized) { $scope.page = "page/welcome.html";
scope.initialized = true;
scope.refresh();
}
}); });
});
scope.$on("room", function(event, room) {
scope.initialized = true;
scope.room = room !== null ? true : false;
scope.refresh();
});
scope.$watch("status", function(event) {
if (scope.initialized) {
scope.refresh();
}
});
scope.refresh = function() {
if (scope.roomid || scope.room || scope.status !== "waiting") {
scope.page = null;
} else {
scope.page = "page/welcome.html";
}
};
}
}; };
return { return {
@ -67,8 +44,7 @@ define(['text!partials/page.html', 'text!partials/page/welcome.html'], function(
replace: true, replace: true,
template: template, template: template,
link: link link: link
} };
}]; }];
}); });

2
static/js/directives/pdfcanvas.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['require', 'underscore', 'jquery'], function(require, _, $) { define(['require', 'underscore', 'jquery'], function(require, _, $) {
return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) { return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) {

14
static/js/directives/presentation.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], function($, _, template, BigScreen) { define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], function($, _, template, BigScreen) {
return ["$window", "mediaStream", "fileUpload", "fileDownload", "alertify", "translation", "randomGen", "fileData", function($window, mediaStream, fileUpload, fileDownload, alertify, translation, randomGen, fileData) { return ["$window", "mediaStream", "fileUpload", "fileDownload", "alertify", "translation", "randomGen", "fileData", function($window, mediaStream, fileUpload, fileDownload, alertify, translation, randomGen, fileData) {
@ -685,7 +687,7 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'],
if (elem) { if (elem) {
BigScreen.toggle(elem); BigScreen.toggle(elem);
} else { } else {
BigScreen.toggle(pane.get(0)); BigScreen.toggle(pane[0]);
} }
} }
@ -704,11 +706,13 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'],
}; };
mediaStream.webrtc.e.on("done", function() { mediaStream.webrtc.e.on("done", function() {
_.each($scope.availablePresentations, function(presentation) { $scope.$apply(function() {
presentation.clear(); _.each($scope.availablePresentations, function(presentation) {
presentation.clear();
});
$scope.availablePresentations = [];
$scope.activeDownloads = [];
}); });
$scope.availablePresentations = [];
$scope.activeDownloads = [];
}); });
$(document).on("keyup", function(event) { $(document).on("keyup", function(event) {

55
static/js/directives/roombar.js

@ -18,49 +18,59 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
define(['underscore', 'text!partials/roombar.html'], function(_, template) {
"use strict";
define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angular, template) {
// roomBar // roomBar
return ["$window", "$rootScope", "mediaStream", function($window, $rootScope, mediaStream) { return ["$window", "rooms", function($window, rooms) {
var link = function($scope, $element) {
var link = function($scope) { var clearRoomName = function() {
$scope.currentRoomName = null;
$scope.newRoomName = "";
};
//console.log("roomBar directive link", arguments); //console.log("roomBar directive link", arguments);
$scope.newroomid = $rootScope.roomid; $scope.layout.roombar = true;
$scope.layout.roombar = false;
$scope.save = function() { $scope.save = function() {
var roomid = mediaStream.changeRoom($scope.newroomid); if ($scope.roombarform.$invalid) {
if (roomid !== $rootScope.roomid) { return;
$scope.roombarform.$setPristine();
} }
$scope.layout.roombar = false; var roomName = rooms.joinByName($scope.newRoomName);
}; if (roomName !== $scope.currentRoomName) {
$scope.roombarform.$setPristine();
$scope.hitEnter = function(evt) {
if (angular.equals(evt.keyCode, 13)) {
$scope.save();
} }
}; };
$scope.exit = function() { $scope.exit = function() {
$scope.newroomid = ""; $scope.newRoomName = "";
$scope.save(); $scope.save();
}; };
$rootScope.$watch("roomid", function(newroomid, roomid) { $scope.$on("room.updated", function(ev, room) {
if (!newroomid) { $scope.currentRoomName = $scope.newRoomName = room.Name;
newroomid = "";
}
$scope.newroomid = newroomid;
}); });
$scope.$watch("newroomid", function(newroomid) { $scope.$on("room.left", clearRoomName);
if (newroomid === $rootScope.roomid) {
$scope.$watch("newRoomName", function(name) {
if (name === $scope.currentRoomName) {
$scope.roombarform.$setPristine(); $scope.roombarform.$setPristine();
} }
}); });
$scope.$watch("layout.roombar", function(value) {
$element.find("input").focus();
});
$scope.$watch("peer", function(peer) {
$scope.layout.roombar = !peer;
});
clearRoomName();
}; };
return { return {
@ -68,7 +78,6 @@ define(['underscore', 'text!partials/roombar.html'], function(_, template) {
replace: true, replace: true,
scope: true, scope: true,
template: template, template: template,
controller: "RoomchangeController",
link: link link: link
} }

9
static/js/directives/screenshare.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials/screensharepeer.html', 'bigscreen', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) { define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials/screensharepeer.html', 'bigscreen', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) {
return ["$window", "mediaStream", "$compile", "safeApply", "videoWaiter", "$timeout", "alertify", "translation", "screensharing", function($window, mediaStream, $compile, safeApply, videoWaiter, $timeout, alertify, translation, screensharing) { return ["$window", "mediaStream", "$compile", "safeApply", "videoWaiter", "$timeout", "alertify", "translation", "screensharing", function($window, mediaStream, $compile, safeApply, videoWaiter, $timeout, alertify, translation, screensharing) {
@ -132,7 +134,7 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
peerTemplate(subscope, function(clonedElement, scope) { peerTemplate(subscope, function(clonedElement, scope) {
pane.append(clonedElement); pane.append(clonedElement);
scope.element = clonedElement; scope.element = clonedElement;
var video = clonedElement.find("video").get(0); var video = clonedElement.find("video")[0];
$window.attachMediaStream(video, stream); $window.attachMediaStream(video, stream);
videoWaiter.wait(video, stream, function() { videoWaiter.wait(video, stream, function() {
console.log("Screensharing size: ", video.videoWidth, video.videoHeight); console.log("Screensharing size: ", video.videoWidth, video.videoHeight);
@ -246,7 +248,6 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
mediaStream.tokens.off(token, handler); mediaStream.tokens.off(token, handler);
mediaStream.webrtc.e.off("statechange", updater); mediaStream.webrtc.e.off("statechange", updater);
handler = null; handler = null;
updated = null;
// Send by to all connected peers. // Send by to all connected peers.
_.each(screenshares, function(peerscreenshare) { _.each(screenshares, function(peerscreenshare) {
peerscreenshare.send({ peerscreenshare.send({
@ -312,14 +313,14 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
if (elem) { if (elem) {
BigScreen.toggle(elem); BigScreen.toggle(elem);
} else { } else {
BigScreen.toggle(pane.get(0)); BigScreen.toggle(pane[0]);
} }
} }
}; };
mediaStream.webrtc.e.on("done", function() { mediaStream.webrtc.e.on("done", function() {
$scope.stopScreenshare(); $scope.$apply($scope.stopScreenshare);
}); });
$scope.$watch("layout.screenshare", function(newval, oldval) { $scope.$watch("layout.screenshare", function(newval, oldval) {

160
static/js/directives/settings.js

@ -18,14 +18,40 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, template) { define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, template) {
var videoQualityMap = {
tiny: {
maxWidth: 80,
maxHeight: 45
},
low: {
maxWidth: 320,
maxHeight: 180
},
high: {
maxWidth: 640,
maxHeight: 360
},
hd: {
minWidth: 1280,
minHeight: 720
},
fullhd: {
minWidth: 1920,
minHeight: 1080
}
};
return ["$compile", "mediaStream", function($compile, mediaStream) { return ["$compile", "mediaStream", function($compile, mediaStream) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', 'userSettingsData', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage, userSettingsData) { var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', 'userSettingsData', 'constraints', 'appData', '$timeout', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage, userSettingsData, constraints, appData, $timeout) {
$scope.layout.settings = false; $scope.layout.settings = false;
$scope.showAdvancedSettings = true; $scope.showAdvancedSettings = true;
$scope.autoshowSettings = true;
$scope.rememberSettings = true; $scope.rememberSettings = true;
$scope.desktopNotify = desktopNotify; $scope.desktopNotify = desktopNotify;
$scope.mediaSources = mediaSources; $scope.mediaSources = mediaSources;
@ -85,28 +111,15 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
}; };
$scope.checkDefaultMediaSources = function() { $scope.checkDefaultMediaSources = function() {
// Check if the stuff exists.
if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) { if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) {
$scope.master.settings.microphoneId = null; $scope.master.settings.microphoneId = null;
} }
if ($scope.master.settings.cameraId && !$scope.mediaSources.hasVideoId($scope.master.settings.cameraId)) { if ($scope.master.settings.cameraId && !$scope.mediaSources.hasVideoId($scope.master.settings.cameraId)) {
$scope.master.settings.cameraId = null; $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) { $scope.$watch("layout.settings", function(showSettings, oldValue) {
if (showSettings) { if (showSettings) {
$scope.desktopNotify.refresh(); $scope.desktopNotify.refresh();
@ -125,23 +138,132 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
$scope.user.settings.cameraId = video[0].id; $scope.user.settings.cameraId = video[0].id;
} }
}); });
$scope.refreshWebrtcSettings();
}); });
} else if (!showSettings && oldValue) { } else if (!showSettings && oldValue) {
$scope.saveSettings(); $scope.saveSettings();
} }
}); });
}]; $scope.maybeShowSettings = function() {
if ($scope.autoshowSettings) {
$scope.autoshowSettings = false;
if (!$scope.loadedUser) {
$scope.layout.settings = true;
}
}
};
$scope.$on("room.joined", function() {
$timeout($scope.maybeShowSettings);
});
appData.e.on("authenticationChanged", function() {
$scope.autoshowSettings = true;
$timeout($scope.maybeShowSettings);
});
constraints.e.on("refresh", function(event, constraints) {
var settings = $scope.master.settings;
// Assert that selected devices are there.
(function() {
var deferred = constraints.defer();
mediaSources.refresh(function() {
$scope.checkDefaultMediaSources();
// Select microphone device by id.
if (settings.microphoneId) {
constraints.add("audio", "sourceId", settings.microphoneId);
}
// Select camera by device id.
if (settings.cameraId) {
constraints.add("video", "sourceId", settings.cameraId);
}
if (!mediaSources.hasAudio()) {
constraints.disable('audio');
console.info("Disabled audio input as no audio source was found.");
}
if (!mediaSources.hasVideo()) {
constraints.disable('video');
console.info("Disabled video input as no video source was found.");
}
deferred.resolve("complete");
});
})();
// Chrome only constraints.
if ($scope.isChrome) {
// Chrome specific constraints overview:
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/mediaconstraintsinterface.cc
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/videosource.cc (video constraints)
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/localaudiosource.cc (audio constraints)
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/webrtcsession.cc (pc constraints)
var link = function($scope, $element) {}; // Experimental audio settings.
if (settings.experimental.enabled) {
constraints.add("audio", "googEchoCancellation", true); // defaults to true
constraints.add("audio", "googEchoCancellation2", settings.experimental.audioEchoCancellation2 && true); // defaults to false in Chrome
constraints.add("audio", "googAutoGainControl", true); // defaults to true
constraints.add("audio", "googAutoGainControl2", settings.experimental.audioAutoGainControl2 && true); // defaults to false in Chrome
constraints.add("audio", "googNoiseSuppression", true); // defaults to true
constraints.add("audio", "googgNoiseSuppression2", settings.experimental.audioNoiseSuppression2 && true); // defaults to false in Chrome
constraints.add("audio", "googHighpassFilter", true); // defaults to true
constraints.add("audio", "googTypingNoiseDetection", settings.experimental.audioTypingNoiseDetection && true); // defaults to true in Chrome
}
if ($scope.supported.renderToAssociatedSink) {
// When true uses the default communications device on Windows.
// https://codereview.chromium.org/155863003
constraints.add("audio", "googDucking", true); // defaults to true on Windows.
// Chrome will start rendering mediastream output to an output device that's associated with
// the input stream that was opened via getUserMedia.
// https://chromiumcodereview.appspot.com/23558010
constraints.add("audio", "chromeRenderToAssociatedSink", settings.audioRenderToAssociatedSkin && true); // defaults to false in Chrome
}
// Experimental video settings.
if (settings.experimental.enabled) {
// Changes the way the video encoding adapts to the available bandwidth.
// https://code.google.com/p/webrtc/issues/detail?id=3351
constraints.add(["video", "screensharing"], "googLeakyBucket", settings.experimental.videoLeakyBucket && true); // defaults to false in Chrome
// Removes the noise in the captured video stream at the expense of CPU.
constraints.add(["video", "screensharing"], "googNoiseReduction", settings.experimental.videoNoiseReduction && true); // defaults to false in Chrome
constraints.add("pc", "googCpuOveruseDetection", settings.experimental.videoCpuOveruseDetection && true); // defaults to true in Chrome
}
// Set video quality.
var videoQuality = videoQualityMap[settings.videoQuality];
if (videoQuality) {
_.forEach(videoQuality, function(v, k) {
constraints.add("video", k, v, true);
});
}
// Set max frame rate if any was selected.
if (settings.maxFrameRate && settings.maxFrameRate != "auto") {
constraints.add("video", "maxFrameRate", parseInt(settings.maxFrameRate, 10), true);
}
} else {
// Other browsers constraints (there are none as of now.);
}
});
}];
return { return {
scope: true, scope: true,
restrict: 'E', restrict: 'E',
replace: true, replace: true,
template: template, template: template,
controller: controller, controller: controller
link: link
}; };
}]; }];

22
static/js/directives/socialshare.js

@ -18,7 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
define(['jquery', 'text!partials/socialshare.html'], function($, template) {
"use strict";
define(['text!partials/socialshare.html'], function(template) {
var urls = { var urls = {
email: "mailto:?subject=_TEXT_%20_URL_", email: "mailto:?subject=_TEXT_%20_URL_",
@ -29,7 +31,7 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
}; };
// socialShare // socialShare
return ["$window", "translation", function($window, translation) { return ["$window", "translation", "rooms", "alertify", function($window, translation, rooms, alertify) {
var title = $window.encodeURIComponent($window.document.title); var title = $window.encodeURIComponent($window.document.title);
var makeUrl = function(nw, target) { var makeUrl = function(nw, target) {
@ -46,8 +48,15 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
template: template, template: template,
replace: true, replace: true,
link: function($scope, $element, $attr) { link: function($scope, $element, $attr) {
$element.on("click", "a", function(event) { $scope.$on("room.updated", function(ev, room) {
var nw = $(event.currentTarget).data("nw"); $scope.roomlink = rooms.link(room);
});
$scope.$on("room.left", function(ev) {
$scope.roomlink = null;
});
$element.find("a").on("click", function(event) {
event.preventDefault();
var nw = event.currentTarget.getAttribute("data-nw");
var url = makeUrl(nw, $scope.roomlink); var url = makeUrl(nw, $scope.roomlink);
if (url) { if (url) {
if (nw === "email") { if (nw === "email") {
@ -56,6 +65,11 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
} else { } 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");
} }
} else {
if (nw === "link") {
//$window.alert("Room link: " + $scope.roomlink);
alertify.dialog.notify(translation._("Room link"), '<a href="'+$scope.roomlink+'" rel="external" target="_blank">'+$scope.roomlink+'</a>');
}
} }
}); });
} }

2
static/js/directives/statusmessage.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['text!partials/statusmessage.html'], function(template) { define(['text!partials/statusmessage.html'], function(template) {
// statusMessage // statusMessage

50
static/js/directives/title.js

@ -0,0 +1,50 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
return [function() {
var link = function($scope, $element, attrs) {
var originalText = $element.text();
var updateTitle = function(roomName) {
if (roomName) {
$element.text(roomName+ " - " + originalText);
} else {
$element.text(originalText);
}
};
$scope.$on("room.updated", function(ev, room) {
updateTitle(room.Name);
});
$scope.$on("room.left", function(ev) {
updateTitle();
});
};
return {
restrict: 'E',
replace: false,
link: link
};
}];
});

40
static/js/directives/usability.js

@ -18,19 +18,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, template) { 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 [function() {
var controller = ['$scope', "mediaStream", "safeApply", "$timeout", "localStorage", "continueConnector", function($scope, mediaStream, safeApply, $timeout, localStorage, continueConnector) { var controller = ['$scope', "webrtc", "safeApply", "$timeout", "localStorage", "continueConnector", function($scope, webrtc, safeApply, $timeout, localStorage, continueConnector) {
var pending = true; var pending = true;
var complete = false; var complete = false;
var initializer = null;
var ctrl = this; var ctrl = this;
ctrl.setInfo = function(info) { ctrl.setInfo = function(info) {
$scope.usabilityInfo = info; $scope.usabilityInfo = info;
@ -46,16 +46,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
localStorage.setItem("mediastream-mediacheck", MEDIA_CHECK) localStorage.setItem("mediastream-mediacheck", MEDIA_CHECK)
console.log("Continue with connect after media check ..."); console.log("Continue with connect after media check ...");
continueDeferred.resolve(); continueDeferred.resolve();
if (mediaStream.config.DefaultRoomEnabled !== true) { ctrl.setInfo("ok");
ctrl.setInfo("initializing");
initializer = $timeout(function() {
ctrl.setInfo("ok");
$scope.layout.settings = false;
$scope.$emit("welcome");
}, 1000);
} else {
ctrl.setInfo("ok");
}
complete = true; complete = true;
} else { } else {
ctrl.setInfo("denied"); ctrl.setInfo("denied");
@ -70,7 +61,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
// NOTE(longsleep): Checkin for media access makes only sense on // NOTE(longsleep): Checkin for media access makes only sense on
// Chrome for now, as its the only one which remembers this // Chrome for now, as its the only one which remembers this
// decision permanently for https. // decision permanently for https.
mediaStream.webrtc.testMediaAccess($scope.continueConnect); webrtc.testMediaAccess($scope.continueConnect);
} else { } else {
$scope.continueConnect(true); $scope.continueConnect(true);
} }
@ -97,19 +88,14 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
} }
}); });
$scope.$on("room", function(event, room) { $scope.$on("room.joined", function(event) {
//console.log("roomStatus", room !== null ? true : false); if (complete) {
ctrl.setInfo("ok");
}
});
$scope.$on("room.left", function(event) {
if (complete) { if (complete) {
if (initializer !== null) {
$timeout.cancel(initializer);
initializer = null;
}
// Check if we should show settings per default when in a room.
if(room && !$scope.loadedUser) {
$scope.layout.settings = true;
} else {
$scope.layout.settings = false;
}
ctrl.setInfo("ok"); ctrl.setInfo("ok");
} }
}); });

92
static/js/directives/welcome.js

@ -0,0 +1,92 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// welcome
return ["rooms", "$timeout", "mediaStream", "translation", function(rooms, $timeout, mediaStream, translation) {
function link($scope, $element) {
//console.log("xxx welcome", $scope.$id, $element);
var placeHolder = translation._("Room name");
$scope.randomRoom = rooms.randomRoom;
$scope.canCreateRooms = rooms.canCreateRooms;
$scope.joinRoomByName = function(name) {
if ($scope.welcome.$invalid) {
return;
}
if (!name) {
return;
}
rooms.joinByName(name);
};
var roomdata = rooms.getRandomRoom();
var recreate = true;
if (roomdata) {
$scope.roomdata = {name: roomdata.name, placeholder: roomdata.name ? roomdata.name : placeHolder};
recreate = false;
} else {
$scope.roomdata = {placeholder: placeHolder};
}
$scope.roomdataInput = {
name: ""
};
$scope.$watch("roomdata.name", function(name) {
$scope.roomdata.link = rooms.link({Name: name});
}, true);
$scope.$watch("roomdataInput.name", function(name) {
if (name === "") {
if (recreate) {
$scope.randomRoom();
} else {
recreate = true;
}
} else {
$scope.roomdata.name = name;
}
});
$scope.$on("room.random", function(event, roomdata) {
$scope.roomdata = {name: roomdata.name, last: roomdata.name, placeholder: roomdata.name ? roomdata.name : placeHolder};
$scope.roomdataInput.name = "";
});
$timeout(function() {
$element.find(".roomdata-link-input:visible:enabled:first").focus();
});
}
return {
restrict: 'EA',
link: link
}
}];
});

31
static/js/directives/youtubevideo.js

@ -18,17 +18,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], function($, _, template, BigScreen) { define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], function($, _, template, BigScreen) {
return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", function($window, $document, mediaStream, alertify, translation, safeApply, appData) { return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", "$q", function($window, $document, mediaStream, alertify, translation, safeApply, appData, $q) {
var YOUTUBE_IFRAME_API_URL = "//www.youtube.com/iframe_api"; var YOUTUBE_IFRAME_API_URL = "//www.youtube.com/iframe_api";
var isYouTubeIframeAPIReady = $.Deferred(); var isYouTubeIframeAPIReady = (function() {
$window.onYouTubeIframeAPIReady = function() { var d = $q.defer();
console.log("YouTube IFrame ready"); $window.onYouTubeIframeAPIReady = function() {
isYouTubeIframeAPIReady.resolve(); console.log("YouTube IFrame ready");
}; d.resolve();
};
return d.promise;
})();
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
@ -68,7 +73,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
$scope.volumebarVisible = true; $scope.volumebarVisible = true;
$scope.volume = null; $scope.volume = null;
isYouTubeIframeAPIReady.done(function() { isYouTubeIframeAPIReady.then(function() {
$scope.$apply(function(scope) { $scope.$apply(function(scope) {
scope.youtubeAPIReady = true; scope.youtubeAPIReady = true;
}); });
@ -270,10 +275,10 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
playerReady = $.Deferred(); playerReady = $.Deferred();
} }
isYouTubeIframeAPIReady.done(function() { isYouTubeIframeAPIReady.then(function() {
if (!player) { if (!player) {
var origin = $window.location.protocol + "//" + $window.location.host; var origin = $window.location.protocol + "//" + $window.location.host;
player = new YT.Player("youtubeplayer", { player = new $window.YT.Player("youtubeplayer", {
height: "390", height: "390",
width: "640", width: "640",
playerVars: { playerVars: {
@ -563,11 +568,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
$scope.toggleFullscreen = function(elem) { $scope.toggleFullscreen = function(elem) {
if (BigScreen.enabled) { if (BigScreen.enabled) {
if (elem) { BigScreen.toggle(elem);
BigScreen.toggle(elem);
} else {
BigScreen.toggle(pane.get(0));
}
} }
}; };
@ -576,7 +577,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
var compile = function(tElement, tAttr) { var compile = function(tElement, tAttr) {
return function(scope, iElement, iAttrs, controller) { return function(scope, iElement, iAttrs, controller) {
$(iElement).on("dblclick", "#youtubecontainer", _.debounce(function(event) { $(iElement).find("#youtubecontainer").on("dblclick", _.debounce(function(event) {
scope.toggleFullscreen(event.delegateTarget); scope.toggleFullscreen(event.delegateTarget);
}, 100, true)); }, 100, true));
} }

2
static/js/filters/buddyimagesrc.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(["underscore"], function(_) { define(["underscore"], function(_) {
// Create URLs for blobs. // Create URLs for blobs.

2
static/js/filters/displayconference.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// displayConference // displayConference

2
static/js/filters/displayname.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// displayName // displayName

2
static/js/filters/displaynameforsession.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define(["jquery"], function($) { define(["jquery"], function($) {
// displayNameForSession // displayNameForSession

2
static/js/filters/displayuserid.js

@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
"use strict";
define([], function() { define([], function() {
// displayUserid // displayUserid

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save