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. 23
      README.md
  5. 10
      configure.ac
  6. 14
      debian/changelog
  7. 4
      package.json
  8. 1
      server.conf.in
  9. 7
      src/app/spreed-webrtc-server/channelling_api.go
  10. 24
      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. 154
      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. 453
      static/js/mediastream/utils.js
  36. 25
      static/js/mediastream/webrtc.js
  37. 19
      static/js/services/contacts.js
  38. 38
      static/js/services/modules.js
  39. 9
      static/js/services/services.js
  40. 84
      static/js/services/videolayout.js
  41. 2
      static/partials/buddy.html
  42. 2
      static/partials/menu.html
  43. 2
      static/partials/presentation.html
  44. 42
      static/partials/settings.html
  45. 12
      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

23
README.md

@ -1,8 +1,10 @@
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.
The latest version of Spreed WebRTC can be found on GitHub:
https://github.com/strukturag/spreed-webrtc https://github.com/strukturag/spreed-webrtc
@ -17,7 +19,8 @@ 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
@ -54,14 +57,16 @@ The latest version of Spreed WebRTC can be found on GitHub:
spreed-webrtc-server [OPTIONS] spreed-webrtc-server [OPTIONS]
``` ```
Options ### Options
```
-c="./server.conf": Configuration file. -c="./server.conf": Configuration file.
-cpuprofile="": Write cpu profile to file. -cpuprofile="": Write cpu profile to file.
-h=false: Show this usage information and exit. -h=false: Show this usage information and exit.
-l="": Log file, defaults to stderr. -l="": Log file, defaults to stderr.
-memprofile="": Write memory profile to this file. -memprofile="": Write memory profile to this file.
-v=false: Display version number and exit. -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.
@ -87,12 +92,14 @@ The latest version of Spreed WebRTC can be found on GitHub:
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
@ -124,11 +131,11 @@ The latest version of Spreed WebRTC can be found on GitHub:
## 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

7
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,11 +176,15 @@ 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 api.Config.WithModule("contacts") {
if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil { if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil {
users = api.GetUserSessions(session, userID) users = api.GetUserSessions(session, userID)
} else { } else {
log.Printf(err.Error()) log.Printf(err.Error())
} }
} else {
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)
if err != nil { if err != nil {

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

@ -48,6 +48,7 @@ type Config struct {
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
modulesTable map[string]bool // Map of enabled modules
globalRoomID string // Id of the global room (not exported to Javascript) globalRoomID string // Id of the global room (not exported to Javascript)
contentSecurityPolicy string // HTML content security policy contentSecurityPolicy string // HTML content security policy
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode contentSecurityPolicyReportOnly string // HTML content security policy in report only mode
@ -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);

154
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) {
return new mozRTCPeerConnection(pcConfig, pcConstraints); 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);
};
// 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 = {
'url': turnUrlParts[0],
'credential': password, 'credential': password,
'username': username}; '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 = {
'url': url,
'credential': password, 'credential': password,
'username': username}; '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 = {
'url': url,
'credential': password, 'credential': password,
'username': username}; '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.
iceServers = [{'urls': urls,
'credential': password, 'credential': password,
'username': username }]; '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) {

453
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) { function mergeConstraints(cons1, cons2) {
if (!cons1 || !cons2) {
return cons1 || cons2;
}
var merged = cons1; var merged = cons1;
var name; for (var name in cons2.mandatory) {
for (name in cons2.mandatory) {
if (cons2.mandatory.hasOwnProperty(name)) { if (cons2.mandatory.hasOwnProperty(name)) {
merged.mandatory[name] = cons2.mandatory[name]; merged.mandatory[name] = cons2.mandatory[name];
} }
} }
merged.optional.concat(cons2.optional); merged.optional = merged.optional.concat(cons2.optional);
return merged; return merged;
}; }
Utils.prototype.extractSdp = function(sdpLine, pattern) { function iceCandidateType(candidateStr) {
var result = sdpLine.match(pattern); return candidateStr.split(' ')[7];
return (result && result.length == 2) ? result[1] : null; }
};
Utils.prototype.addStereo = function(sdp) { function maybeSetOpusOptions(sdp, params) {
// Set Opus in Stereo. // Set Opus in Stereo, if stereo is true, unset it, if stereo is false, and
// do nothing if otherwise.
if (params.opusStereo === 'true') {
sdp = setCodecParam(sdp, 'opus/48000', 'stereo', '1');
} else if (params.opusStereo === 'false') {
sdp = removeCodecParam(sdp, 'opus/48000', 'stereo');
}
var sdpLines = sdp.split('\r\n'); // Set Opus FEC, if opusfec is true, unset it, if opusfec is false, and
var opusPayload = ""; // do nothing if otherwise.
var fmtpLineIndex = null; if (params.opusFec === 'true') {
var i; sdp = setCodecParam(sdp, 'opus/48000', 'useinbandfec', '1');
} else if (params.opusFec === 'false') {
sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec');
}
// Find opus payload. // Set Opus maxplaybackrate, if requested.
for (i = 0; i < sdpLines.length; i++) { if (params.opusMaxPbr) {
if (sdpLines[i].search('opus/48000') !== -1) { sdp = setCodecParam(
opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); sdp, 'opus/48000', 'maxplaybackrate', params.opusMaxPbr);
break;
} }
return sdp;
} }
// Find the payload in fmtp line. function maybeSetAudioSendBitRate(sdp, params) {
for (i = 0; i < sdpLines.length; i++) { if (!params.audioSendBitrate) {
if (sdpLines[i].search('a=fmtp') !== -1) { return sdp;
var payload = this.extractSdp(sdpLines[i], /a=fmtp:(\d+)/);
if (payload === opusPayload) {
fmtpLineIndex = i;
break;
} }
trace('Prefer audio send bitrate: ' + params.audioSendBitrate);
return preferBitRate(sdp, params.audioSendBitrate, 'audio');
} }
function maybeSetAudioReceiveBitRate(sdp, params) {
if (!params.audioRecvBitrate) {
return sdp;
} }
// No fmtp line found. trace('Prefer audio receive bitrate: ' + params.audioRecvBitrate);
if (fmtpLineIndex === null) { return preferBitRate(sdp, params.audioRecvBitrate, 'audio');
console.log("Unable to add stereo (no fmtp line for opus payload)", opusPayload); }
function maybeSetVideoSendBitRate(sdp, params) {
if (!params.videoSendBitrate) {
return sdp; return sdp;
} }
trace('Prefer video send bitrate: ' + params.videoSendBitrate);
return preferBitRate(sdp, params.videoSendBitrate, 'video');
}
// Append stereo=1 to fmtp line. function maybeSetVideoReceiveBitRate(sdp, params) {
sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1'); if (!params.videoRecvBitrate) {
return sdp;
}
trace('Prefer video receive bitrate: ' + params.videoRecvBitrate);
return preferBitRate(sdp, params.videoRecvBitrate, 'video');
}
// Add a b=AS:bitrate line to the m=mediaType section.
function preferBitRate(sdp, bitrate, mediaType) {
var sdpLines = sdp.split('\r\n');
// Find m line for the given mediaType.
var mLineIndex = findLine(sdpLines, 'm=', mediaType);
if (mLineIndex === null) {
trace('Failed to add bandwidth line to sdp, as no m-line found');
return sdp;
}
// Find next m-line if any.
var nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, 'm=');
if (nextMLineIndex === null) {
nextMLineIndex = sdpLines.length;
}
// Find c-line corresponding to the m-line.
var cLineIndex = findLineInRange(sdpLines, mLineIndex + 1,
nextMLineIndex, 'c=');
if (cLineIndex === null) {
trace('Failed to add bandwidth line to sdp, as no c-line found');
return sdp;
}
// Check if bandwidth line already exists between c-line and next m-line.
var bLineIndex = findLineInRange(sdpLines, cLineIndex + 1,
nextMLineIndex, 'b=AS');
if (bLineIndex) {
sdpLines.splice(bLineIndex, 1);
}
// Create the b (bandwidth) sdp line.
var bwLine = 'b=AS:' + bitrate;
// As per RFC 4566, the b line should follow after c-line.
sdpLines.splice(cLineIndex + 1, 0, bwLine);
sdp = sdpLines.join('\r\n'); sdp = sdpLines.join('\r\n');
console.log("Enabled opus stereo.");
return sdp; return sdp;
}
}; // Add an a=fmtp: x-google-min-bitrate=kbps line, if videoSendInitialBitrate
// is specified. We'll also add a x-google-min-bitrate value, since the max
// must be >= the min.
function maybeSetVideoSendInitialBitRate(sdp, params) {
var initialBitrate = params.videoSendInitialBitrate;
if (!initialBitrate) {
return sdp;
}
Utils.prototype.preferOpus = function(sdp) { // Validate the initial bitrate value.
// Set Opus as the preferred codec in SDP if Opus is present. var maxBitrate = initialBitrate;
var bitrate = params.videoSendBitrate;
if (bitrate) {
if (initialBitrate > bitrate) {
trace('Clamping initial bitrate to max bitrate of ' +
bitrate + ' kbps.');
initialBitrate = bitrate;
params.videoSendInitialBitrate = initialBitrate;
}
maxBitrate = bitrate;
}
var sdpLines = sdp.split('\r\n'); var sdpLines = sdp.split('\r\n');
var mLineIndex = null;
var i;
// Search for m line. // Search for m line.
for (i = 0; i < sdpLines.length; i++) { var mLineIndex = findLine(sdpLines, 'm=', 'video');
if (sdpLines[i].search('m=audio') !== -1) { if (mLineIndex === null) {
mLineIndex = i; trace('Failed to find video m-line');
break; 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) { if (mLineIndex === null) {
return sdp; return sdp;
} }
// If Opus is available, set it as the default in m line. // If the codec is available, set it as the default in m line.
for (i = 0; i < sdpLines.length; i++) { var payload = getCodecPayloadType(sdpLines, codec);
if (sdpLines[i].search('opus/48000') !== -1) { if (payload) {
var opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
if (opusPayload) { }
sdpLines[mLineIndex] = this.setDefaultCodec(sdpLines[mLineIndex], opusPayload);
sdp = sdpLines.join('\r\n');
return sdp;
} }
break;
// 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);
} }
// Remove CN in m line and sdp. sdp = sdpLines.join('\r\n');
sdpLines = this.removeCN(sdpLines, mLineIndex); 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'); sdp = sdpLines.join('\r\n');
return sdp; 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('; ');
Utils.prototype.setDefaultCodec = function(mLine, payload) { var pattern = new RegExp('a=fmtp:(\\d+)');
// Set the selected codec to the first in m line. var result = fmtpLine.match(pattern);
var elements = mLine.split(' '); if (result && result.length === 2) {
var newLine = []; fmtpObj.pt = result[1];
var index = 0; } else {
for (var i = 0; i < elements.length; i++) { return null;
// Format of media starts from the fourth.
if (index === 3) {
newLine[index++] = payload; // Put target payload to the first.
} }
if (elements[i] !== payload) {
newLine[index++] = elements[i]; var params = {};
for (var i = 0; i < keyValues.length; ++i) {
var pair = keyValues[i].split('=');
if (pair.length === 2) {
params[pair[0]] = pair[1];
} }
} }
return newLine.join(' '); fmtpObj.params = params;
};
return fmtpObj;
Utils.prototype.removeCN = function(sdpLines, mLineIndex) { }
// Strip CN from sdp before CN constraints is ready.
var mLineElements = sdpLines[mLineIndex].split(' '); // Generate an fmtp line from an object including 'pt' and 'params'.
// Scan from end for the convenience of removing an item. function writeFmtpLine(fmtpObj) {
for (var i = sdpLines.length - 1; i >= 0; i--) { if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) {
var payload = this.extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); return null;
if (payload) { }
var cnPos = mLineElements.indexOf(payload); var pt = fmtpObj.pt;
if (cnPos !== -1) { var params = fmtpObj.params;
// Remove CN payload from m line. var keyValues = [];
mLineElements.splice(cnPos, 1); 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;
}
} }
// Remove CN line in sdp
sdpLines.splice(i, 1);
} }
return null;
} }
sdpLines[mLineIndex] = mLineElements.join(' ');
return sdpLines;
};
var utils = new Utils(); // Gets the codec payload type from sdp lines.
return utils; 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() {

19
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) {
var withContacts = modules.withModule("contacts");
if (withContacts) {
// Inject our templates. // Inject our templates.
$templateCache.put('/contactsmanager/main.html', templateContactsManager); $templateCache.put('/contactsmanager/main.html', templateContactsManager);
$templateCache.put('/contactsmanager/edit.html', templateContactsManagerEdit); $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) {

84
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,40 +96,31 @@ 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.
// http://caniuse.com/object-fit
var aspectRatioLocal = scope.localVideo.videoWidth / scope.localVideo.videoHeight;
extraCSS = {};
extraCSS[".renderer-"+this.name+" .miniVideo"] = { extraCSS[".renderer-"+this.name+" .miniVideo"] = {
width: ($(scope.mini).height() * aspectRatioLocal) + "px" width: ($(scope.mini).height() * defaultAspectRatio) + "px"
}; };
}
if (videos.length === 1) {
var newVideoWidth = innerWidth < aspectRatio * innerHeight ? innerWidth : aspectRatio * innerHeight;
var newVideoHeight = innerHeight < innerWidth / aspectRatio ? innerHeight : innerWidth / aspectRatio;
container.style.width = newVideoWidth + 'px';
container.style.left = ((innerWidth - newVideoWidth) / 2) + 'px';
} else {
var space = innerHeight * innerWidth; // square pixels var space = innerHeight * innerWidth; // square pixels
var videoSpace = space / videos.length; var videoSpace = space / videos.length;
var singleVideoWidthOptimal = Math.pow(videoSpace * aspectRatio, 0.5); var singleVideoWidthOptimal = Math.pow(videoSpace * aspectRatio, 0.5);
@ -143,20 +147,14 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern
*/ */
container.style.width = newContainerWidth + "px"; container.style.width = newContainerWidth + "px";
container.style.left = ((innerWidth - newContainerWidth) / 2) + 'px'; container.style.left = ((innerWidth - newContainerWidth) / 2) + 'px';
var extraCSS2 = {}; extraCSS[".renderer-"+this.name+" .remoteVideos"] = {
extraCSS2[".renderer-"+this.name+" .remoteVideos"] = {
">div": { ">div": {
width: singleVideoWidth + "px", width: singleVideoWidth + "px",
height: singleVideoHeight + "px" height: singleVideoHeight + "px"
} }
}; };
extraCSS = $.extend(extraCSS, extraCSS2);
} injectCSS(extraCSS);
$.injectCSS(extraCSS, {
truncateFirst: true,
containerName: dynamicCSSContainer,
useRawValues: true
});
}; };
@ -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>

42
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,36 +191,49 @@
</div> </div>
</div> </div>
<div ng-show="user.settings.experimental.enabled"> <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"> <div class="form-group" ng-show="isChrome">
<label class="col-xs-4 control-label">{{_('Optimize for high resolution video')}}</label> <label class="col-xs-4 control-label">{{_('Detect CPU over use')}}</label>
<div class="col-xs-8"> <div class="col-xs-8">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="user.settings.experimental.videoLeakyBucket"/>&nbsp; <input type="checkbox" ng-model="user.settings.videoCpuOveruseDetection"/>&nbsp;
</label> </label>
<span class="help-block">{{_('Automatically reduces video quality as needed.')}}</span>
</div> </div>
</div> </div>
</div> </div>
<div ng-show="user.settings.experimental.enabled">
<div class="form-group" ng-show="isChrome"> <div class="form-group" ng-show="isChrome">
<label class="col-xs-4 control-label">{{_('Reduce video noise')}}</label> <label class="col-xs-4 control-label">{{_('Optimize for high resolution video')}}</label>
<div class="col-xs-8"> <div class="col-xs-8">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="user.settings.experimental.videoNoiseReduction"/>&nbsp; <input type="checkbox" ng-model="user.settings.experimental.videoLeakyBucket"/>&nbsp;
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group" ng-show="isChrome"> <div class="form-group" ng-show="isChrome">
<label class="col-xs-4 control-label">{{_('Detect CPU over use')}}</label> <label class="col-xs-4 control-label">{{_('Reduce video noise')}}</label>
<div class="col-xs-8"> <div class="col-xs-8">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="user.settings.experimental.videoCpuOveruseDetection"/>&nbsp; <input type="checkbox" ng-model="user.settings.experimental.videoNoiseReduction"/>&nbsp;
</label> </label>
</div> </div>
</div> </div>

12
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