Browse Source

Merge branch 'release-0.23'

pull/222/merge v0.23.6
Simon Eisenmann 11 years ago
parent
commit
f27cf89da1
  1. 4
      .hound.yml
  2. 8
      .travis.yml
  3. 3
      Makefile.am
  4. 123
      README.md
  5. 10
      configure.ac
  6. 14
      debian/changelog
  7. 4
      package.json
  8. 1
      server.conf.in
  9. 13
      src/app/spreed-webrtc-server/channelling_api.go
  10. 64
      src/app/spreed-webrtc-server/config.go
  11. 28
      src/i18n/messages-de.po
  12. 25
      src/i18n/messages-ja.po
  13. 25
      src/i18n/messages-ko.po
  14. 25
      src/i18n/messages-zh-cn.po
  15. 25
      src/i18n/messages-zh-tw.po
  16. 22
      src/i18n/messages.pot
  17. 4
      src/styles/Makefile.am
  18. 2
      src/styles/_shame.scss
  19. 3
      src/styles/components/_audiovideo.scss
  20. 4
      src/styles/components/_presentation.scss
  21. 44
      src/styles/global/_withs.scss
  22. 1
      src/styles/main.scss
  23. 199
      src/styles/scss.yml
  24. 2
      static/css/main.min.css
  25. 9
      static/js/controllers/mediastreamcontroller.js
  26. 3
      static/js/directives/buddylist.js
  27. 9
      static/js/directives/menu.js
  28. 6
      static/js/directives/settings.js
  29. 2
      static/js/directives/youtubevideo.js
  30. 10
      static/js/libs/require/text.js
  31. 164
      static/js/libs/webrtc.adapter.js
  32. 35
      static/js/mediastream/peercall.js
  33. 5
      static/js/mediastream/peerscreenshare.js
  34. 10
      static/js/mediastream/peerxfer.js
  35. 555
      static/js/mediastream/utils.js
  36. 25
      static/js/mediastream/webrtc.js
  37. 25
      static/js/services/contacts.js
  38. 38
      static/js/services/modules.js
  39. 9
      static/js/services/services.js
  40. 148
      static/js/services/videolayout.js
  41. 2
      static/partials/buddy.html
  42. 2
      static/partials/menu.html
  43. 2
      static/partials/presentation.html
  44. 50
      static/partials/settings.html
  45. 18
      static/partials/youtubevideo.html
  46. 2
      static/translation/messages-de.json
  47. 2
      static/translation/messages-ja.json
  48. 2
      static/translation/messages-ko.json
  49. 2
      static/translation/messages-zh-cn.json
  50. 2
      static/translation/messages-zh-tw.json

4
.hound.yml

@ -2,3 +2,7 @@ java_script:
enabled: true enabled: true
config_file: .jshint config_file: .jshint
ignore_file: .javascript_ignore ignore_file: .javascript_ignore
scss:
enabled: true
config_file: .scss.yml

8
.travis.yml

@ -9,16 +9,16 @@ go:
- tip - tip
env: env:
- GEM_HOME=/var/lib/gems/1.8 - GEM_HOME=/var/lib/gems/1.9.1
before_install: before_install:
- sudo add-apt-repository -y ppa:chris-lea/node.js - sudo add-apt-repository -y ppa:chris-lea/node.js
- sudo apt-get update - sudo apt-get update
install: install:
- sudo apt-get -y install nodejs rubygems python-babel - sudo apt-get -y install nodejs python-babel ruby1.9.1-dev
- sudo gem install oj - sudo gem1.9.1 install compass
- sudo gem install compass - sudo gem1.9.1 install scss-lint
- npm install - npm install
script: script:

3
Makefile.am

@ -95,6 +95,9 @@ styles: fonts
styleshint: styleshint:
cd $(CURDIR)/src/styles && $(MAKE) styleshint cd $(CURDIR)/src/styles && $(MAKE) styleshint
styleslint:
cd $(CURDIR)/src/styles && $(MAKE) styleslint
jshint: jshint:
@if [ "$(JSHINT)" = "" ]; then echo "Command 'jshint' not found"; exit 1; fi @if [ "$(JSHINT)" = "" ]; then echo "Command 'jshint' not found"; exit 1; fi
$(FIND) static/ -wholename static/js/libs -prune -o -name "*.js" -print0 | xargs -0 -n1 $(JSHINT) --config .jshint $(FIND) static/ -wholename static/js/libs -prune -o -name "*.js" -print0 | xargs -0 -n1 $(JSHINT) --config .jshint

123
README.md

