Browse Source

Merge branch 'release-0.23'

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

4
.hound.yml

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

8
.travis.yml

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

3
Makefile.am

@ -95,6 +95,9 @@ styles: fonts @@ -95,6 +95,9 @@ styles: fonts
styleshint:
cd $(CURDIR)/src/styles && $(MAKE) styleshint
styleslint:
cd $(CURDIR)/src/styles && $(MAKE) styleslint
jshint:
@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

123
README.md

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

10
configure.ac

@ -37,6 +37,7 @@ GO_VERSION_MIN=1.1 @@ -37,6 +37,7 @@ GO_VERSION_MIN=1.1
NODEJS_VERSION_MIN=0.6.0
NODEJS_VERSION_STYLES_MIN=0.10.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_MACRO_DIR([m4])
@ -123,6 +124,15 @@ else @@ -123,6 +124,15 @@ else
SASS_SUPPORT_STYLES=no],[SASS_SUPPORT_STYLES=yes])
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
AC_MSG_CHECKING([for compass support in sass])
tempfile=`mktemp -t XXXXXXblah`

14
debian/changelog vendored

@ -1,3 +1,17 @@ @@ -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
* No longer install config file in install target of Makefile. We leave it to the packaging.

4
package.json

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
{
"private": true,
"dependencies": {
"jshint": ">= 2.5.5",
"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 @@ -124,6 +124,7 @@ serverRealm = local
;screensharing = true
;youtube = true
;presentation = true
;contacts = true
[log]
;logfile = /var/log/spreed-webrtc-server.log

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

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

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

@ -31,26 +31,27 @@ import ( @@ -31,26 +31,27 @@ import (
)
type Config struct {
Title string // Title
ver string // Version (not exported to Javascript)
S string // Static URL prefix with version
B string // Base URL
Token string // Server token
StunURIs []string // STUN server URIs
TurnURIs []string // TURN server URIs
Tokens bool // True when we got a tokens file
Version string // Server version number
UsersEnabled bool // Flag if users are enabled
UsersAllowRegistration bool // Flag if users can register
UsersMode string // Users mode string
DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load
AuthorizeRoomCreation bool // Whether a user account is required to create rooms
AuthorizeRoomJoin bool // Whether a user account is required to join rooms
Modules []string // List of enabled modules
globalRoomID string // Id of the global room (not exported to Javascript)
contentSecurityPolicy string // HTML content security policy
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode
Title string // Title
ver string // Version (not exported to Javascript)
S string // Static URL prefix with version
B string // Base URL
Token string // Server token
StunURIs []string // STUN server URIs
TurnURIs []string // TURN server URIs
Tokens bool // True when we got a tokens file
Version string // Server version number
UsersEnabled bool // Flag if users are enabled
UsersAllowRegistration bool // Flag if users can register
UsersMode string // Users mode string
DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load
AuthorizeRoomCreation bool // Whether a user account is required to create rooms
AuthorizeRoomJoin bool // Whether a user account is required to join rooms
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)
contentSecurityPolicy string // HTML content security policy
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode
}
func NewConfig(container phoenix.Container, tokens bool) *Config {
@ -88,11 +89,18 @@ func NewConfig(container phoenix.Container, tokens bool) *Config { @@ -88,11 +89,18 @@ func NewConfig(container phoenix.Container, tokens bool) *Config {
trimAndRemoveDuplicates(&turnURIs)
// Get enabled modules.
allModules := []string{"screensharing", "youtube", "presentation"}
modules := allModules[:0]
for _, module := range allModules {
modulesTable := map[string]bool{
"screensharing": true,
"youtube": true,
"presentation": true,
"contacts": true,
}
modules := []string{}
for module, _ := range modulesTable {
if container.GetBoolDefault("modules", module, true) {
modules = append(modules, module)
} else {
modulesTable[module] = false
}
}
log.Println("Enabled modules:", modules)
@ -115,6 +123,7 @@ func NewConfig(container phoenix.Container, tokens bool) *Config { @@ -115,6 +123,7 @@ func NewConfig(container phoenix.Container, tokens bool) *Config {
AuthorizeRoomCreation: container.GetBoolDefault("app", "authorizeRoomCreation", false),
AuthorizeRoomJoin: container.GetBoolDefault("app", "authorizeRoomJoin", false),
Modules: modules,
modulesTable: modulesTable,
globalRoomID: container.GetStringDefault("app", "globalRoom", ""),
contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""),
contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""),
@ -125,6 +134,15 @@ func (config *Config) Get(request *http.Request) (int, interface{}, http.Header) @@ -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"}}
}
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.
func trimAndRemoveDuplicates(data *[]string) {
found := make(map[string]bool)

28
src/i18n/messages-de.po

@ -8,8 +8,8 @@ msgid "" @@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2015-01-28 15:06+0100\n"
"PO-Revision-Date: 2015-01-28 15:07+0100\n"
"POT-Creation-Date: 2015-02-18 14:46+0100\n"
"PO-Revision-Date: 2015-02-18 14:49+0100\n"
"Last-Translator: Simon Eisenmann <simon@struktur.de>\n"
"Language-Team: struktur AG <opensource@struktur.de>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
@ -347,9 +347,6 @@ msgstr "Aktiviert" @@ -347,9 +347,6 @@ msgstr "Aktiviert"
msgid "Advanced settings"
msgstr "Erweiterte Einstellungen"
msgid "Stereo audio"
msgstr "Stereo-Audio"
msgid "Play audio on same device as selected microphone"
msgstr "Audioausgabe auf dem zum Mikrofon gehörenden Gerät"
@ -368,15 +365,28 @@ msgstr "Max. Bildwiederholrate" @@ -368,15 +365,28 @@ msgstr "Max. Bildwiederholrate"
msgid "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"
msgstr "Für hohe Auflösung optimieren"
msgid "Reduce video noise"
msgstr "Rauschen reduzieren"
msgid "Detect CPU over use"
msgstr "CPU-Überlast erkennen"
msgid "Enable experiments"
msgstr "Experimente aktivieren"
@ -503,7 +513,7 @@ msgstr "Raum-Verlauf" @@ -503,7 +513,7 @@ msgstr "Raum-Verlauf"
msgid "Please sign in."
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."
msgid "YouTube URL"

25
src/i18n/messages-ja.po

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\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"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -343,9 +343,6 @@ msgstr "許可" @@ -343,9 +343,6 @@ msgstr "許可"
msgid "Advanced settings"
msgstr "詳細設定"
msgid "Stereo audio"
msgstr "ステレオ・オーディオ"
msgid "Play audio on same device as selected microphone"
msgstr ""
@ -364,15 +361,27 @@ msgstr "ビデオ最高フレームレート" @@ -364,15 +361,27 @@ msgstr "ビデオ最高フレームレート"
msgid "auto"
msgstr "自動"
msgid "Optimize for high resolution video"
msgstr ""
#, fuzzy
msgid "Send stereo audio"
msgstr "ステレオ・オーディオ"
msgid "Reduce video noise"
msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr ""
msgid "Detect CPU over use"
msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments"
msgstr ""
@ -495,7 +504,7 @@ msgstr "" @@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call."
msgid "Videos play simultaneously for everyone in this call."
msgstr ""
msgid "YouTube URL"

25
src/i18n/messages-ko.po

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\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"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -343,9 +343,6 @@ msgstr "허락됨" @@ -343,9 +343,6 @@ msgstr "허락됨"
msgid "Advanced settings"
msgstr "고급 설정"
msgid "Stereo audio"
msgstr "스테레오 음성"
msgid "Play audio on same device as selected microphone"
msgstr ""
@ -364,15 +361,27 @@ msgstr "비디오프레임 비율 최대화" @@ -364,15 +361,27 @@ msgstr "비디오프레임 비율 최대화"
msgid "auto"
msgstr "자동"
msgid "Optimize for high resolution video"
msgstr ""
#, fuzzy
msgid "Send stereo audio"
msgstr "스테레오 음성"
msgid "Reduce video noise"
msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr ""
msgid "Detect CPU over use"
msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments"
msgstr ""
@ -495,7 +504,7 @@ msgstr "" @@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call."
msgid "Videos play simultaneously for everyone in this call."
msgstr ""
msgid "YouTube URL"

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

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\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"
"Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -343,9 +343,6 @@ msgstr "启用" @@ -343,9 +343,6 @@ msgstr "启用"
msgid "Advanced settings"
msgstr "高级设置"
msgid "Stereo audio"
msgstr "立体声"
msgid "Play audio on same device as selected microphone"
msgstr ""
@ -364,15 +361,27 @@ msgstr "最大视频帧速率" @@ -364,15 +361,27 @@ msgstr "最大视频帧速率"
msgid "auto"
msgstr "自动"
msgid "Optimize for high resolution video"
msgstr ""
#, fuzzy
msgid "Send stereo audio"
msgstr "立体声"
msgid "Reduce video noise"
msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr ""
msgid "Detect CPU over use"
msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments"
msgstr ""
@ -495,7 +504,7 @@ msgstr "" @@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call."
msgid "Videos play simultaneously for everyone in this call."
msgstr ""
msgid "YouTube URL"

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

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\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"
"Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -343,9 +343,6 @@ msgstr "啟用" @@ -343,9 +343,6 @@ msgstr "啟用"
msgid "Advanced settings"
msgstr "高級設置"
msgid "Stereo audio"
msgstr "立體聲"
msgid "Play audio on same device as selected microphone"
msgstr ""
@ -364,15 +361,27 @@ msgstr "最大視頻幀速率" @@ -364,15 +361,27 @@ msgstr "最大視頻幀速率"
msgid "auto"
msgstr "自動"
msgid "Optimize for high resolution video"
msgstr ""
#, fuzzy
msgid "Send stereo audio"
msgstr "立體聲"
msgid "Reduce video noise"
msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr ""
msgid "Detect CPU over use"
msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments"
msgstr ""
@ -495,7 +504,7 @@ msgstr "" @@ -495,7 +504,7 @@ msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call."
msgid "Videos play simultaneously for everyone in this call."
msgstr ""
msgid "YouTube URL"

22
src/i18n/messages.pot

@ -9,7 +9,7 @@ msgid "" @@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -339,9 +339,6 @@ msgstr "" @@ -339,9 +339,6 @@ msgstr ""
msgid "Advanced settings"
msgstr ""
msgid "Stereo audio"
msgstr ""
msgid "Play audio on same device as selected microphone"
msgstr ""
@ -360,15 +357,26 @@ msgstr "" @@ -360,15 +357,26 @@ msgstr ""
msgid "auto"
msgstr ""
msgid "Optimize for high resolution video"
msgid "Send stereo audio"
msgstr ""
msgid "Reduce video noise"
msgid ""
"Sending stereo audio disables echo cancellation. Enable only if you have "
"stereo input."
msgstr ""
msgid "Detect CPU over use"
msgstr ""
msgid "Automatically reduces video quality as needed."
msgstr ""
msgid "Optimize for high resolution video"
msgstr ""
msgid "Reduce video noise"
msgstr ""
msgid "Enable experiments"
msgstr ""
@ -489,7 +497,7 @@ msgstr "" @@ -489,7 +497,7 @@ msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call."
msgid "Videos play simultaneously for everyone in this call."
msgstr ""
msgid "YouTube URL"

4
src/styles/Makefile.am

@ -43,3 +43,7 @@ styleshint: @@ -43,3 +43,7 @@ styleshint:
@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
$(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 @@ @@ -19,6 +19,8 @@
*
*/
// scss-lint:disable IdSelector
// Remove boostrap 3 modal scroll bar.
.modal {
overflow-y: auto;

3
src/styles/components/_audiovideo.scss

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

4
src/styles/components/_presentation.scss

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

44
src/styles/global/_withs.scss

@ -0,0 +1,44 @@ @@ -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 @@ @@ -33,6 +33,7 @@
@import "global/nicescroll";
@import "global/animations";
@import "global/overlaybar";
@import "global/withs";
@import "components/rightslide";
@import "components/bar";

199
src/styles/scss.yml

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

3
static/js/directives/buddylist.js

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

9
static/js/directives/menu.js

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

6
static/js/directives/settings.js

@ -248,6 +248,12 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -248,6 +248,12 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
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 {
// 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'], @@ -74,7 +74,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
$scope.volume = null;
isYouTubeIframeAPIReady.then(function() {
$scope.$apply(function(scope) {
safeApply($scope, function(scope) {
scope.youtubeAPIReady = true;
});
});

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

@ -1,5 +1,5 @@ @@ -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.
* see: http://github.com/requirejs/text for details
*/
@ -23,7 +23,7 @@ define(['module'], function (module) { @@ -23,7 +23,7 @@ define(['module'], function (module) {
masterConfig = (module.config && module.config()) || {};
text = {
version: '2.0.12',
version: '2.0.13',
strip: function (content) {
//Strips <?xml ...?> declarations so that external SVG and XML
@ -85,13 +85,13 @@ define(['module'], function (module) { @@ -85,13 +85,13 @@ define(['module'], function (module) {
parseName: function (name) {
var modName, ext, temp,
strip = false,
index = name.indexOf("."),
index = name.lastIndexOf("."),
isRelative = name.indexOf('./') === 0 ||
name.indexOf('../') === 0;
if (index !== -1 && (!isRelative || index > 1)) {
modName = name.substring(0, index);
ext = name.substring(index + 1, name.length);
ext = name.substring(index + 1);
} else {
modName = name;
}
@ -252,7 +252,7 @@ define(['module'], function (module) { @@ -252,7 +252,7 @@ define(['module'], function (module) {
try {
var file = fs.readFileSync(url, 'utf8');
//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);
}
callback(file);

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

@ -37,117 +37,103 @@ var reattachMediaStream = null; @@ -37,117 +37,103 @@ var reattachMediaStream = null;
var webrtcDetectedBrowser = 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) {
console.log("This appears to be Firefox");
console.log('This appears to be Firefox');
webrtcDetectedBrowser = "firefox";
webrtcDetectedBrowser = 'firefox';
webrtcDetectedVersion =
parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
// The RTCPeerConnection object.
var RTCPeerConnection = function(pcConfig, pcConstraints) {
window.RTCPeerConnection = function(pcConfig, pcConstraints) {
// .urls is not supported in FF yet.
maybeFixConfiguration(pcConfig);
if (pcConfig && pcConfig.iceServers) {
for (var i = 0; i < pcConfig.iceServers.length; i++) {
if (pcConfig.iceServers[i].hasOwnProperty('urls')) {
pcConfig.iceServers[i].url = pcConfig.iceServers[i].urls;
delete pcConfig.iceServers[i].urls;
}
}
}
return new mozRTCPeerConnection(pcConfig, pcConstraints);
}
};
// The RTCSessionDescription object.
RTCSessionDescription = mozRTCSessionDescription;
window.RTCSessionDescription = mozRTCSessionDescription;
// 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.
getUserMedia = navigator.mozGetUserMedia.bind(navigator);
navigator.getUserMedia = getUserMedia;
// Creates iceServer from the url for FF.
createIceServer = function(url, username, password) {
// Creates ICE server from the URL for FF.
window.createIceServer = function(url, username, password) {
var iceServer = null;
var url_parts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) {
// Create iceServer with stun url.
iceServer = { 'url': url };
} else if (url_parts[0].indexOf('turn') === 0) {
var urlParts = url.split(':');
if (urlParts[0].indexOf('stun') === 0) {
// Create ICE server with STUN URL.
iceServer = {
'url': url
};
} else if (urlParts[0].indexOf('turn') === 0) {
if (webrtcDetectedVersion < 27) {
// Create iceServer with turn url.
// 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.
if (turn_url_parts.length === 1 ||
turn_url_parts[1].indexOf('transport=udp') === 0) {
iceServer = {'url': turn_url_parts[0],
'credential': password,
'username': username};
if (turnUrlParts.length === 1 ||
turnUrlParts[1].indexOf('transport=udp') === 0) {
iceServer = {
'url': turnUrlParts[0],
'credential': password,
'username': username
};
}
} else {
// FF 27 and above supports transport parameters in TURN url,
// So passing in the full url to create iceServer.
iceServer = {'url': url,
'credential': password,
'username': username};
iceServer = {
'url': url,
'credential': password,
'username': username
};
}
}
return iceServer;
};
createIceServers = function(urls, username, password) {
window.createIceServers = function(urls, username, password) {
var iceServers = [];
// Use .url for FireFox.
for (i = 0; i < urls.length; i++) {
var iceServer = createIceServer(urls[i],
username,
password);
for (var i = 0; i < urls.length; i++) {
var iceServer =
window.createIceServer(urls[i], username, password);
if (iceServer !== null) {
iceServers.push(iceServer);
}
}
return iceServers;
}
};
// Attach a media stream to an element.
attachMediaStream = function(element, stream) {
console.log("Attaching media stream");
console.log('Attaching media stream');
element.mozSrcObject = stream;
element.play();
};
reattachMediaStream = function(to, from) {
console.log("Reattaching media stream");
console.log('Reattaching media stream');
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) {
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.
// Setting Chrome version to 999, if version is unavailable.
var result = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
@ -158,50 +144,38 @@ if (navigator.mozGetUserMedia) { @@ -158,50 +144,38 @@ if (navigator.mozGetUserMedia) {
}
// 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 url_parts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) {
var urlParts = url.split(':');
if (urlParts[0].indexOf('stun') === 0) {
// Create iceServer with stun url.
iceServer = { 'url': url };
} else if (url_parts[0].indexOf('turn') === 0) {
iceServer = {
'url': url
};
} else if (urlParts[0].indexOf('turn') === 0) {
// Chrome M28 & above uses below TURN format.
iceServer = {'url': url,
'credential': password,
'username': username};
iceServer = {
'url': url,
'credential': password,
'username': username
};
}
return iceServer;
};
// Creates iceServers from the urls for Chrome M34 and above.
createIceServers = function(urls, username, password) {
var iceServers = [];
if (webrtcDetectedVersion >= 34) {
// .urls is supported since Chrome M34.
iceServers = [{'urls': urls,
'credential': password,
'username': username }];
} else {
for (i = 0; i < urls.length; i++) {
var iceServer = createIceServer(urls[i],
username,
password);
if (iceServer !== null) {
iceServers.push(iceServer);
}
}
}
return iceServers;
// Creates an ICEServer object from multiple URLs.
window.createIceServers = function(urls, username, password) {
return [{
'urls': urls,
'credential': password,
'username': username
}];
};
// The RTCPeerConnection object.
var RTCPeerConnection = function(pcConfig, pcConstraints) {
// .urls is supported since Chrome M34.
if (webrtcDetectedVersion < 34) {
maybeFixConfiguration(pcConfig);
}
RTCPeerConnection = function(pcConfig, pcConstraints) {
return new webkitRTCPeerConnection(pcConfig, pcConstraints);
}
};
// Get UserMedia (only difference is the prefix).
// Code from Adam Barth.
@ -225,5 +199,5 @@ if (navigator.mozGetUserMedia) { @@ -225,5 +199,5 @@ if (navigator.mozGetUserMedia) {
to.src = from.src;
};
} 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 @@ -36,6 +36,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
this.pcConstraints = $.extend(true, {}, this.webrtc.settings.pcConstraints);
this.sdpConstraints = $.extend(true, {}, this.webrtc.settings.sdpConstraints);
this.offerConstraints = $.extend(true, {}, this.webrtc.settings.offerConstraints);
this.sdpParams = $.extend(true, {}, this.webrtc.settings.sdpParams);
this.peerconnection = null;
this.datachannels = {};
@ -84,8 +85,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection @@ -84,8 +85,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
PeerCall.prototype.onCreateAnswerOffer = function(cb, sessionDescription) {
// Prefer Opus.
sessionDescription.sdp = utils.preferOpus(sessionDescription.sdp);
this.setLocalSdp(sessionDescription);
// Convert to object to allow custom property injection.
var sessionDescriptionObj = sessionDescription;
@ -128,6 +128,9 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection @@ -128,6 +128,9 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
console.log("Got a remote description but not connected -> ignored.");
return;
}
this.setRemoteSdp(sessionDescription);
peerconnection.setRemoteDescription(sessionDescription, _.bind(function() {
console.log("Set remote session description.", sessionDescription, this);
if (cb) {
@ -155,6 +158,28 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection @@ -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) {
if (event.candidate) {
//console.log("ice candidate", event.candidate);
@ -218,7 +243,11 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection @@ -218,7 +243,11 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection
// Avoid errors when still receiving candidates but closed.
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 @@ -56,7 +56,10 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens'], f
// SCTP is supported from Chrome M31.
// No need to pass DTLS constraint as it is on by default in Chrome M31.
// For SCTP, reliable and ordered is true by default.
this.pcConstraints = {};
this.pcConstraints = {
mandatory: {},
optional: []
};
// Inject token into sessiondescription and ice candidate 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 @@ -48,11 +48,17 @@ define(['jquery', 'underscore', 'mediastream/peercall', 'mediastream/tokens', 'w
audio: false,
video: false
};
this.sdpConstraints = {};
this.sdpConstraints = {
mandatory: {},
optional: []
};
// SCTP is supported from Chrome M31.
// No need to pass DTLS constraint as it is on by default in Chrome M31.
// For SCTP, reliable and ordered is true by default.
this.pcConstraints = {};
this.pcConstraints = {
mandatory: {},
optional: []
};
// Inject token into sessiondescription and ice candidate data.
this.e.on("sessiondescription icecandidate", _.bind(function(event, data) {

555
static/js/mediastream/utils.js

@ -1,162 +1,423 @@ @@ -1,162 +1,423 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
* Copyright (C) 2013-2015 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 file is a AMD wrapped version of the sdputils.js from the
* WebRTC apprtc example. https://github.com/webrtc/apprtc/blob/master/src
*
* 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.
* Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
*
* 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/>.
* Redistribution and use in source and binary forms, with or without
* 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";
define([], function() {
var Utils = function() {}
Utils.prototype.mergeConstraints = function(cons1, cons2) {
var merged = cons1;
var name;
for (name in cons2.mandatory) {
if (cons2.mandatory.hasOwnProperty(name)) {
merged.mandatory[name] = cons2.mandatory[name];
}
}
merged.optional.concat(cons2.optional);
return merged;
};
Utils.prototype.extractSdp = function(sdpLine, pattern) {
var result = sdpLine.match(pattern);
return (result && result.length == 2) ? result[1] : null;
};
Utils.prototype.addStereo = function(sdp) {
// Set Opus in Stereo.
var sdpLines = sdp.split('\r\n');
var opusPayload = "";
var fmtpLineIndex = null;
var i;
// Find opus payload.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
break;
}
}
// Find the payload in fmtp line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('a=fmtp') !== -1) {
var payload = this.extractSdp(sdpLines[i], /a=fmtp:(\d+)/);
if (payload === opusPayload) {
fmtpLineIndex = i;
break;
}
}
}
// No fmtp line found.
if (fmtpLineIndex === null) {
console.log("Unable to add stereo (no fmtp line for opus payload)", opusPayload);
return sdp;
}
// Append stereo=1 to fmtp line.
sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1');
sdp = sdpLines.join('\r\n');
console.log("Enabled opus stereo.");
return sdp;
};
Utils.prototype.preferOpus = function(sdp) {
// Set Opus as the preferred codec in SDP if Opus is present.
var sdpLines = sdp.split('\r\n');
var mLineIndex = null;
var i;
// Search for m line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('m=audio') !== -1) {
mLineIndex = i;
break;
}
}
if (mLineIndex === null) {
return sdp;
}
// If Opus is available, set it as the default in m line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
var opusPayload = this.extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
if (opusPayload) {
sdpLines[mLineIndex] = this.setDefaultCodec(sdpLines[mLineIndex], opusPayload);
}
break;
}
}
// Remove CN in m line and sdp.
sdpLines = this.removeCN(sdpLines, mLineIndex);
sdp = sdpLines.join('\r\n');
return sdp;
};
Utils.prototype.setDefaultCodec = function(mLine, payload) {
// Set the selected codec to the first in m line.
var elements = mLine.split(' ');
var newLine = [];
var index = 0;
for (var i = 0; i < elements.length; i++) {
// Format of media starts from the fourth.
if (index === 3) {
newLine[index++] = payload; // Put target payload to the first.
}
if (elements[i] !== payload) {
newLine[index++] = elements[i];
}
}
return newLine.join(' ');
};
Utils.prototype.removeCN = function(sdpLines, mLineIndex) {
// Strip CN from sdp before CN constraints is ready.
var mLineElements = sdpLines[mLineIndex].split(' ');
// Scan from end for the convenience of removing an item.
for (var i = sdpLines.length - 1; i >= 0; i--) {
var payload = this.extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
if (payload) {
var cnPos = mLineElements.indexOf(payload);
if (cnPos !== -1) {
// Remove CN payload from m line.
mLineElements.splice(cnPos, 1);
}
// Remove CN line in sdp
sdpLines.splice(i, 1);
}
}
sdpLines[mLineIndex] = mLineElements.join(' ');
return sdpLines;
};
var utils = new Utils();
return utils;
function trace(text) {
// noop
}
function mergeConstraints(cons1, cons2) {
if (!cons1 || !cons2) {
return cons1 || cons2;
}
var merged = cons1;
for (var name in cons2.mandatory) {
if (cons2.mandatory.hasOwnProperty(name)) {
merged.mandatory[name] = cons2.mandatory[name];
}
}
merged.optional = merged.optional.concat(cons2.optional);
return merged;
}
function iceCandidateType(candidateStr) {
return candidateStr.split(' ')[7];
}
function maybeSetOpusOptions(sdp, params) {
// 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');
}
// Set Opus FEC, if opusfec is true, unset it, if opusfec is false, and
// do nothing if otherwise.
if (params.opusFec === 'true') {
sdp = setCodecParam(sdp, 'opus/48000', 'useinbandfec', '1');
} else if (params.opusFec === 'false') {
sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec');
}
// Set Opus maxplaybackrate, if requested.
if (params.opusMaxPbr) {
sdp = setCodecParam(
sdp, 'opus/48000', 'maxplaybackrate', params.opusMaxPbr);
}
return sdp;
}
function maybeSetAudioSendBitRate(sdp, params) {
if (!params.audioSendBitrate) {
return sdp;
}
trace('Prefer audio send bitrate: ' + params.audioSendBitrate);
return preferBitRate(sdp, params.audioSendBitrate, 'audio');
}
function maybeSetAudioReceiveBitRate(sdp, params) {
if (!params.audioRecvBitrate) {
return sdp;
}
trace('Prefer audio receive bitrate: ' + params.audioRecvBitrate);
return preferBitRate(sdp, params.audioRecvBitrate, 'audio');
}
function maybeSetVideoSendBitRate(sdp, params) {
if (!params.videoSendBitrate) {
return sdp;
}
trace('Prefer video send bitrate: ' + params.videoSendBitrate);
return preferBitRate(sdp, params.videoSendBitrate, 'video');
}
function maybeSetVideoReceiveBitRate(sdp, params) {
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');
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;
}
// Validate the initial bitrate value.
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');
// Search for m line.
var mLineIndex = findLine(sdpLines, 'm=', 'video');
if (mLineIndex === null) {
trace('Failed to find video m-line');
return sdp;
}
sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-min-bitrate',
params.videoSendInitialBitrate.toString());
sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-max-bitrate',
maxBitrate.toString());
return sdp;
}
// Promotes |audioSendCodec| to be the first in the m=audio line, if set.
function maybePreferAudioSendCodec(sdp, params) {
return maybePreferCodec(sdp, 'audio', 'send', params.audioSendCodec);
}
// Promotes |audioRecvCodec| to be the first in the m=audio line, if set.
function maybePreferAudioReceiveCodec(sdp, params) {
return maybePreferCodec(sdp, 'audio', 'receive', params.audioRecvCodec);
}
// Promotes |videoSendCodec| to be the first in the m=audio line, if set.
function maybePreferVideoSendCodec(sdp, params) {
return maybePreferCodec(sdp, 'video', 'send', params.videoSendCodec);
}
// Promotes |videoRecvCodec| to be the first in the m=audio line, if set.
function maybePreferVideoReceiveCodec(sdp, params) {
return maybePreferCodec(sdp, 'video', 'receive', params.videoRecvCodec);
}
// Sets |codec| as the default |type| codec if it's present.
// The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
function maybePreferCodec(sdp, type, dir, codec) {
var str = type + ' ' + dir + ' codec';
if (codec === '') {
trace('No preference on ' + str + '.');
return sdp;
}
trace('Prefer ' + str + ': ' + codec);
var sdpLines = sdp.split('\r\n');
// Search for m line.
var mLineIndex = findLine(sdpLines, 'm=', type);
if (mLineIndex === null) {
return sdp;
}
// If the codec is available, set it as the default in m line.
var payload = getCodecPayloadType(sdpLines, codec);
if (payload) {
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
}
sdp = sdpLines.join('\r\n');
return sdp;
}
// Set fmtp param to specific codec in SDP. If param does not exists, add it.
function setCodecParam(sdp, codec, param, value) {
var sdpLines = sdp.split('\r\n');
var fmtpLineIndex = findFmtpLine(sdpLines, codec);
var fmtpObj = {};
if (fmtpLineIndex === null) {
var index = findLine(sdpLines, 'a=rtpmap', codec);
if (index === null) {
return sdp;
}
var payload = getCodecPayloadTypeFromLine(sdpLines[index]);
fmtpObj.pt = payload.toString();
fmtpObj.params = {};
fmtpObj.params[param] = value;
sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj));
} else {
fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]);
fmtpObj.params[param] = value;
sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj);
}
sdp = sdpLines.join('\r\n');
return sdp;
}
// Remove fmtp param if it exists.
function removeCodecParam(sdp, codec, param) {
var sdpLines = sdp.split('\r\n');
var fmtpLineIndex = findFmtpLine(sdpLines, codec);
if (fmtpLineIndex === null) {
return sdp;
}
var map = parseFmtpLine(sdpLines[fmtpLineIndex]);
delete map.params[param];
var newLine = writeFmtpLine(map);
if (newLine === null) {
sdpLines.splice(fmtpLineIndex, 1);
} else {
sdpLines[fmtpLineIndex] = newLine;
}
sdp = sdpLines.join('\r\n');
return sdp;
}
// Split an fmtp line into an object including 'pt' and 'params'.
function parseFmtpLine(fmtpLine) {
var fmtpObj = {};
var spacePos = fmtpLine.indexOf(' ');
var keyValues = fmtpLine.substring(spacePos + 1).split('; ');
var pattern = new RegExp('a=fmtp:(\\d+)');
var result = fmtpLine.match(pattern);
if (result && result.length === 2) {
fmtpObj.pt = result[1];
} else {
return null;
}
var params = {};
for (var i = 0; i < keyValues.length; ++i) {
var pair = keyValues[i].split('=');
if (pair.length === 2) {
params[pair[0]] = pair[1];
}
}
fmtpObj.params = params;
return fmtpObj;
}
// Generate an fmtp line from an object including 'pt' and 'params'.
function writeFmtpLine(fmtpObj) {
if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) {
return null;
}
var pt = fmtpObj.pt;
var params = fmtpObj.params;
var keyValues = [];
var i = 0;
for (var key in params) {
if (params.hasOwnProperty(key)) {
keyValues[i] = key + '=' + params[key];
++i;
}
}
if (i === 0) {
return null;
}
return 'a=fmtp:' + pt.toString() + ' ' + keyValues.join('; ');
}
// Find fmtp attribute for |codec| in |sdpLines|.
function findFmtpLine(sdpLines, codec) {
// Find payload of codec.
var payload = getCodecPayloadType(sdpLines, codec);
// Find the payload in fmtp line.
return payload ? findLine(sdpLines, 'a=fmtp:' + payload.toString()) : null;
}
// Find the line in sdpLines that starts with |prefix|, and, if specified,
// contains |substr| (case-insensitive search).
function findLine(sdpLines, prefix, substr) {
return findLineInRange(sdpLines, 0, -1, prefix, substr);
}
// Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
// and, if specified, contains |substr| (case-insensitive search).
function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
for (var i = startLine; i < realEndLine; ++i) {
if (sdpLines[i].indexOf(prefix) === 0) {
if (!substr ||
sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
return i;
}
}
}
return null;
}
// Gets the codec payload type from sdp lines.
function getCodecPayloadType(sdpLines, codec) {
var index = findLine(sdpLines, 'a=rtpmap', codec);
return index ? getCodecPayloadTypeFromLine(sdpLines[index]) : null;
}
// Gets the codec payload type from an a=rtpmap:X line.
function getCodecPayloadTypeFromLine(sdpLine) {
var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
var result = sdpLine.match(pattern);
return (result && result.length === 2) ? result[1] : null;
}
// Returns a new m= line with the specified codec as the first one.
function setDefaultCodec(mLine, payload) {
var elements = mLine.split(' ');
// Just copy the first three parameters; codec order starts on fourth.
var newLine = elements.slice(0, 3);
// Put target payload first and copy in the rest.
newLine.push(payload);
for (var i = 3; i < elements.length; i++) {
if (elements[i] !== payload) {
newLine.push(elements[i]);
}
}
return newLine.join(' ');
}
// Exported utils.
return {
mergeConstraints: mergeConstraints,
maybeSetOpusOptions: maybeSetOpusOptions,
maybeSetAudioSendBitRate: maybeSetAudioSendBitRate,
maybeSetAudioReceiveBitRate: maybeSetAudioReceiveBitRate,
maybeSetVideoSendBitRate: maybeSetVideoSendBitRate,
maybeSetVideoReceiveBitRate: maybeSetVideoReceiveBitRate,
maybeSetVideoSendInitialBitRate: maybeSetVideoSendInitialBitRate,
maybePreferAudioSendCodec: maybePreferAudioSendCodec,
maybePreferAudioReceiveCodec: maybePreferAudioReceiveCodec,
maybePreferVideoSendCodec: maybePreferVideoSendCodec,
maybePreferVideoReceiveCodec: maybePreferVideoReceiveCodec
}
});

25
static/js/mediastream/webrtc.js

@ -59,7 +59,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -59,7 +59,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Settings.are cloned into peer call on call creation.
this.settings = {
stereo: false,
mediaConstraints: {
audio: true,
video: {
@ -76,6 +75,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -76,6 +75,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
}]
},
pcConstraints: {
mandatory: {},
optional: []
},
// Set up audio and video regardless of what devices are present.
@ -83,7 +83,8 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -83,7 +83,8 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
},
optional: []
},
offerConstraints: {
mandatory: {},
@ -97,6 +98,20 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -97,6 +98,20 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
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 @@ -228,9 +243,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
switch (type) {
case "Offer":
console.log("Offer process.");
if (this.settings.stereo) {
data.sdp = utils.addStereo(data.sdp);
}
targetcall = this.findTargetCall(from);
if (targetcall) {
// Hey we know this call.
@ -280,9 +292,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -280,9 +292,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
return;
}
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
// and result in a offer sdp sent as answer data. We need to handle this.
targetcall.setRemoteDescription(new window.RTCSessionDescription(data), function() {

25
static/js/services/contacts.js

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

38
static/js/services/modules.js

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

148
static/js/services/videolayout.js

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

2
static/partials/buddy.html

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<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="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>

2
static/partials/menu.html

@ -3,7 +3,7 @@ @@ -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('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 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="{{_('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>

2
static/partials/presentation.html

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
</div>
<div class="welcome container-fluid" ng-hide="currentPresentation || loading">
<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><button class="btn btn-lg btn-primary">{{_('Upload')}}</button></p>
<p>{{_('You can drag files here too.')}}</p>

50
static/partials/settings.html

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

18
static/partials/youtubevideo.html

@ -3,32 +3,28 @@ @@ -3,32 +3,28 @@
<form class="container-fluid form" role="form">
<div class="welcome container-fluid" ng-show="!playbackActive && youtubeAPIReady">
<div class="welcome-logo fa fa-youtube"></div>
<h1>{{_('Share a YouTube video')}}</h1>
<div class="welcome-container">
<p>
<div>{{_("Videos are shared with everyone in this call.")}}</div>
<div class="form-group welcome-input">
<p>{{_("Videos play simultaneously for everyone in this call.")}}</p>
<div class="form-group welcome-input">
<input type="text" class="form-control input-lg" ng-model="youtubeurl" placeholder="{{_('YouTube URL')}}">
<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>
</p>
</div>
</div>
</div>
<div class="welcome container-fluid" ng-show="!youtubeAPIReady">
<div class="welcome-logo fa fa-youtube"></div>
<h1>{{_('Share a YouTube video')}}</h1>
<div class="welcome-container text-center">
<p>
<div class="welcome-container">
<div>
<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 class="form-group welcome-input">
<button class="btn btn-primary btn-lg" type="button" ng-click="loadYouTubeAPI()">{{_("Retry")}}</button>
</p>
</p>
</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