@ -1,9 +1,11 @@
Spreed WebRTC Spreed WebRTC
=================== ===================
The latest version of Spreed WebRTC can be found on GitHub: Spreed WebRTC implements a WebRTC audio/video call and conferencing server
and web client.
https://github.com/strukturag/spreed-webrtc The latest version of Spreed WebRTC can be found on GitHub:
https://github.com/strukturag/spreed-webrtc
## Build prerequisites ## Build prerequisites
@ -16,119 +18,124 @@ The latest version of Spreed WebRTC can be found on GitHub:
## Runtime dependencies ## Runtime dependencies
Spreed WebRTC compiles directly to native code and has no Spreed WebRTC compiles directly to native code and has no
external runtime dependencies. See http://golang.org/doc/faq#How_is_the_run_time_support_implemented for details. external runtime dependencies. See [here](http://golang.org/doc/faq#How_is_the_run_time_support_implemented)
for details.
## Building ## Building
[![Build Status](https://travis-ci.org/strukturag/spreed-webrtc.png?branch=master)](https://travis-ci.org/strukturag/spreed-webrtc) [![Build Status](https://travis-ci.org/strukturag/spreed-webrtc.png?branch=master)](https://travis-ci.org/strukturag/spreed-webrtc)
If you got spreed-webrtc from the git repository, you will first need If you got spreed-webrtc from the git repository, you will first need
to run the included `autogen.sh` script to generate the `configure` to run the included `autogen.sh` script to generate the `configure`
script. script.
Configure does try to find all the tools on your system at the standard Configure does try to find all the tools on your system at the standard
locations. If the dependencies are somewhere else, add the corresponding locations. If the dependencies are somewhere else, add the corresponding
parameters to the ./configure call. parameters to the ./configure call.
```bash ```bash
$ ./configure $ ./configure
$ make $ make
``` ```
## Build seperately ## Build seperately
Get Go external dependencies first with ``make get``. Get Go external dependencies first with ``make get``.
```bash ```bash
$ make assets $ make assets
$ make binary $ make binary
``` ```
## Server startup ## Server startup
```bash ```bash
spreed-webrtc-server [OPTIONS] spreed-webrtc-server [OPTIONS]
``` ```
Options ### Options
-c="./server.conf": Configuration file. ```
-cpuprofile="": Write cpu profile to file. -c="./server.conf": Configuration file.
-h=false: Show this usage information and exit. -cpuprofile="": Write cpu profile to file.
-l="": Log file, defaults to stderr. -h=false: Show this usage information and exit.
-memprofile="": Write memory profile to this file. -l="": Log file, defaults to stderr.
-v=false: Display version number and exit. -memprofile="": Write memory profile to this file.
-v=false: Display version number and exit.
```
An example configuration file can be found in server.conf.in. An example configuration file can be found in server.conf.in.
## Usage ## Usage
Connect to the server URL and port with a web browser and the Connect to the server URL and port with a web browser and the
web client will launch. web client will launch.
## Development ## Development
To build styles and translations, further dependencies are required. To build styles and translations, further dependencies are required.
The source tree contains already built styles and translations, so The source tree contains already built styles and translations, so
these are only required if you want to make changes. these are only required if you want to make changes.
- [NodeJS](http://nodejs.org/) >= 0.10.0 - [NodeJS](http://nodejs.org/) >= 0.10.0
- [Compass](http://compass-style.org/) >= 1.0.0 - [Compass](http://compass-style.org/) >= 1.0.0
- [Sass](http://sass-lang.com/) >= 3.3.0 - [Sass](http://sass-lang.com/) >= 3.3.0
- [Babel](http://babel.pocoo.org/) - [Babel](http://babel.pocoo.org/)
The following Node.js modules are required, these may be installed The following Node.js modules are required, these may be installed
locally by running `npm install` from the project root. Consult the locally by running `npm install` from the project root. Consult the
`package.json` file for more details. `package.json` file for more details.
- [JSHint](http://www.jshint.com/) >= 2.0.0
- [autoprefixer](https://www.npmjs.org/package/autoprefixer) >= 1.1 - [autoprefixer](https://www.npmjs.org/package/autoprefixer) >= 1.1
- [po2json](https://github.com/mikeedwards/po2json) - [po2json](https://github.com/mikeedwards/po2json)
- [JSHint](http://www.jshint.com/) >= 2.0.0
- [scss-lint](https://github.com/causes/scss-lint) >= 0.33.0
Styles can be found in src/styles. Translations are found in src/i18n. Styles can be found in src/styles. Translations are found in src/i18n.
Each folder has its own Makefile to build the corresponding files. Each folder has its own Makefile to build the corresponding files. Check the
Makefile.am templates for available make targets.
## Running server for development ## Running server for development
Copy the server.conf.in to server.conf. Copy the server.conf.in to server.conf.
Build styles, javascript and binary using make. Then run Build styles, javascript and binary using make. Then run
``./spreed-webrtc-server`` ``./spreed-webrtc-server``
The server runs on http://localhost:8080/ per default. The server runs on http://localhost:8080/ per default.
HTML changes and Go rebuilds need a server restart. Javascript HTML changes and Go rebuilds need a server restart. Javascript
and CSS reload directly. and CSS reload directly.
## Running for production ## Running for production
Spreed WebRTC should be run through a SSL frontend proxy with Spreed WebRTC should be run through a SSL frontend proxy with
support for Websockets. Example configuration for Nginx can be support for Websockets. Example configuration for Nginx can be
found in `doc/NGINX.txt`. found in `doc/NGINX.txt`.
In addion for real work use one also needs a STUN/TURN server In addion for real work use one also needs a STUN/TURN server
configured with shared secret support. configured with shared secret support.
See https://code.google.com/p/rfc5766-turn-server/ for a free See https://code.google.com/p/rfc5766-turn-server/ for a free
open source TURN server implementation. Make sure to use a recent open source TURN server implementation. Make sure to use a recent
version (We recommend 3.2). Versions below 2.5 are not supported. version (We recommend 3.2). Versions below 2.5 are not supported.
## Contributing ## Contributing
1. "Fork". 1. "Fork" develop branch.
2. Make a feature branch. 2. Create a feature branch.
3. Make changes. 3. Make changes.
4. Do your commits (run ``make fmt`` and ``make jshint`` before commit). 4. Do your commits (run ``make fmt`` and ``make jshint`` before commit).
5. Send "pull request". 5. Send "pull request" for develop branch.
## License ## License

10
configure.ac

@ -37,6 +37,7 @@ GO_VERSION_MIN=1.1
NODEJS_VERSION_MIN=0.6.0 NODEJS_VERSION_MIN=0.6.0
NODEJS_VERSION_STYLES_MIN=0.10.0 NODEJS_VERSION_STYLES_MIN=0.10.0
SASS_VERSION_MIN=3.3.0 SASS_VERSION_MIN=3.3.0
SCSS_LINT_VERSION_MIN=0.33.0
AC_CONFIG_SRCDIR([src/app/spreed-webrtc-server/main.go]) AC_CONFIG_SRCDIR([src/app/spreed-webrtc-server/main.go])
AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_MACRO_DIR([m4])
@ -123,6 +124,15 @@ else
SASS_SUPPORT_STYLES=no],[SASS_SUPPORT_STYLES=yes]) SASS_SUPPORT_STYLES=no],[SASS_SUPPORT_STYLES=yes])
fi fi
AC_PATH_PROG([SCSS_LINT],scss-lint, [], [$PWD/node_modules/.bin$PATH_SEPARATOR$PATH])
if test x"${SCSS_LINT}" == x"" ; then
AC_MSG_WARN([Please install scss-lint to lint styles.])
else
AC_MSG_CHECKING([for version of scss-lint])
SCSS_LINT_VERSION=`$SCSS_LINT --version | $SED 's/^scss-lint //' | $SED 's/ .*//'`
AC_MSG_RESULT([$SCSS_LINT_VERSION])
fi
if test x"${SASS}" != x"" ; then if test x"${SASS}" != x"" ; then
AC_MSG_CHECKING([for compass support in sass]) AC_MSG_CHECKING([for compass support in sass])
tempfile=`mktemp -t XXXXXXblah` tempfile=`mktemp -t XXXXXXblah`

14
debian/changelog vendored

@ -1,3 +1,17 @@
spreed-webrtc-server (0.23.6) precise; urgency=low
* Fixed Youtube module.
* Contacts is now a module and can be disabled in server configuration.
* Fixed stereo send support.
* Improved Firefox support and added support for Firefox 36 and later.
* Dropped support for Chrome < 34.
* Account button in settings now use button style.
* Added support for scss-lint validation.
* Text.js was updated.
* CPU overuse detection (Chrome) is no longe experimental and now enabled by default.
-- Simon Eisenmann <simon@struktur.de> Fri, 20 Feb 2015 18:30:16 +0100
spreed-webrtc-server (0.23.5) precise; urgency=low spreed-webrtc-server (0.23.5) precise; urgency=low
* No longer install config file in install target of Makefile. We leave it to the packaging. * No longer install config file in install target of Makefile. We leave it to the packaging.

4
package.json

@ -1,8 +1,8 @@
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"jshint": ">= 2.5.5",
"autoprefixer": ">= 3.1.0", "autoprefixer": ">= 3.1.0",
"po2json": ">= 0.3.0" "po2json": ">= 0.3.0",
"jshint": ">= 2.5.5"
} }
} }

1
server.conf.in

@ -124,6 +124,7 @@ serverRealm = local
;screensharing = true ;screensharing = true
;youtube = true ;youtube = true
;presentation = true ;presentation = true
;contacts = true
[log] [log]
;logfile = /var/log/spreed-webrtc-server.log ;logfile = /var/log/spreed-webrtc-server.log

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

@ -139,6 +139,9 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
} }
} else { } else {
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil { if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil {
if !api.Config.WithModule("contacts") {
return
}
if err := api.contactrequestHandler(session, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest); err != nil { if err := api.contactrequestHandler(session, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest); err != nil {
log.Println("Ignoring invalid contact request.", err) log.Println("Ignoring invalid contact request.", err)
return return
@ -173,10 +176,14 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
var users []*DataSession var users []*DataSession
switch msg.Sessions.Sessions.Type { switch msg.Sessions.Sessions.Type {
case "contact": case "contact":
if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil { if api.Config.WithModule("contacts") {
users = api.GetUserSessions(session, userID) if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil {
users = api.GetUserSessions(session, userID)
} else {
log.Printf(err.Error())
}
} else { } else {
log.Printf(err.Error()) log.Printf("Incoming contacts session request with contacts disabled")
} }
case "session": case "session":
id, err := session.attestation.Decode(msg.Sessions.Sessions.Token) id, err := session.attestation.Decode(msg.Sessions.Sessions.Token)

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

@ -31,26 +31,27 @@ import (
) )
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
AuthorizeRoomCreation bool // Whether a user account is required to create rooms AuthorizeRoomCreation bool // Whether a user account is required to create rooms
AuthorizeRoomJoin bool // Whether a user account is required to join rooms AuthorizeRoomJoin bool // Whether a user account is required to join rooms
Modules []string // List of enabled modules Modules []string // List of enabled modules
globalRoomID string // Id of the global room (not exported to Javascript) modulesTable map[string]bool // Map of enabled modules
contentSecurityPolicy string // HTML content security policy globalRoomID string // Id of the global room (not exported to Javascript)
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode contentSecurityPolicy string // HTML content security policy
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode
} }
func NewConfig(container phoenix.Container, tokens bool) *Config { func NewConfig(container phoenix.Container, tokens bool) *Config {
@ -88,11 +89,18 @@ func NewConfig(container phoenix.Container, tokens bool) *Config {
trimAndRemoveDuplicates(&turnURIs) trimAndRemoveDuplicates(&turnURIs)
// Get enabled modules. // Get enabled modules.
allModules := []string{"screensharing", "youtube", "presentation"} modulesTable := map[string]bool{
modules := allModules[:0] "screensharing": true,
for _, module := range allModules { "youtube": true,
"presentation": true,
"contacts": true,
}
modules := []string{}
for module, _ := range modulesTable {
if container.GetBoolDefault("modules", module, true) { if container.GetBoolDefault("modules", module, true) {
modules = append(modules, module) modules = append(modules, module)
} else {
modulesTable[module] = false
} }
} }
log.Println("Enabled modules:", modules) log.Println("Enabled modules:", modules)
@ -115,6 +123,7 @@ func NewConfig(container phoenix.Container, tokens bool) *Config {
AuthorizeRoomCreation: container.GetBoolDefault("app", "authorizeRoomCreation", false), AuthorizeRoomCreation: container.GetBoolDefault("app", "authorizeRoomCreation", false),
AuthorizeRoomJoin: container.GetBoolDefault("app", "authorizeRoomJoin", false), AuthorizeRoomJoin: container.GetBoolDefault("app", "authorizeRoomJoin", false),
Modules: modules, Modules: modules,
modulesTable: modulesTable,
globalRoomID: container.GetStringDefault("app", "globalRoom", ""), globalRoomID: container.GetStringDefault("app", "globalRoom", ""),
contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""), contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""),
contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""), contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""),
@ -125,6 +134,15 @@ 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"}}
} }
func (config *Config) WithModule(m string) bool {
if val, ok := config.modulesTable[m]; ok && val {
return true
}
return false
}
// Helper function to clean up string arrays. // Helper function to clean up string arrays.
func trimAndRemoveDuplicates(data *[]string) { func trimAndRemoveDuplicates(data *[]string) {
found := make(map[string]bool) found := make(map[string]bool)

28
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: 2015-01-28 15:06+0100\n" "POT-Creation-Date: 2015-02-18 14:46+0100\n"
"PO-Revision-Date: 2015-01-28 15:07+0100\n" "PO-Revision-Date: 2015-02-18 14:49+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"
@ -347,9 +347,6 @@ msgstr "Aktiviert"
msgid "Advanced settings" msgid "Advanced settings"
msgstr "Erweiterte Einstellungen" msgstr "Erweiterte Einstellungen"
msgid "Stereo audio"
msgstr "Stereo-Audio"
msgid "Play audio on same device as selected microphone" msgid "Play audio on same device as selected microphone"
msgstr "Audioausgabe auf dem zum Mikrofon gehörenden Gerät" msgstr "Audioausgabe auf dem zum Mikrofon gehörenden Gerät"
@ -368,15 +365,28 @@ msgstr "Max. Bildwiederholrate"
msgid "auto" msgid "auto"
msgstr "auto" msgstr "auto"
msgid "Send stereo audio"
msgstr "Audio in Stereo übertragen"
msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr ""
"Um Stereo zu übertragen wird die Echo-Unterdrückung deaktiviert. Nur "
"aktivieren wenn das Eingangssignal Stereo ist."
msgid "Detect CPU over use"
msgstr "CPU-Überlast erkennen"
msgid "Automatically reduces video quality as needed."
msgstr "Reduziert die Videoqualität wenn nötig."
msgid "Optimize for high resolution video" msgid "Optimize for high resolution video"
msgstr "Für hohe Auflösung optimieren" msgstr "Für hohe Auflösung optimieren"
msgid "Reduce video noise" msgid "Reduce video noise"
msgstr "Rauschen reduzieren" msgstr "Rauschen reduzieren"
msgid "Detect CPU over use"
msgstr "CPU-Überlast erkennen"
msgid "Enable experiments" msgid "Enable experiments"
msgstr "Experimente aktivieren" msgstr "Experimente aktivieren"
@ -503,7 +513,7 @@ msgstr "Raum-Verlauf"
msgid "Please sign in." msgid "Please sign in."
msgstr "Bitte melden Sie sich an." msgstr "Bitte melden Sie sich an."
msgid "Videos are shared with everyone in this call." msgid "Videos play simultaneously for everyone in this call."
msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt." msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt."
msgid "YouTube URL" msgid "YouTube URL"

25
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: 2015-01-28 15:06+0100\n" "POT-Creation-Date: 2015-02-18 14:46+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"
@ -343,9 +343,6 @@ msgstr "許可"
msgid "Advanced settings" msgid "Advanced settings"
msgstr "詳細設定" msgstr "詳細設定"
msgid "Stereo audio"
msgstr "ステレオ・オーディオ"
msgid "Play audio on same device as selected microphone" msgid "Play audio on same device as selected microphone"
msgstr "" msgstr ""
@ -364,15 +361,27 @@ msgstr "ビデオ最高フレームレート"
msgid "auto" msgid "auto"
msgstr "自動" msgstr "自動"
msgid "Optimize for high resolution video" #, fuzzy
msgstr "" msgid "Send stereo audio"
msgstr "ステレオ・オーディオ"
msgid "Reduce video noise" msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr "" msgstr ""
msgid "Detect CPU over use" msgid "Detect CPU over use"
msgstr "" msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments" msgid "Enable experiments"
msgstr "" msgstr ""
@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in." msgid "Please sign in."
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos play simultaneously for everyone in this call."
msgstr "" msgstr ""
msgid "YouTube URL" msgid "YouTube URL"

25
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: 2015-01-28 15:06+0100\n" "POT-Creation-Date: 2015-02-18 14:46+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"
@ -343,9 +343,6 @@ msgstr "허락됨"
msgid "Advanced settings" msgid "Advanced settings"
msgstr "고급 설정" msgstr "고급 설정"
msgid "Stereo audio"
msgstr "스테레오 음성"
msgid "Play audio on same device as selected microphone" msgid "Play audio on same device as selected microphone"
msgstr "" msgstr ""
@ -364,15 +361,27 @@ msgstr "비디오프레임 비율 최대화"
msgid "auto" msgid "auto"
msgstr "자동" msgstr "자동"
msgid "Optimize for high resolution video" #, fuzzy
msgstr "" msgid "Send stereo audio"
msgstr "스테레오 음성"
msgid "Reduce video noise" msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr "" msgstr ""
msgid "Detect CPU over use" msgid "Detect CPU over use"
msgstr "" msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments" msgid "Enable experiments"
msgstr "" msgstr ""
@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in." msgid "Please sign in."
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos play simultaneously for everyone in this call."
msgstr "" msgstr ""
msgid "YouTube URL" msgid "YouTube URL"

25
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: 2015-01-28 15:06+0100\n" "POT-Creation-Date: 2015-02-18 14:46+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"
@ -343,9 +343,6 @@ msgstr "启用"
msgid "Advanced settings" msgid "Advanced settings"
msgstr "高级设置" msgstr "高级设置"
msgid "Stereo audio"
msgstr "立体声"
msgid "Play audio on same device as selected microphone" msgid "Play audio on same device as selected microphone"
msgstr "" msgstr ""
@ -364,15 +361,27 @@ msgstr "最大视频帧速率"
msgid "auto" msgid "auto"
msgstr "自动" msgstr "自动"
msgid "Optimize for high resolution video" #, fuzzy
msgstr "" msgid "Send stereo audio"
msgstr "立体声"
msgid "Reduce video noise" msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr "" msgstr ""
msgid "Detect CPU over use" msgid "Detect CPU over use"
msgstr "" msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments" msgid "Enable experiments"
msgstr "" msgstr ""
@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in." msgid "Please sign in."
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos play simultaneously for everyone in this call."
msgstr "" msgstr ""
msgid "YouTube URL" msgid "YouTube URL"

25
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: 2015-01-28 15:06+0100\n" "POT-Creation-Date: 2015-02-18 14:46+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"
@ -343,9 +343,6 @@ msgstr "啟用"
msgid "Advanced settings" msgid "Advanced settings"
msgstr "高級設置" msgstr "高級設置"
msgid "Stereo audio"
msgstr "立體聲"
msgid "Play audio on same device as selected microphone" msgid "Play audio on same device as selected microphone"
msgstr "" msgstr ""
@ -364,15 +361,27 @@ msgstr "最大視頻幀速率"
msgid "auto" msgid "auto"
msgstr "自動" msgstr "自動"
msgid "Optimize for high resolution video" #, fuzzy
msgstr "" msgid "Send stereo audio"
msgstr "立體聲"
msgid "Reduce video noise" msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr "" msgstr ""
msgid "Detect CPU over use" msgid "Detect CPU over use"
msgstr "" msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments" msgid "Enable experiments"
msgstr "" msgstr ""
@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in." msgid "Please sign in."
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos play simultaneously for everyone in this call."
msgstr "" msgstr ""
msgid "YouTube URL" msgid "YouTube URL"

22
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: 2015-01-28 15:06+0100\n" "POT-Creation-Date: 2015-02-18 14:46+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"
@ -339,9 +339,6 @@ msgstr ""
msgid "Advanced settings" msgid "Advanced settings"
msgstr "" msgstr ""
msgid "Stereo audio"
msgstr ""
msgid "Play audio on same device as selected microphone" msgid "Play audio on same device as selected microphone"
msgstr "" msgstr ""
@ -360,15 +357,26 @@ msgstr ""
msgid "auto" msgid "auto"
msgstr "" msgstr ""
msgid "Optimize for high resolution video" msgid "Send stereo audio"
msgstr "" msgstr ""
msgid "Reduce video noise" msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr "" msgstr ""
msgid "Detect CPU over use" msgid "Detect CPU over use"
msgstr "" msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments" msgid "Enable experiments"
msgstr "" msgstr ""
@ -489,7 +497,7 @@ msgstr ""
msgid "Please sign in." msgid "Please sign in."
msgstr "" msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos play simultaneously for everyone in this call."
msgstr "" msgstr ""
msgid "YouTube URL" msgid "YouTube URL"

4
src/styles/Makefile.am

@ -43,3 +43,7 @@ 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
@if [ "$(SASS_SUPPORT_STYLES)" = "no" ]; then echo "Your version of sass does not support checking styles"; exit 1; fi @if [ "$(SASS_SUPPORT_STYLES)" = "no" ]; then echo "Your version of sass does not support checking styles"; exit 1; fi
$(FIND) ./ -maxdepth 1 -name "*.scss" -print0 | xargs -0 -n1 $(SASS) --compass --scss $(SASSFLAGS) -c $(FIND) ./ -maxdepth 1 -name "*.scss" -print0 | xargs -0 -n1 $(SASS) --compass --scss $(SASSFLAGS) -c
styleslint:
@if [ "$(SCSS_LINT)" = "" ]; then echo "Command 'scss-lint' not found, required when linting styles"; exit 1; fi
$(SCSS_LINT) -c scss.yml

2
src/styles/_shame.scss

@ -19,6 +19,8 @@
* *
*/ */
// scss-lint:disable IdSelector
// Remove boostrap 3 modal scroll bar. // Remove boostrap 3 modal scroll bar.
.modal { .modal {
overflow-y: auto; overflow-y: auto;

3
src/styles/components/_audiovideo.scss

@ -97,7 +97,7 @@
z-index: 2; z-index: 2;
} }
video { video {
object-fit: contain; object-fit: cover;
} }
} }
@ -133,6 +133,7 @@
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
height: 100%; height: 100%;
width: 100%;
} }
.localVideo { .localVideo {
background: $video-background; background: $video-background;

4
src/styles/components/_presentation.scss

@ -35,10 +35,6 @@
white-space: normal; white-space: normal;
} }
.presentationpane .welcome div {
text-align: center;
}
.presentationpane .welcome button { .presentationpane .welcome button {
margin-top: 30px; margin-top: 30px;
} }

44
src/styles/global/_withs.scss

@ -0,0 +1,44 @@
/*
* 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/>.
*
*/
.visible-with-contacts,
.visible-with-contacts-inline {
display: none;
}
.hidden-with-contacts {
}
.with-contacts {
.visible-with-contacts {
display: block;
}
.visible-with-contacts-inline {
display: inline-block;
}
.hidden-with-contacts {
display: none;
}
}

1
src/styles/main.scss

@ -33,6 +33,7 @@
@import "global/nicescroll"; @import "global/nicescroll";
@import "global/animations"; @import "global/animations";
@import "global/overlaybar"; @import "global/overlaybar";
@import "global/withs";
@import "components/rightslide"; @import "components/rightslide";
@import "components/bar"; @import "components/bar";

199
src/styles/scss.yml

@ -0,0 +1,199 @@
scss_files: "**/*.scss"
exclude: "libs/**"
linters:
BangFormat:
enabled: true
space_before_bang: true
space_after_bang: false
BorderZero:
enabled: true
convention: zero # or `none`
ColorKeyword:
enabled: true
ColorVariable:
enabled: true
Comment:
enabled: false
DebugStatement:
enabled: true
DeclarationOrder:
enabled: true
DuplicateProperty:
enabled: true
ElsePlacement:
enabled: true
style: same_line # or 'new_line'
EmptyLineBetweenBlocks:
enabled: true
ignore_single_line_blocks: true
EmptyRule:
enabled: false
FinalNewline:
enabled: true
present: true
HexLength:
enabled: true
style: short # or 'long'
HexNotation:
enabled: true
style: lowercase # or 'uppercase'
HexValidation:
enabled: true
IdSelector:
enabled: true
ImportantRule:
enabled: true
ImportPath:
enabled: true
leading_underscore: false
filename_extension: false
Indentation:
enabled: true
allow_non_nested_indentation: false
character: space # or 'tab'
width: 2
LeadingZero:
enabled: true
style: exclude_zero # or 'include_zero'
MergeableSelector:
enabled: true
force_nesting: true
NameFormat:
enabled: true
allow_leading_underscore: true
convention: hyphenated_lowercase # or 'BEM', or a regex pattern
NestingDepth:
enabled: true
max_depth: 3
PlaceholderInExtend:
enabled: true
PropertyCount:
enabled: false
include_nested: false
max_properties: 10
PropertySortOrder:
enabled: true
ignore_unspecified: false
separate_groups: false
PropertySpelling:
enabled: true
extra_properties: []
QualifyingElement:
enabled: true
allow_element_with_attribute: false
allow_element_with_class: false
allow_element_with_id: false
SelectorDepth:
enabled: true
max_depth: 3
SelectorFormat:
enabled: true
convention: hyphenated_lowercase # or 'BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern
Shorthand:
enabled: true
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
SingleLinePerSelector:
enabled: true
SpaceAfterComma:
enabled: true
SpaceAfterPropertyColon:
enabled: true
style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned'
SpaceAfterPropertyName:
enabled: true
SpaceBeforeBrace:
enabled: true
style: space # or 'new_line'
allow_single_line_padding: false
SpaceBetweenParens:
enabled: true
spaces: 0
StringQuotes:
enabled: true
style: single_quotes # or double_quotes
TrailingSemicolon:
enabled: true
TrailingZero:
enabled: false
UnnecessaryMantissa:
enabled: true
UnnecessaryParentReference:
enabled: true
UrlFormat:
enabled: true
UrlQuotes:
enabled: true
VariableForProperty:
enabled: false
properties: []
VendorPrefixes:
enabled: true
identifier_list: base
include: []
exclude: []
ZeroUnit:
enabled: true
Compass::*:
enabled: true
Compass::PropertyWithMixin:
ignore:
- 'inline-block'
- 'box-shadow'
- 'opacity'
- 'border-radius'
- 'transform'
- 'text-shadow'
- 'background-clip'

2
static/css/main.min.css vendored

File diff suppressed because one or more lines are too long

9
static/js/controllers/mediastreamcontroller.js

@ -140,11 +140,12 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder
message: null, message: null,
settings: { settings: {
videoQuality: "high", videoQuality: "high",
stereo: true, sendStereo: false,
maxFrameRate: 20, maxFrameRate: 20,
defaultRoom: "", defaultRoom: "",
language: "", language: "",
audioRenderToAssociatedSkin: true, audioRenderToAssociatedSkin: true,
videoCpuOveruseDetection: true,
experimental: { experimental: {
enabled: false, enabled: false,
audioEchoCancellation2: true, audioEchoCancellation2: true,
@ -152,8 +153,7 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder
audioNoiseSuppression2: true, audioNoiseSuppression2: true,
audioTypingNoiseDetection: true, audioTypingNoiseDetection: true,
videoLeakyBucket: true, videoLeakyBucket: true,
videoNoiseReduction: false, videoNoiseReduction: false
videoCpuOveruseDetection: true
} }
} }
}; };
@ -226,9 +226,6 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder
} }
mediaStream.webrtc.settings.pcConfig.iceServers = iceServers; mediaStream.webrtc.settings.pcConfig.iceServers = iceServers;
// Stereo.
mediaStream.webrtc.settings.stereo = settings.stereo;
// Refresh constraints. // Refresh constraints.
constraints.refresh($scope.master.settings); constraints.refresh($scope.master.settings);

3
static/js/directives/buddylist.js

@ -141,6 +141,9 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
} }
scope.$apply(); scope.$apply();
}); });
if (contacts.enabled) {
iElement.addClass("with-contacts");
}
}; };

9
static/js/directives/menu.js

@ -23,15 +23,10 @@
define(['text!partials/menu.html'], function(template) { define(['text!partials/menu.html'], function(template) {
// menu // menu
return ["mediaStream", function(mediaStream) { return ["modules", function(modules) {
var link = function($scope, $element) { var link = function($scope, $element) {
$scope.withModule = modules.withModule;
$scope.modules = mediaStream.config.Modules || [];
$scope.withModule = function(m) {
return $scope.modules.indexOf(m) !== -1;
};
}; };
return { return {

6
static/js/directives/settings.js

@ -248,6 +248,12 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
constraints.add("video", "maxFrameRate", parseInt(settings.maxFrameRate, 10), true); constraints.add("video", "maxFrameRate", parseInt(settings.maxFrameRate, 10), true);
} }
// Disable AEC if stereo.
// https://github.com/webrtc/apprtc/issues/23
if (settings.sendStereo) {
constraints.add("audio", "echoCancellation", false);
}
} else { } else {
// Other browsers constraints (there are none as of now.); // Other browsers constraints (there are none as of now.);

2
static/js/directives/youtubevideo.js

@ -74,7 +74,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
$scope.volume = null; $scope.volume = null;
isYouTubeIframeAPIReady.then(function() { isYouTubeIframeAPIReady.then(function() {
$scope.$apply(function(scope) { safeApply($scope, function(scope) {
scope.youtubeAPIReady = true; scope.youtubeAPIReady = true;
}); });
}); });

10
static/js/libs/require/text.js

@ -1,5 +1,5 @@
/** /**
* @license RequireJS text 2.0.12 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. * @license RequireJS text 2.0.13 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license. * Available via the MIT or new BSD license.
* see: http://github.com/requirejs/text for details * see: http://github.com/requirejs/text for details
*/ */
@ -23,7 +23,7 @@ define(['module'], function (module) {
masterConfig = (module.config && module.config()) || {}; masterConfig = (module.config && module.config()) || {};
text = { text = {
version: '2.0.12', version: '2.0.13',
strip: function (content) { strip: function (content) {
//Strips <?xml ...?> declarations so that external SVG and XML //Strips <?xml ...?> declarations so that external SVG and XML
@ -85,13 +85,13 @@ define(['module'], function (module) {
parseName: function (name) { parseName: function (name) {
var modName, ext, temp, var modName, ext, temp,
strip = false, strip = false,
index = name.indexOf("."), index = name.lastIndexOf("."),
isRelative = name.indexOf('./') === 0 || isRelative = name.indexOf('./') === 0 ||
name.indexOf('../') === 0; name.indexOf('../') === 0;
if (index !== -1 && (!isRelative || index > 1)) { if (index !== -1 && (!isRelative || index > 1)) {
modName = name.substring(0, index); modName = name.substring(0, index);
ext = name.substring(index + 1, name.length); ext = name.substring(index + 1);
} else { } else {
modName = name; modName = name;
} }
@ -252,7 +252,7 @@ define(['module'], function (module) {
try { try {
var file = fs.readFileSync(url, 'utf8'); var file = fs.readFileSync(url, 'utf8');
//Remove BOM (Byte Mark Order) from utf8 files if it is there. //Remove BOM (Byte Mark Order) from utf8 files if it is there.
if (file.indexOf('\uFEFF') === 0) { if (file[0] === '\uFEFF') {
file = file.substring(1); file = file.substring(1);
} }
callback(file); callback(file);

164
static/js/libs/webrtc.adapter.js

@ -37,117 +37,103 @@ var reattachMediaStream = null;
var webrtcDetectedBrowser = null; var webrtcDetectedBrowser = null;
var webrtcDetectedVersion = null; var webrtcDetectedVersion = null;
function maybeFixConfiguration(pcConfig) {
if (!pcConfig) {
return;
}
for (var i = 0; i < pcConfig.iceServers.length; i++) {
if (pcConfig.iceServers[i].hasOwnProperty('urls')){
pcConfig.iceServers[i]['url'] = pcConfig.iceServers[i]['urls'];
delete pcConfig.iceServers[i]['urls'];
}
}
}
if (navigator.mozGetUserMedia) { if (navigator.mozGetUserMedia) {
console.log("This appears to be Firefox"); console.log('This appears to be Firefox');
webrtcDetectedBrowser = "firefox"; webrtcDetectedBrowser = 'firefox';
webrtcDetectedVersion = webrtcDetectedVersion =
parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
// The RTCPeerConnection object. // The RTCPeerConnection object.
var RTCPeerConnection = function(pcConfig, pcConstraints) { window.RTCPeerConnection = function(pcConfig, pcConstraints) {
// .urls is not supported in FF yet. // .urls is not supported in FF yet.
maybeFixConfiguration(pcConfig); if (pcConfig && pcConfig.iceServers) {
for (var i = 0; i < pcConfig.iceServers.length; i++) {
if (pcConfig.iceServers[i].hasOwnProperty('urls')) {
pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls;
delete pcConfig.iceServers[i].urls;
}
}
}
return new mozRTCPeerConnection(pcConfig, pcConstraints); return new mozRTCPeerConnection(pcConfig, pcConstraints);
} };
// The RTCSessionDescription object. // The RTCSessionDescription object.
RTCSessionDescription = mozRTCSessionDescription; window.RTCSessionDescription = mozRTCSessionDescription;
// The RTCIceCandidate object. // The RTCIceCandidate object.
RTCIceCandidate = mozRTCIceCandidate; window.RTCIceCandidate = mozRTCIceCandidate;
// Get UserMedia (only difference is the prefix). // getUserMedia shim (only difference is the prefix).
// Code from Adam Barth. // Code from Adam Barth.
getUserMedia = navigator.mozGetUserMedia.bind(navigator); getUserMedia = navigator.mozGetUserMedia.bind(navigator);
navigator.getUserMedia = getUserMedia; navigator.getUserMedia = getUserMedia;
// Creates iceServer from the url for FF. // Creates ICE server from the URL for FF.
createIceServer = function(url, username, password) { window.createIceServer = function(url, username, password) {
var iceServer = null; var iceServer = null;
var url_parts = url.split(':'); var urlParts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) { if (urlParts[0].indexOf('stun') === 0) {
// Create iceServer with stun url. // Create ICE server with STUN URL.
iceServer = { 'url': url }; iceServer = {
} else if (url_parts[0].indexOf('turn') === 0) { 'url': url
};
} else if (urlParts[0].indexOf('turn') === 0) {
if (webrtcDetectedVersion < 27) { if (webrtcDetectedVersion < 27) {
// Create iceServer with turn url. // Create iceServer with turn url.
// Ignore the transport parameter from TURN url for FF version <=27. // Ignore the transport parameter from TURN url for FF version <=27.
var turn_url_parts = url.split("?"); var turnUrlParts = url.split('?');
// Return null for createIceServer if transport=tcp. // Return null for createIceServer if transport=tcp.
if (turn_url_parts.length === 1 || if (turnUrlParts.length === 1 ||
turn_url_parts[1].indexOf('transport=udp') === 0) { turnUrlParts[1].indexOf('transport=udp') === 0) {
iceServer = {'url': turn_url_parts[0], iceServer = {
'credential': password, 'url': turnUrlParts[0],
'username': username}; 'credential': password,
'username': username
};
} }
} else { } else {
// FF 27 and above supports transport parameters in TURN url, // FF 27 and above supports transport parameters in TURN url,
// So passing in the full url to create iceServer. // So passing in the full url to create iceServer.
iceServer = {'url': url, iceServer = {
'credential': password, 'url': url,
'username': username}; 'credential': password,
'username': username
};
} }
} }
return iceServer; return iceServer;
}; };
createIceServers = function(urls, username, password) { window.createIceServers = function(urls, username, password) {
var iceServers = []; var iceServers = [];
// Use .url for FireFox. // Use .url for FireFox.
for (i = 0; i < urls.length; i++) { for (var i = 0; i < urls.length; i++) {
var iceServer = createIceServer(urls[i], var iceServer =
username, window.createIceServer(urls[i], username, password);
password);
if (iceServer !== null) { if (iceServer !== null) {
iceServers.push(iceServer); iceServers.push(iceServer);
} }
} }
return iceServers; return iceServers;
} };
// Attach a media stream to an element. // Attach a media stream to an element.
attachMediaStream = function(element, stream) { attachMediaStream = function(element, stream) {
console.log("Attaching media stream"); console.log('Attaching media stream');
element.mozSrcObject = stream; element.mozSrcObject = stream;
element.play();
}; };
reattachMediaStream = function(to, from) { reattachMediaStream = function(to, from) {
console.log("Reattaching media stream"); console.log('Reattaching media stream');
to.mozSrcObject = from.mozSrcObject; to.mozSrcObject = from.mozSrcObject;
to.play();
}; };
// Fake get{Video,Audio}Tracks
if (!MediaStream.prototype.getVideoTracks) {
MediaStream.prototype.getVideoTracks = function() {
return [];
};
}
if (!MediaStream.prototype.getAudioTracks) {
MediaStream.prototype.getAudioTracks = function() {
return [];
};
}
} else if (navigator.webkitGetUserMedia) { } else if (navigator.webkitGetUserMedia) {
console.log("This appears to be Chrome"); console.log('This appears to be Chrome');
webrtcDetectedBrowser = "chrome"; webrtcDetectedBrowser = 'chrome';
// Temporary fix until crbug/374263 is fixed. // Temporary fix until crbug/374263 is fixed.
// Setting Chrome version to 999, if version is unavailable. // Setting Chrome version to 999, if version is unavailable.
var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
@ -158,50 +144,38 @@ if (navigator.mozGetUserMedia) {
} }
// Creates iceServer from the url for Chrome M33 and earlier. // Creates iceServer from the url for Chrome M33 and earlier.
createIceServer = function(url, username, password) { window.createIceServer = function(url, username, password) {
var iceServer = null; var iceServer = null;
var url_parts = url.split(':'); var urlParts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) { if (urlParts[0].indexOf('stun') === 0) {
// Create iceServer with stun url. // Create iceServer with stun url.
iceServer = { 'url': url }; iceServer = {
} else if (url_parts[0].indexOf('turn') === 0) { 'url': url
};
} else if (urlParts[0].indexOf('turn') === 0) {
// Chrome M28 & above uses below TURN format. // Chrome M28 & above uses below TURN format.
iceServer = {'url': url, iceServer = {
'credential': password, 'url': url,
'username': username}; 'credential': password,
'username': username
};
} }
return iceServer; return iceServer;
}; };
// Creates iceServers from the urls for Chrome M34 and above. // Creates an ICEServer object from multiple URLs.
createIceServers = function(urls, username, password) { window.createIceServers = function(urls, username, password) {
var iceServers = []; return [{
if (webrtcDetectedVersion >= 34) { 'urls': urls,
// .urls is supported since Chrome M34. 'credential': password,
iceServers = [{'urls': urls, 'username': username
'credential': password, }];
'username': username }];
} else {
for (i = 0; i < urls.length; i++) {
var iceServer = createIceServer(urls[i],
username,
password);
if (iceServer !== null) {
iceServers.push(iceServer);
}
}
}
return iceServers;
}; };
// The RTCPeerConnection object. // The RTCPeerConnection object.
var RTCPeerConnection = function(pcConfig, pcConstraints) { RTCPeerConnection = function(pcConfig, pcConstraints) {
// .urls is supported since Chrome M34.
if (webrtcDetectedVersion < 34) {
maybeFixConfiguration(pcConfig);
}
return new webkitRTCPeerConnection(pcConfig, pcConstraints); return new webkitRTCPeerConnection(pcConfig, pcConstraints);
} };
// Get UserMedia (only difference is the prefix). // Get UserMedia (only difference is the prefix).
// Code from Adam Barth. // Code from Adam Barth.
@ -225,5 +199,5 @@ if (navigator.mozGetUserMedia) {
to.src = from.src; to.src = from.src;
}; };
} else { } else {
console.log("Browser does not appear to be WebRTC-capable"); console.log('Browser does not appear to be WebRTC-capable');
} }

35
static/js/mediastream/peercall.js

@ -36,6 +36,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
this.pcConstraints = $.extend(true, {}, this.webrtc.settings.pcConstraints); this.pcConstraints = $.extend(true, {}, this.webrtc.settings.pcConstraints);
this.sdpConstraints = $.extend(true, {}, this.webrtc.settings.sdpConstraints); this.sdpConstraints = $.extend(true, {}, this.webrtc.settings.sdpConstraints);
this.offerConstraints = $.extend(true, {}, this.webrtc.settings.offerConstraints); this.offerConstraints = $.extend(true, {}, this.webrtc.settings.offerConstraints);
this.sdpParams = $.extend(true, {}, this.webrtc.settings.sdpParams);
this.peerconnection = null; this.peerconnection = null;
this.datachannels = {}; this.datachannels = {};
@ -84,8 +85,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
PeerCall.prototype.onCreateAnswerOffer = function(cb, sessionDescription) { PeerCall.prototype.onCreateAnswerOffer = function(cb, sessionDescription) {
// Prefer Opus. this.setLocalSdp(sessionDescription);
sessionDescription.sdp = utils.preferOpus(sessionDescription.sdp);
// Convert to object to allow custom property injection. // Convert to object to allow custom property injection.
var sessionDescriptionObj = sessionDescription; var sessionDescriptionObj = sessionDescription;
@ -128,6 +128,9 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
console.log("Got a remote description but not connected -> ignored."); console.log("Got a remote description but not connected -> ignored.");
return; return;
} }
this.setRemoteSdp(sessionDescription);
peerconnection.setRemoteDescription(sessionDescription, _.bind(function() { peerconnection.setRemoteDescription(sessionDescription, _.bind(function() {
console.log("Set remote session description.", sessionDescription, this); console.log("Set remote session description.", sessionDescription, this);
if (cb) { if (cb) {
@ -155,6 +158,28 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
}; };
PeerCall.prototype.setLocalSdp = function(sessionDescription) {
var params = this.sdpParams;
sessionDescription.sdp = utils.maybePreferAudioReceiveCodec(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybePreferVideoReceiveCodec(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybeSetAudioReceiveBitRate(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybeSetVideoReceiveBitRate(sessionDescription.sdp, params);
};
PeerCall.prototype.setRemoteSdp = function(sessionDescription) {
var params = this.sdpParams;
sessionDescription.sdp = utils.maybeSetOpusOptions(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybePreferAudioSendCodec(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybePreferVideoSendCodec(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybeSetAudioSendBitRate(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybeSetVideoSendBitRate(sessionDescription.sdp, params);
sessionDescription.sdp = utils.maybeSetVideoSendInitialBitRate(sessionDescription.sdp, params);
};
PeerCall.prototype.onIceCandidate = function(event) { PeerCall.prototype.onIceCandidate = function(event) {
if (event.candidate) { if (event.candidate) {
//console.log("ice candidate", event.candidate); //console.log("ice candidate", event.candidate);
@ -218,7 +243,11 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
// Avoid errors when still receiving candidates but closed. // Avoid errors when still receiving candidates but closed.
return; return;
} }
this.peerconnection.addIceCandidate(candidate); this.peerconnection.addIceCandidate(candidate, function() {
//console.log("Remote candidate added successfully.", candidate);
}, function(error) {
console.warn("Failed to add remote candidate:", error, candidate);
});
}; };

5
static/js/mediastream/peerscreenshare.js

@ -56,7 +56,10 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens'], f
// SCTP is supported from Chrome M31. // SCTP is supported from Chrome M31.
// No need to pass DTLS constraint as it is on by default in 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. // For SCTP, reliable and ordered is true by default.
this.pcConstraints = {}; this.pcConstraints = {
mandatory: {},
optional: []
};
// Inject token into sessiondescription and ice candidate data. // Inject token into sessiondescription and ice candidate data.
this.e.on("sessiondescription icecandidate", _.bind(function(event, data) { this.e.on("sessiondescription icecandidate", _.bind(function(event, data) {

10
static/js/mediastream/peerxfer.js

@ -48,11 +48,17 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens', 'w
audio: false, audio: false,
video: false video: false
}; };
this.sdpConstraints = {}; this.sdpConstraints = {
mandatory: {},
optional: []
};
// SCTP is supported from Chrome M31. // SCTP is supported from Chrome M31.
// No need to pass DTLS constraint as it is on by default in 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. // For SCTP, reliable and ordered is true by default.
this.pcConstraints = {}; this.pcConstraints = {
mandatory: {},
optional: []
};
// Inject token into sessiondescription and ice candidate data. // Inject token into sessiondescription and ice candidate data.
this.e.on("sessiondescription icecandidate", _.bind(function(event, data) { this.e.on("sessiondescription icecandidate", _.bind(function(event, data) {

555
static/js/mediastream/utils.js

@ -1,162 +1,423 @@
/* /*
* Spreed WebRTC. * Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG * Copyright (C) 2013-2015 struktur AG
* *
* This file is part of Spreed WebRTC. * This file is part of Spreed WebRTC.
* *
* This program is free software: you can redistribute it and/or modify * This file is a AMD wrapped version of the sdputils.js from the
* it under the terms of the GNU Affero General Public License as published by * WebRTC apprtc example. https://github.com/webrtc/apprtc/blob/master/src
* 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, * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
* 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 * Redistribution and use in source and binary forms, with or without
* along with this program. If not, see <http://www.gnu.org/licenses/>. * modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* *
*/ */
"use strict"; "use strict";
define([], function() { define([], function() {
var Utils = function() {} function trace(text) {
// noop
Utils.prototype.mergeConstraints = function(cons1, cons2) { }
var merged = cons1;
var name; function mergeConstraints(cons1, cons2) {
for (name in cons2.mandatory) { if (!cons1 || !cons2) {
if (cons2.mandatory.hasOwnProperty(name)) { return cons1 || cons2;
merged.mandatory[name] = cons2.mandatory[name]; }
} var merged = cons1;
} for (var name in cons2.mandatory) {
merged.optional.concat(cons2.optional); if (cons2.mandatory.hasOwnProperty(name)) {
return merged; merged.mandatory[name] = cons2.mandatory[name];
}; }
}
Utils.prototype.extractSdp = function(sdpLine, pattern) { merged.optional = merged.optional.concat(cons2.optional);
var result = sdpLine.match(pattern); return merged;
return (result && result.length == 2) ? result[1] : null; }
};
function iceCandidateType(candidateStr) {
Utils.prototype.addStereo = function(sdp) { return candidateStr.split(' ')[7];
// Set Opus in Stereo. }
var sdpLines = sdp.split('\r\n'); function maybeSetOpusOptions(sdp, params) {
var opusPayload = ""; // Set Opus in Stereo, if stereo is true, unset it, if stereo is false, and
var fmtpLineIndex = null; // do nothing if otherwise.
var i; if (params.opusStereo === 'true') {
sdp = setCodecParam(sdp, 'opus/48000', 'stereo', '1');
// Find opus payload. } else if (params.opusStereo === 'false') {
for (i = 0; i < sdpLines.length; i++) { sdp = removeCodecParam(sdp, 'opus/48000', 'stereo');
if (sdpLines[i].search('opus/48000') !== -1) { }
opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
break; // Set Opus FEC, if opusfec is true, unset it, if opusfec is false, and
} // do nothing if otherwise.
} if (params.opusFec === 'true') {
sdp = setCodecParam(sdp, 'opus/48000', 'useinbandfec', '1');
// Find the payload in fmtp line. } else if (params.opusFec === 'false') {
for (i = 0; i < sdpLines.length; i++) { sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec');
if (sdpLines[i].search('a=fmtp') !== -1) { }
var payload = this.extractSdp(sdpLines[i], /a=fmtp:(\d+)/);
if (payload === opusPayload) { // Set Opus maxplaybackrate, if requested.
fmtpLineIndex = i; if (params.opusMaxPbr) {
break; sdp = setCodecParam(
} sdp, 'opus/48000', 'maxplaybackrate', params.opusMaxPbr);
} }
} return sdp;
// No fmtp line found. }
if (fmtpLineIndex === null) {
console.log("Unable to add stereo (no fmtp line for opus payload)", opusPayload); function maybeSetAudioSendBitRate(sdp, params) {
return sdp; if (!params.audioSendBitrate) {
} return sdp;
}
// Append stereo=1 to fmtp line. trace('Prefer audio send bitrate: ' + params.audioSendBitrate);
sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1'); return preferBitRate(sdp, params.audioSendBitrate, 'audio');
}
sdp = sdpLines.join('\r\n');
console.log("Enabled opus stereo."); function maybeSetAudioReceiveBitRate(sdp, params) {
return sdp; if (!params.audioRecvBitrate) {
return sdp;
}; }
trace('Prefer audio receive bitrate: ' + params.audioRecvBitrate);
Utils.prototype.preferOpus = function(sdp) { return preferBitRate(sdp, params.audioRecvBitrate, 'audio');
// Set Opus as the preferred codec in SDP if Opus is present. }
var sdpLines = sdp.split('\r\n'); function maybeSetVideoSendBitRate(sdp, params) {
var mLineIndex = null; if (!params.videoSendBitrate) {
var i; return sdp;
}
// Search for m line. trace('Prefer video send bitrate: ' + params.videoSendBitrate);
for (i = 0; i < sdpLines.length; i++) { return preferBitRate(sdp, params.videoSendBitrate, 'video');
if (sdpLines[i].search('m=audio') !== -1) { }
mLineIndex = i;
break; function maybeSetVideoReceiveBitRate(sdp, params) {
} if (!params.videoRecvBitrate) {
} return sdp;
if (mLineIndex === null) { }
return sdp; trace('Prefer video receive bitrate: ' + params.videoRecvBitrate);
} return preferBitRate(sdp, params.videoRecvBitrate, 'video');
}
// If Opus is available, set it as the default in m line.
for (i = 0; i < sdpLines.length; i++) { // Add a b=AS:bitrate line to the m=mediaType section.
if (sdpLines[i].search('opus/48000') !== -1) { function preferBitRate(sdp, bitrate, mediaType) {
var opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); var sdpLines = sdp.split('\r\n');
if (opusPayload) {
sdpLines[mLineIndex] = this.setDefaultCodec(sdpLines[mLineIndex], opusPayload); // Find m line for the given mediaType.
} var mLineIndex = findLine(sdpLines, 'm=', mediaType);
break; if (mLineIndex === null) {
} trace('Failed to add bandwidth line to sdp, as no m-line found');
} return sdp;
}
// Remove CN in m line and sdp.
sdpLines = this.removeCN(sdpLines, mLineIndex); // Find next m-line if any.
var nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, 'm=');
sdp = sdpLines.join('\r\n'); if (nextMLineIndex === null) {
return sdp; nextMLineIndex = sdpLines.length;
}
};
// Find c-line corresponding to the m-line.
Utils.prototype.setDefaultCodec = function(mLine, payload) { var cLineIndex = findLineInRange(sdpLines, mLineIndex + 1,
// Set the selected codec to the first in m line. nextMLineIndex, 'c=');
var elements = mLine.split(' '); if (cLineIndex === null) {
var newLine = []; trace('Failed to add bandwidth line to sdp, as no c-line found');
var index = 0; return sdp;
for (var i = 0; i < elements.length; i++) { }
// Format of media starts from the fourth.
if (index === 3) { // Check if bandwidth line already exists between c-line and next m-line.
newLine[index++] = payload; // Put target payload to the first. var bLineIndex = findLineInRange(sdpLines, cLineIndex + 1,
} nextMLineIndex, 'b=AS');
if (elements[i] !== payload) { if (bLineIndex) {
newLine[index++] = elements[i]; sdpLines.splice(bLineIndex, 1);
} }
}
return newLine.join(' '); // Create the b (bandwidth) sdp line.
}; var bwLine = 'b=AS:' + bitrate;
// As per RFC 4566, the b line should follow after c-line.
Utils.prototype.removeCN = function(sdpLines, mLineIndex) { sdpLines.splice(cLineIndex + 1, 0, bwLine);
// Strip CN from sdp before CN constraints is ready. sdp = sdpLines.join('\r\n');
var mLineElements = sdpLines[mLineIndex].split(' '); return sdp;
// Scan from end for the convenience of removing an item. }
for (var i = sdpLines.length - 1; i >= 0; i--) {
var payload = this.extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); // Add an a=fmtp: x-google-min-bitrate=kbps line, if videoSendInitialBitrate
if (payload) { // is specified. We'll also add a x-google-min-bitrate value, since the max
var cnPos = mLineElements.indexOf(payload); // must be >= the min.
if (cnPos !== -1) { function maybeSetVideoSendInitialBitRate(sdp, params) {
// Remove CN payload from m line. var initialBitrate = params.videoSendInitialBitrate;
mLineElements.splice(cnPos, 1); if (!initialBitrate) {
} return sdp;
// Remove CN line in sdp }
sdpLines.splice(i, 1);
} // Validate the initial bitrate value.
} var maxBitrate = initialBitrate;
sdpLines[mLineIndex] = mLineElements.join(' '); var bitrate = params.videoSendBitrate;
return sdpLines; if (bitrate) {
}; if (initialBitrate > bitrate) {
trace('Clamping initial bitrate to max bitrate of ' +
var utils = new Utils(); bitrate + ' kbps.');
return utils; initialBitrate = bitrate;
params.videoSendInitialBitrate = initialBitrate;
}
maxBitrate = bitrate;
}
var sdpLines = sdp.split('\r\n');
// Search for m line.
var mLineIndex = findLine(sdpLines, 'm=', 'video');
if (mLineIndex === null) {
trace('Failed to find video m-line');
return sdp;
}
sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-min-bitrate',
params.videoSendInitialBitrate.toString());
sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-max-bitrate',
maxBitrate.toString());
return sdp;
}
// Promotes |audioSendCodec| to be the first in the m=audio line, if set.
function maybePreferAudioSendCodec(sdp, params) {
return maybePreferCodec(sdp, 'audio', 'send', params.audioSendCodec);
}
// Promotes |audioRecvCodec| to be the first in the m=audio line, if set.
function maybePreferAudioReceiveCodec(sdp, params) {
return maybePreferCodec(sdp, 'audio', 'receive', params.audioRecvCodec);
}
// Promotes |videoSendCodec| to be the first in the m=audio line, if set.
function maybePreferVideoSendCodec(sdp, params) {
return maybePreferCodec(sdp, 'video', 'send', params.videoSendCodec);
}
// Promotes |videoRecvCodec| to be the first in the m=audio line, if set.
function maybePreferVideoReceiveCodec(sdp, params) {
return maybePreferCodec(sdp, 'video', 'receive', params.videoRecvCodec);
}
// Sets |codec| as the default |type| codec if it's present.
// The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
function maybePreferCodec(sdp, type, dir, codec) {
var str = type + ' ' + dir + ' codec';
if (codec === '') {
trace('No preference on ' + str + '.');
return sdp;
}
trace('Prefer ' + str + ': ' + codec);
var sdpLines = sdp.split('\r\n');
// Search for m line.
var mLineIndex = findLine(sdpLines, 'm=', type);
if (mLineIndex === null) {
return sdp;
}
// If the codec is available, set it as the default in m line.
var payload = getCodecPayloadType(sdpLines, codec);
if (payload) {
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
}
sdp = sdpLines.join('\r\n');
return sdp;
}
// Set fmtp param to specific codec in SDP. If param does not exists, add it.
function setCodecParam(sdp, codec, param, value) {
var sdpLines = sdp.split('\r\n');
var fmtpLineIndex = findFmtpLine(sdpLines, codec);
var fmtpObj = {};
if (fmtpLineIndex === null) {
var index = findLine(sdpLines, 'a=rtpmap', codec);
if (index === null) {
return sdp;
}
var payload = getCodecPayloadTypeFromLine(sdpLines[index]);
fmtpObj.pt = payload.toString();
fmtpObj.params = {};
fmtpObj.params[param] = value;
sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj));
} else {
fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]);
fmtpObj.params[param] = value;
sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj);
}
sdp = sdpLines.join('\r\n');
return sdp;
}
// Remove fmtp param if it exists.
function removeCodecParam(sdp, codec, param) {
var sdpLines = sdp.split('\r\n');
var fmtpLineIndex = findFmtpLine(sdpLines, codec);
if (fmtpLineIndex === null) {
return sdp;
}
var map = parseFmtpLine(sdpLines[fmtpLineIndex]);
delete map.params[param];
var newLine = writeFmtpLine(map);
if (newLine === null) {
sdpLines.splice(fmtpLineIndex, 1);
} else {
sdpLines[fmtpLineIndex] = newLine;
}
sdp = sdpLines.join('\r\n');
return sdp;
}
// Split an fmtp line into an object including 'pt' and 'params'.
function parseFmtpLine(fmtpLine) {
var fmtpObj = {};
var spacePos = fmtpLine.indexOf(' ');
var keyValues = fmtpLine.substring(spacePos + 1).split('; ');
var pattern = new RegExp('a=fmtp:(\\d+)');
var result = fmtpLine.match(pattern);
if (result && result.length === 2) {
fmtpObj.pt = result[1];
} else {
return null;
}
var params = {};
for (var i = 0; i < keyValues.length; ++i) {
var pair = keyValues[i].split('=');
if (pair.length === 2) {
params[pair[0]] = pair[1];
}
}
fmtpObj.params = params;
return fmtpObj;
}
// Generate an fmtp line from an object including 'pt' and 'params'.
function writeFmtpLine(fmtpObj) {
if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) {
return null;
}
var pt = fmtpObj.pt;
var params = fmtpObj.params;
var keyValues = [];
var i = 0;
for (var key in params) {
if (params.hasOwnProperty(key)) {
keyValues[i] = key + '=' + params[key];
++i;
}
}
if (i === 0) {
return null;
}
return 'a=fmtp:' + pt.toString() + ' ' + keyValues.join('; ');
}
// Find fmtp attribute for |codec| in |sdpLines|.
function findFmtpLine(sdpLines, codec) {
// Find payload of codec.
var payload = getCodecPayloadType(sdpLines, codec);
// Find the payload in fmtp line.
return payload ? findLine(sdpLines, 'a=fmtp:' + payload.toString()) : null;
}
// Find the line in sdpLines that starts with |prefix|, and, if specified,
// contains |substr| (case-insensitive search).
function findLine(sdpLines, prefix, substr) {
return findLineInRange(sdpLines, 0, -1, prefix, substr);
}
// Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
// and, if specified, contains |substr| (case-insensitive search).
function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
for (var i = startLine; i < realEndLine; ++i) {
if (sdpLines[i].indexOf(prefix) === 0) {
if (!substr ||
sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
return i;
}
}
}
return null;
}
// Gets the codec payload type from sdp lines.
function getCodecPayloadType(sdpLines, codec) {
var index = findLine(sdpLines, 'a=rtpmap', codec);
return index ? getCodecPayloadTypeFromLine(sdpLines[index]) : null;
}
// Gets the codec payload type from an a=rtpmap:X line.
function getCodecPayloadTypeFromLine(sdpLine) {
var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
var result = sdpLine.match(pattern);
return (result && result.length === 2) ? result[1] : null;
}
// Returns a new m= line with the specified codec as the first one.
function setDefaultCodec(mLine, payload) {
var elements = mLine.split(' ');
// Just copy the first three parameters; codec order starts on fourth.
var newLine = elements.slice(0, 3);
// Put target payload first and copy in the rest.
newLine.push(payload);
for (var i = 3; i < elements.length; i++) {
if (elements[i] !== payload) {
newLine.push(elements[i]);
}
}
return newLine.join(' ');
}
// Exported utils.
return {
mergeConstraints: mergeConstraints,
maybeSetOpusOptions: maybeSetOpusOptions,
maybeSetAudioSendBitRate: maybeSetAudioSendBitRate,
maybeSetAudioReceiveBitRate: maybeSetAudioReceiveBitRate,
maybeSetVideoSendBitRate: maybeSetVideoSendBitRate,
maybeSetVideoReceiveBitRate: maybeSetVideoReceiveBitRate,
maybeSetVideoSendInitialBitRate: maybeSetVideoSendInitialBitRate,
maybePreferAudioSendCodec: maybePreferAudioSendCodec,
maybePreferAudioReceiveCodec: maybePreferAudioReceiveCodec,
maybePreferVideoSendCodec: maybePreferVideoSendCodec,
maybePreferVideoReceiveCodec: maybePreferVideoReceiveCodec
}
}); });

25
static/js/mediastream/webrtc.js

@ -59,7 +59,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Settings.are cloned into peer call on call creation. // Settings.are cloned into peer call on call creation.
this.settings = { this.settings = {
stereo: false,
mediaConstraints: { mediaConstraints: {
audio: true, audio: true,
video: { video: {
@ -76,6 +75,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
}] }]
}, },
pcConstraints: { pcConstraints: {
mandatory: {},
optional: [] optional: []
}, },
// Set up audio and video regardless of what devices are present. // Set up audio and video regardless of what devices are present.
@ -83,7 +83,8 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
mandatory: { mandatory: {
OfferToReceiveAudio: true, OfferToReceiveAudio: true,
OfferToReceiveVideo: true OfferToReceiveVideo: true
} },
optional: []
}, },
offerConstraints: { offerConstraints: {
mandatory: {}, mandatory: {},
@ -97,6 +98,20 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
mandatory: {} mandatory: {}
} }
} }
},
// sdpParams values need to be strings.
sdpParams: {
//audioSendBitrate: ,
audioSendCodec: "opus/48000",
//audioRecvBitrate: ,
//audioRecvCodec: ,
//opusMaxPbr: ,
opusStereo: "true",
//videoSendBitrate: ,
//videoSendInitialBitrate: ,
videoSendCodec: "VP8/90000"
//videoRecvBitrate: ,
//videoRecvCodec
} }
} }
@ -228,9 +243,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
switch (type) { switch (type) {
case "Offer": case "Offer":
console.log("Offer process."); console.log("Offer process.");
if (this.settings.stereo) {
data.sdp = utils.addStereo(data.sdp);
}
targetcall = this.findTargetCall(from); targetcall = this.findTargetCall(from);
if (targetcall) { if (targetcall) {
// Hey we know this call. // Hey we know this call.
@ -280,9 +292,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
return; return;
} }
console.log("Answer process."); console.log("Answer process.");
if (this.settings.stereo) {
data.sdp = utils.addStereo(data.sdp);
}
// TODO(longsleep): In case of negotiation this could switch offer and answer // TODO(longsleep): In case of negotiation this could switch offer and answer
// and result in a offer sdp sent as answer data. We need to handle this. // and result in a offer sdp sent as answer data. We need to handle this.
targetcall.setRemoteDescription(new window.RTCSessionDescription(data), function() { targetcall.setRemoteDescription(new window.RTCSessionDescription(data), function() {

25
static/js/services/contacts.js

@ -123,11 +123,15 @@ define(['underscore', 'jquery', 'modernizr', 'sjcl', 'text!partials/contactsmana
}; };
// contacts // contacts
return ["appData", "contactData", "mediaStream", "$templateCache", function(appData, contactData, mediaStream, $templateCache) { return ["appData", "contactData", "mediaStream", "$templateCache", "modules", function(appData, contactData, mediaStream, $templateCache, modules) {
// Inject our templates. var withContacts = modules.withModule("contacts");
$templateCache.put('/contactsmanager/main.html', templateContactsManager);
$templateCache.put('/contactsmanager/edit.html', templateContactsManagerEdit); if (withContacts) {
// Inject our templates.
$templateCache.put('/contactsmanager/main.html', templateContactsManager);
$templateCache.put('/contactsmanager/edit.html', templateContactsManagerEdit);
}
var Contacts = function() { var Contacts = function() {
@ -135,6 +139,7 @@ define(['underscore', 'jquery', 'modernizr', 'sjcl', 'text!partials/contactsmana
this.userid = null; this.userid = null;
this.key = null; this.key = null;
this.database = null; this.database = null;
this.enabled = withContacts;
appData.e.on("authenticationChanged", _.bind(function(event, userid, suserid) { appData.e.on("authenticationChanged", _.bind(function(event, userid, suserid) {
// TODO(longsleep): Avoid creating empty databases. Create db on store only. // TODO(longsleep): Avoid creating empty databases. Create db on store only.
@ -160,14 +165,24 @@ define(['underscore', 'jquery', 'modernizr', 'sjcl', 'text!partials/contactsmana
}; };
Contacts.prototype.put = function(contact) { Contacts.prototype.put = function(contact) {
if (!this.database) {
console.warn("Unable to put contact as no database is loaded.");
return;
}
this.database.put("contacts", { this.database.put("contacts", {
id: this.id(contact.Userid), id: this.id(contact.Userid),
contact: this.encrypt(contact) contact: this.encrypt(contact)
}); });
}
};
Contacts.prototype.open = function(userid, suserid) { Contacts.prototype.open = function(userid, suserid) {
if (!this.enabled) {
return null;
}
if (this.database && (!userid || this.userid !== userid)) { if (this.database && (!userid || this.userid !== userid)) {
// Unload existing contacts. // Unload existing contacts.
this.unload(); this.unload();

38
static/js/services/modules.js

@ -0,0 +1,38 @@
/*
* 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() {
// modules
return ["mediaStream", function(mediaStream) {
var enabledModules = mediaStream.config.Modules || [];
// Public api.
return {
withModule: function(m) {
return enabledModules.indexOf(m) !== -1;
}
}
}];
});

9
static/js/services/services.js

@ -65,7 +65,8 @@ define([
'services/rooms', 'services/rooms',
'services/resturl', 'services/resturl',
'services/roompin', 'services/roompin',
'services/constraints'], function(_, 'services/constraints',
'services/modules'], function(_,
desktopNotify, desktopNotify,
playSound, playSound,
safeApply, safeApply,
@ -108,7 +109,8 @@ localStatus,
rooms, rooms,
restURL, restURL,
roompin, roompin,
constraints) { constraints,
modules) {
var services = { var services = {
desktopNotify: desktopNotify, desktopNotify: desktopNotify,
@ -153,7 +155,8 @@ constraints) {
rooms: rooms, rooms: rooms,
restURL: restURL, restURL: restURL,
roompin: roompin, roompin: roompin,
constraints: constraints constraints: constraints,
modules: modules
}; };
var initialize = function(angModule) { var initialize = function(angModule) {

148
static/js/services/videolayout.js

@ -22,13 +22,17 @@
"use strict"; "use strict";
define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modernizr) { define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modernizr) {
var dynamicCSSContainer = "audiovideo-dynamic";
var renderers = {}; var renderers = {};
var defaultSize = {
width: 640,
height: 360
};
var defaultAspectRatio = defaultSize.width/defaultSize.height;
var getRemoteVideoSize = function(videos, streams) { var getRemoteVideoSize = function(videos, streams) {
var size = { var size = {
width: 1920, width: defaultSize.width,
height: 1080 height: defaultSize.height
} }
if (videos.length) { if (videos.length) {
if (videos.length === 1) { if (videos.length === 1) {
@ -41,7 +45,16 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
} }
} }
return size; return size;
} };
var dynamicCSSContainer = "audiovideo-dynamic";
var injectCSS = function(css) {
$.injectCSS(css, {
containerName: dynamicCSSContainer,
truncateFirst: true,
useRawValues: true
});
};
var objectFitSupport = Modernizr["object-fit"] && true; var objectFitSupport = Modernizr["object-fit"] && true;
@ -83,80 +96,65 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
} }
if (!videoWidth) { if (!videoWidth) {
videoWidth = 640; videoWidth = defaultSize.width;
} }
if (!videoHeight) { if (!videoHeight) {
videoHeight = 360; videoHeight = defaultSize.height;
} }
if (this.countSelfAsRemote) { if (this.countSelfAsRemote) {
videos.unshift(null); videos.unshift(null);
} }
var aspectRatio = videoWidth / videoHeight;
var innerHeight = size.height; var innerHeight = size.height;
var innerWidth = size.width; var innerWidth = size.width;
// We use the same aspect ratio to make all videos look the same.
var aspectRatio = defaultAspectRatio;
//console.log("resize", innerHeight, innerWidth); //console.log("resize", innerHeight, innerWidth);
//console.log("resize", container, videos.length, aspectRatio, innerHeight, innerWidth); //console.log("resize", container, videos.length, aspectRatio, innerHeight, innerWidth);
var extraCSS = {}; var extraCSS = {};
if (!objectFitSupport) { // Always set size of mini video.
// Make mini video fit into available space on browsers with no object-fit support. extraCSS[".renderer-"+this.name+" .miniVideo"] = {
// http://caniuse.com/object-fit width: ($(scope.mini).height() * defaultAspectRatio) + "px"
var aspectRatioLocal = scope.localVideo.videoWidth / scope.localVideo.videoHeight; };
extraCSS = {};
extraCSS[".renderer-"+this.name+" .miniVideo"] = { var space = innerHeight * innerWidth; // square pixels
width: ($(scope.mini).height() * aspectRatioLocal) + "px" var videoSpace = space / videos.length;
}; var singleVideoWidthOptimal = Math.pow(videoSpace * aspectRatio, 0.5);
var videosPerRow = Math.ceil(innerWidth / singleVideoWidthOptimal);
if (videosPerRow > videos.length) {
videosPerRow = videos.length;
} }
var singleVideoWidth = Math.ceil(innerWidth / videosPerRow);
if (videos.length === 1) { var singleVideoHeight = Math.ceil(singleVideoWidth / aspectRatio);
var newVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight; var newContainerWidth = (videosPerRow * singleVideoWidth);
var newVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio; var newContainerHeight = Math.ceil(videos.length / videosPerRow) * singleVideoHeight;
container.style.width = newVideoWidth + 'px'; if (newContainerHeight > innerHeight) {
container.style.left = ((innerWidth - newVideoWidth) / 2) + 'px'; var tooHigh = (newContainerHeight - innerHeight) / Math.ceil(videos.length / videosPerRow);
} else { singleVideoHeight -= tooHigh;
var space = innerHeight * innerWidth; // square pixels singleVideoWidth = singleVideoHeight * aspectRatio;
var videoSpace = space / videos.length;
var singleVideoWidthOptimal = Math.pow(videoSpace * aspectRatio, 0.5);
var videosPerRow = Math.ceil(innerWidth / singleVideoWidthOptimal);
if (videosPerRow > videos.length) {
videosPerRow = videos.length;
}
var singleVideoWidth = Math.ceil(innerWidth / videosPerRow);
var singleVideoHeight = Math.ceil(singleVideoWidth / aspectRatio);
var newContainerWidth = (videosPerRow * singleVideoWidth);
var newContainerHeight = Math.ceil(videos.length / videosPerRow) * singleVideoHeight;
if (newContainerHeight > innerHeight) {
var tooHigh = (newContainerHeight - innerHeight) / Math.ceil(videos.length / videosPerRow);
singleVideoHeight -= tooHigh;
singleVideoWidth = singleVideoHeight * aspectRatio;
}
/*
console.log("space", space);
console.log("videospace", videoSpace);
console.log("singleVideoWidthOptimal", singleVideoWidthOptimal);
console.log("videosPerRow", videosPerRow);
console.log("singleVideoWidth", singleVideoWidth);
console.log("singleVideoHeight", singleVideoHeight);
*/
container.style.width = newContainerWidth + "px";
container.style.left = ((innerWidth - newContainerWidth) / 2) + 'px';
var extraCSS2 = {};
extraCSS2[".renderer-"+this.name+" .remoteVideos"] = {
">div": {
width: singleVideoWidth + "px",
height: singleVideoHeight + "px"
}
};
extraCSS = $.extend(extraCSS, extraCSS2);
} }
$.injectCSS(extraCSS, { /*
truncateFirst: true, console.log("space", space);
containerName: dynamicCSSContainer, console.log("videospace", videoSpace);
useRawValues: true console.log("singleVideoWidthOptimal", singleVideoWidthOptimal);
}); console.log("videosPerRow", videosPerRow);
console.log("singleVideoWidth", singleVideoWidth);
console.log("singleVideoHeight", singleVideoHeight);
*/
container.style.width = newContainerWidth + "px";
container.style.left = ((innerWidth - newContainerWidth) / 2) + 'px';
extraCSS[".renderer-"+this.name+" .remoteVideos"] = {
">div": {
width: singleVideoWidth + "px",
height: singleVideoHeight + "px"
}
};
injectCSS(extraCSS);
}; };
@ -255,33 +253,32 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
} }
var remoteSize = getRemoteVideoSize(videos, streams);
var aspectRatio = remoteSize.width / remoteSize.height;
var innerHeight = size.height - 110; var innerHeight = size.height - 110;
var innerWidth = size.width; var innerWidth = size.width;
var extraCSS = {}; var extraCSS = {};
// Use the same aspect ratio for all videos.
var aspectRatio = defaultAspectRatio;
var bigVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight; var bigVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight;
var bigVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio; var bigVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio;
this.bigVideo.style.width = bigVideoWidth + 'px';
this.bigVideo.style.height = bigVideoHeight + 'px';
// Make space for own video on the right if width goes low. // Make space for own video on the right if width goes low.
if (((size.width - (videos.length - 1) * 192) / 2) < 192) { if (((size.width - (videos.length - 1) * 192) / 2) < 192) {
extraCSS = {};
extraCSS[".renderer-"+this.name+" .remoteVideos"] = { extraCSS[".renderer-"+this.name+" .remoteVideos"] = {
"margin-right": "192px", "margin-right": "192px",
"overflow-x": "auto", "overflow-x": "auto",
"overflow-y": "hidden" "overflow-y": "hidden"
}; };
} }
// Big video size.
extraCSS[".renderer-"+this.name+" .bigVideo .remoteVideo"] = {
"height": bigVideoHeight + 'px',
"width": bigVideoWidth + 'px',
"margin": "auto",
"display": "block"
};
$.injectCSS(extraCSS, { injectCSS(extraCSS);
truncateFirst: true,
containerName: dynamicCSSContainer,
useRawValues: true
});
}; };
@ -320,8 +317,13 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
this.makeBig(streams[videos[0]].element); this.makeBig(streams[videos[0]].element);
this.bigVideo.style.opacity = 1; this.bigVideo.style.opacity = 1;
} }
} }
var extraCSS = {};
// Always set size of mini video.
extraCSS[".renderer-"+this.name+" .miniVideo"] = {
width: ($(scope.mini).height() * defaultAspectRatio) + "px"
};
injectCSS(extraCSS);
}; };
// Register renderers. // Register renderers.

2
static/partials/buddy.html

@ -1,5 +1,5 @@
<div class="buddy" ng-class="{'contact': contact, 'withSubline': display.subline || session.Userid, 'isself': session.Userid === myuserid}"> <div class="buddy" ng-class="{'contact': contact, 'withSubline': display.subline || session.Userid, 'isself': session.Userid === myuserid}">
<div class="buddyPicture"><i class="fa fa-user"/><img ng-show="display.buddyPicture" alt ng-src="{{display.buddyPicture}}"/></div> <div class="buddyPicture"><i class="fa fa-user"/><img ng-show="display.buddyPicture" alt ng-src="{{display.buddyPicture}}"/></div>
<div class="buddy1">{{session.Id|displayName}}</div> <div class="buddy1">{{session.Id|displayName}}</div>
<div class="buddy2"><span ng-show="session.Userid"><i class="fa contact" data-action="contact"></i><span ng-show="session.count"> ({{session.count}})</span></span> <span title="{{display.sublineFull}}">{{display.subline}}</span></div> <div class="buddy2"><span ng-show="session.Userid"><i class="fa contact visible-with-contacts-inline" data-action="contact"></i><span ng-show="session.count"> ({{session.count}})</span></span> <span title="{{display.sublineFull}}">{{display.subline}}</span></div>
</div> </div>

2
static/partials/menu.html

@ -3,7 +3,7 @@
<button ng-if="withModule('presentation')" 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 ng-if="withModule('presentation')" 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 ng-if="withModule('screensharing')" 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 ng-if="withModule('screensharing')" 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-sitemap"></i></button> <button ng-if="withModule('contacts')" 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>

2
static/partials/presentation.html

@ -9,7 +9,7 @@
</div> </div>
<div class="welcome container-fluid" ng-hide="currentPresentation || loading"> <div class="welcome container-fluid" ng-hide="currentPresentation || loading">
<h1>{{_('Please upload a document')}}</h1> <h1>{{_('Please upload a document')}}</h1>
<div class="center-block"> <div>
<p>{{_('Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.')}}</p> <p>{{_('Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.')}}</p>
<p><button class="btn btn-lg btn-primary">{{_('Upload')}}</button></p> <p><button class="btn btn-lg btn-primary">{{_('Upload')}}</button></p>
<p>{{_('You can drag files here too.')}}</p> <p>{{_('You can drag files here too.')}}</p>

50
static/partials/settings.html

@ -51,7 +51,7 @@
<button class="btn btn-small btn-primary btn-sign-in" ng-click="usersettings.loginUserid()">{{_('Sign in')}}</button> <button class="btn btn-small btn-primary btn-sign-in" ng-click="usersettings.loginUserid()">{{_('Sign in')}}</button>
</label> </label>
<label ng-if="usersettings.registerUserid"> <label ng-if="usersettings.registerUserid">
<button class="btn btn-small btn-link btn-create-account" ng-click="usersettings.registerUserid($event)">{{_('Create an account')}}</button> <button class="btn btn-small btn-info btn-create-account" ng-click="usersettings.registerUserid($event)">{{_('Create an account')}}</button>
</label> </label>
</div> </div>
<p class="form-control-static well well-sm" ng-if="userid">{{userid|displayUserid}}</p> <p class="form-control-static well well-sm" ng-if="userid">{{userid|displayUserid}}</p>
@ -59,7 +59,7 @@
<button class="btn btn-small btn-default btn-sign-out" ng-click="usersettings.forgetUserid()">{{_('Sign out')}}</button> <button class="btn btn-small btn-default btn-sign-out" ng-click="usersettings.forgetUserid()">{{_('Sign out')}}</button>
</label> </label>
<label ng-if="userid && usersettings.accountUserid"> <label ng-if="userid && usersettings.accountUserid">
<button class="btn btn-small btn-link btn-manage-account" ng-click="usersettings.accountUserid()">{{_('Manage account')}}</button> <button class="btn btn-small btn-info btn-manage-account" ng-click="usersettings.accountUserid()">{{_('Manage account')}}</button>
</label> </label>
</div> </div>
</div> </div>
@ -130,17 +130,6 @@
<div> <div>
<div class="form-group">
<label class="col-xs-4 control-label">{{_('Stereo audio')}}</label>
<div class="col-xs-8">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="user.settings.stereo"/>&nbsp;
</label>
</div>
</div>
</div>
<div class="form-group" ng-show="isChrome && supported.renderToAssociatedSink"> <div class="form-group" ng-show="isChrome && supported.renderToAssociatedSink">
<label class="col-xs-4 control-label">{{_('Play audio on same device as selected microphone')}}</label> <label class="col-xs-4 control-label">{{_('Play audio on same device as selected microphone')}}</label>
<div class="col-xs-8"> <div class="col-xs-8">
@ -202,6 +191,30 @@
</div> </div>
</div> </div>
<div class="form-group" ng-show="isChrome">
<label class="col-xs-4 control-label">{{_('Send stereo audio')}}</label>
<div class="col-xs-8">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="user.settings.sendStereo"/>&nbsp;
</label>
</div>
<span class="help-block">{{_('Sending stereo audio disables echo cancellation. Enable only if you have stereo input.')}}</span>
</div>
</div>
<div class="form-group" ng-show="isChrome">
<label class="col-xs-4 control-label">{{_('Detect CPU over use')}}</label>
<div class="col-xs-8">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="user.settings.videoCpuOveruseDetection"/>&nbsp;
</label>
<span class="help-block">{{_('Automatically reduces video quality as needed.')}}</span>
</div>
</div>
</div>
<div ng-show="user.settings.experimental.enabled"> <div ng-show="user.settings.experimental.enabled">
<div class="form-group" ng-show="isChrome"> <div class="form-group" ng-show="isChrome">
@ -226,17 +239,6 @@
</div> </div>
</div> </div>
<div class="form-group" ng-show="isChrome">
<label class="col-xs-4 control-label">{{_('Detect CPU over use')}}</label>
<div class="col-xs-8">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="user.settings.experimental.videoCpuOveruseDetection"/>&nbsp;
</label>
</div>
</div>
</div>
</div> </div>
<div class="form-group" ng-show="isChrome"> <div class="form-group" ng-show="isChrome">

18
static/partials/youtubevideo.html

@ -3,32 +3,28 @@
<form class="container-fluid form" role="form"> <form class="container-fluid form" role="form">
<div class="welcome container-fluid" ng-show="!playbackActive && youtubeAPIReady"> <div class="welcome container-fluid" ng-show="!playbackActive && youtubeAPIReady">
<div class="welcome-logo fa fa-youtube"></div>
<h1>{{_('Share a YouTube video')}}</h1> <h1>{{_('Share a YouTube video')}}</h1>
<div class="welcome-container"> <div class="welcome-container">
<p> <p>{{_("Videos play simultaneously for everyone in this call.")}}</p>
<div>{{_("Videos are shared with everyone in this call.")}}</div> <div class="form-group welcome-input">
<div class="form-group welcome-input">
<input type="text" class="form-control input-lg" ng-model="youtubeurl" placeholder="{{_('YouTube URL')}}"> <input type="text" class="form-control input-lg" ng-model="youtubeurl" placeholder="{{_('YouTube URL')}}">
<div class="welcome-input-buttons"> <div class="welcome-input-buttons">
<button class="btn btn-primary" type="button" ng-disabled="!youtubeurl" ng-click="shareVideo(youtubeurl)">{{_("Share")}}</button> <button class="btn btn-primary" type="button" ng-disabled="!youtubeurl" ng-click="shareVideo(youtubeurl)">{{_("Share")}}</button>
</div> </div>
</div> </div>
</p>
</div> </div>
</div> </div>
<div class="welcome container-fluid" ng-show="!youtubeAPIReady"> <div class="welcome container-fluid" ng-show="!youtubeAPIReady">
<div class="welcome-logo fa fa-youtube"></div>
<h1>{{_('Share a YouTube video')}}</h1> <h1>{{_('Share a YouTube video')}}</h1>
<div class="welcome-container text-center"> <div class="welcome-container">
<p> <div>
<p>{{_("Could not load YouTube player API, please check your network / firewall settings.")}}</p> <p>{{_("Could not load YouTube player API, please check your network / firewall settings.")}}</p>
<p ng-if="currentVideoUrl">{{_('Currently playing')}}<br><a href="{{ currentVideoUrl }}" rel="external" target="_blank">{{ currentVideoUrl }}</a></p> <p ng-if="currentVideoUrl">{{_('Currently playing')}}<br><a href="{{ currentVideoUrl }}" rel="external" target="_blank">{{ currentVideoUrl }}</a></p>
<p class="form-group welcome-input"> <p class="form-group welcome-input">
<button class="btn btn-primary btn-lg" type="button" ng-click="loadYouTubeAPI()">{{_("Retry")}}</button> <button class="btn btn-primary btn-lg" type="button" ng-click="loadYouTubeAPI()">{{_("Retry")}}</button>
</p> </p>
</p> </div>
</div> </div>
</div> </div>

2
static/translation/messages-de.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-ja.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-ko.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-zh-cn.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-zh-tw.json

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save