Browse Source

Implement TURN service client

To provide peer to peer support for users with a firewall, a TURN
service might be required. Not everyone can/wants to setup a self-hosted
TURN server. This change adds support to consume a remote TURN service
which usually requires authentication. Spreed WebRTC received TURN
credentials using this service in regular intervals and provides them to
all Spreed WebRTC sessions.

If the remote TURN service provides multiple zones and a GEO location
endpoint, the web client will also directly connect to that TURN service
GEO endpoint to let the TURN service select preferred zones based on the
clients information.

The advanced settings provide a way to control the TURN service zone
directly and to disable the client side GEO call. By default the
selection is auto if the TURN service provides a GEO endpoint. If no
such endpoint is required, the zone with the highest priority is used by
default (as sent by the TURN credentials service).
pull/346/head
Simon Eisenmann 10 years ago
parent
commit
e18dd5c933
  1. 1
      dependencies.tsv
  2. 75
      doc/CHANNELING-API.txt
  3. 6
      go/channelling/api/api.go
  4. 3
      go/channelling/api/api_test.go
  5. 4
      go/channelling/api/handle_authentication.go
  6. 4
      go/channelling/api/handle_self.go
  7. 7
      go/channelling/client.go
  8. 19
      go/channelling/data.go
  9. 9
      go/channelling/hub.go
  10. 4
      go/channelling/pipeline.go
  11. 2
      go/channelling/turndata.go
  12. 125
      go/channelling/turnservice_manager.go
  13. 11
      server.conf.in
  14. 12
      src/app/spreed-webrtc-server/main.go
  15. 3
      static/js/controllers/appcontroller.js
  16. 24
      static/js/controllers/uicontroller.js
  17. 20
      static/js/directives/settings.js
  18. 4
      static/js/mediastream/api.js
  19. 2
      static/js/services/constraints.js
  20. 9
      static/js/services/services.js
  21. 180
      static/js/services/turndata.js
  22. 10
      static/partials/settings.html

1
dependencies.tsv

@ -10,3 +10,4 @@ github.com/strukturag/goacceptlanguageparser git 68066e68c2940059aadc6e19661610c @@ -10,3 +10,4 @@ github.com/strukturag/goacceptlanguageparser git 68066e68c2940059aadc6e19661610c
github.com/strukturag/httputils git afbf05c71ac03ee7989c96d033a9571ba4ded468 2014-07-02T01:35:33Z
github.com/strukturag/phoenix git 31b7f25f4815e6e0b8e7c4010f6e9a71c4165b19 2016-06-01T11:34:58Z
github.com/strukturag/sloth git 74a8bcf67368de59baafe5d3e17aee9875564cfc 2015-04-22T08:59:42Z
github.com/strukturag/spreed-turnservicecli git 51f45889f0c6a4a7d406c29b9cc345b07a1a94ab 2016-08-26T13:54:01Z

1 github.com/dlintw/goconf git dcc070983490608a14480e3bf943bad464785df5 2012-02-28T08:26:10Z
10 github.com/strukturag/httputils git afbf05c71ac03ee7989c96d033a9571ba4ded468 2014-07-02T01:35:33Z
11 github.com/strukturag/phoenix git 31b7f25f4815e6e0b8e7c4010f6e9a71c4165b19 2016-06-01T11:34:58Z
12 github.com/strukturag/sloth git 74a8bcf67368de59baafe5d3e17aee9875564cfc 2015-04-22T08:59:42Z
13 github.com/strukturag/spreed-turnservicecli git 51f45889f0c6a4a7d406c29b9cc345b07a1a94ab 2016-08-26T13:54:01Z

75
doc/CHANNELING-API.txt

@ -150,12 +150,8 @@ Special purpose documents for channling @@ -150,12 +150,8 @@ Special purpose documents for channling
Version : Server version number. Use this to detect server upgrades.
ApiVersion : Server channeling API base version. Use this version to select
client side compatibility with the connected server.
Turn : Mapping (interface{}) to contain TURN server details, like
urls, password and username. See
http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
and TURN REST API section in
https://code.google.com/p/rfc5766-turn-server/wiki/turnserver
for details.
Turn : Mapping (interface{}) to contain TURN server details. See
TURN credentials and URNs section below for specification.
Stun : Array with STUN server URLs.
You can also send an empty Self document to the server to make the server
@ -524,6 +520,19 @@ Additional types for session listing and notifications @@ -524,6 +520,19 @@ Additional types for session listing and notifications
The Alive value is a timestamp integer in milliseconds (unix time).
TurnUpdate
{
"Type": "TurnUpdate",
"TurnUpdate": {
"Turn": {...}
}
}
The server might send a TurnUpdate document at any time. If received by the
client, the Turn details from the Self document have changed and the client
should use the updated data as received in the TurnUpdate document.
User authorization and session authentication
@ -1035,6 +1044,58 @@ File sharing data channel protocol @@ -1035,6 +1044,58 @@ File sharing data channel protocol
message was received.
TURN cedentials and URNs document
TURN example data
{
"username": "turn-username",
"password": "turn-password",
"ttl": 3600
"urls": [
"turn:213.203.211.154:3478?transport=udp",
"turn:213.203.211.154:3479?transport=tcp",
"turns:213.203.211.154:443?transport=tcp"
]
}
TURN data is provided as a mapping of key value pairs like `urls`, `password`
and `username`. See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
and the TURN REST API section in https://github.com/coturn/coturn for details.
In addition, if the service is configured to use a TURN service to provide
credentials for TURN, it can can contain `geo_uri` and `servers` as returned
by the TURN service client. See https://github.com/strukturag/spreed-turnservicecli
for documentation.
TURN example data with TURN service
{
"geo_uri": "https://turnservice.spreed.me/api/v1/turn/geo",
"servers": [
{
"prio": 10,
"urns": [
"turn:213.203.211.154:3478?transport=udp",
"turn:213.203.211.154:3479?transport=tcp",
"turns:213.203.211.154:443?transport=tcp"
],
"id": "zone-id"
}
],
"ttl": 3600,
"username": "turn-username",
"password": "turn-password"
}
The TURN credentials data does expire after a while and needs to be refreshed
before the `ttl` is reached. The `ttl` is the number of seconds from now until
the data expires. To refresh the TURN data, a `Self` document with empty data
should be sent.
All values in the TURN data except the `ttl` are optional.
End of Channeling API.
For latest version of Spreed WebRTC check
@ -1043,4 +1104,4 @@ https://github.com/strukturag/spreed-webrtc @@ -1043,4 +1104,4 @@ https://github.com/strukturag/spreed-webrtc
For questions, contact mailto:opensource@struktur.de.
(c)2014 struktur AG
(c)2016 struktur AG

6
go/channelling/api/api.go

@ -74,7 +74,7 @@ func New(config *channelling.Config, @@ -74,7 +74,7 @@ func New(config *channelling.Config,
func (api *channellingAPI) OnConnect(client *channelling.Client, session *channelling.Session) (interface{}, error) {
api.Unicaster.OnConnect(client, session)
self, err := api.HandleSelf(session)
self, err := api.HandleSelf(client, session)
if err == nil {
api.BusManager.Trigger(channelling.BusManagerConnect, session.Id, "", nil, nil)
}
@ -90,7 +90,7 @@ func (api *channellingAPI) OnIncoming(sender channelling.Sender, session *channe @@ -90,7 +90,7 @@ func (api *channellingAPI) OnIncoming(sender channelling.Sender, session *channe
var pipeline *channelling.Pipeline
switch msg.Type {
case "Self":
return api.HandleSelf(session)
return api.HandleSelf(sender, session)
case "Hello":
if msg.Hello == nil {
return nil, channelling.NewDataError("bad_request", "message did not contain Hello")
@ -138,7 +138,7 @@ func (api *channellingAPI) OnIncoming(sender channelling.Sender, session *channe @@ -138,7 +138,7 @@ func (api *channellingAPI) OnIncoming(sender channelling.Sender, session *channe
return nil, channelling.NewDataError("bad_request", "message did not contain Authentication")
}
return api.HandleAuthentication(session, msg.Authentication.Authentication)
return api.HandleAuthentication(sender, session, msg.Authentication.Authentication)
case "Bye":
if msg.Bye == nil {
log.Println("Received invalid bye message.", msg)

3
go/channelling/api/api_test.go

@ -42,6 +42,9 @@ func (fake *fakeClient) Index() uint64 { @@ -42,6 +42,9 @@ func (fake *fakeClient) Index() uint64 {
func (fake *fakeClient) Send(_ buffercache.Buffer) {
}
func (fake *fakeClient) Outgoing(_ interface{}) {
}
type fakeRoomManager struct {
joinedRoomID string
leftRoomID string

4
go/channelling/api/handle_authentication.go

@ -27,14 +27,14 @@ import ( @@ -27,14 +27,14 @@ import (
"github.com/strukturag/spreed-webrtc/go/channelling"
)
func (api *channellingAPI) HandleAuthentication(session *channelling.Session, st *channelling.SessionToken) (*channelling.DataSelf, error) {
func (api *channellingAPI) HandleAuthentication(sender channelling.Sender, session *channelling.Session, st *channelling.SessionToken) (*channelling.DataSelf, error) {
if err := api.SessionManager.Authenticate(session, st, ""); err != nil {
log.Println("Authentication failed", err, st.Userid, st.Nonce)
return nil, err
}
log.Println("Authentication success", session.Userid())
self, err := api.HandleSelf(session)
self, err := api.HandleSelf(sender, session)
if err == nil {
session.BroadcastStatus()
}

4
go/channelling/api/handle_self.go

@ -27,7 +27,7 @@ import ( @@ -27,7 +27,7 @@ import (
"github.com/strukturag/spreed-webrtc/go/channelling"
)
func (api *channellingAPI) HandleSelf(session *channelling.Session) (*channelling.DataSelf, error) {
func (api *channellingAPI) HandleSelf(sender channelling.Sender, session *channelling.Session) (*channelling.DataSelf, error) {
token, err := api.SessionEncoder.EncodeSessionToken(session)
if err != nil {
log.Println("Error in OnRegister", err)
@ -44,7 +44,7 @@ func (api *channellingAPI) HandleSelf(session *channelling.Session) (*channellin @@ -44,7 +44,7 @@ func (api *channellingAPI) HandleSelf(session *channelling.Session) (*channellin
Token: token,
Version: api.config.Version,
ApiVersion: apiVersion,
Turn: api.TurnDataCreator.CreateTurnData(session),
Turn: api.TurnDataCreator.CreateTurnData(sender, session),
Stun: api.config.StunURIs,
}
api.BusManager.Trigger(channelling.BusManagerSession, session.Id, session.Userid(), nil, nil)

7
go/channelling/client.go

@ -30,6 +30,7 @@ import ( @@ -30,6 +30,7 @@ import (
type Sender interface {
Index() uint64
Send(buffercache.Buffer)
Outgoing(interface{})
}
type Client struct {
@ -50,7 +51,7 @@ func NewClient(codec Codec, api ChannellingAPI, session *Session) *Client { @@ -50,7 +51,7 @@ func NewClient(codec Codec, api ChannellingAPI, session *Session) *Client {
func (client *Client) OnConnect(conn Connection) {
client.Connection = conn
if reply, err := client.ChannellingAPI.OnConnect(client, client.session); err == nil {
client.reply("", reply)
client.Outgoing(reply)
} else {
log.Println("OnConnect error", err)
}
@ -85,6 +86,10 @@ func (client *Client) reply(iid string, m interface{}) { @@ -85,6 +86,10 @@ func (client *Client) reply(iid string, m interface{}) {
}
}
func (client *Client) Outgoing(m interface{}) {
client.reply("", m)
}
func (client *Client) Session() *Session {
return client.session
}

19
go/channelling/data.go

@ -21,6 +21,10 @@ @@ -21,6 +21,10 @@
package channelling
import (
"github.com/strukturag/spreed-turnservicecli/turnservicecli"
)
type DataError struct {
Type string
Code string
@ -91,11 +95,18 @@ type DataSelf struct { @@ -91,11 +95,18 @@ type DataSelf struct {
Stun []string
}
type DataTurnUpdate struct {
Type string
Turn *DataTurn
}
type DataTurn struct {
Username string `json:"username"`
Password string `json:"password"`
Ttl int `json:"ttl"`
Urls []string `json:"urls"`
Username string `json:"username"`
Password string `json:"password"`
Ttl int `json:"ttl"`
Urls []string `json:"urls,omitempty"`
Servers []*turnservicecli.URNsWithID `json:"servers,omitempty"`
GeoURI string `json:"geo_uri,omitempty"`
}
type DataSession struct {

9
go/channelling/hub.go

@ -91,7 +91,7 @@ func (h *hub) ClientInfo(details bool) (clientCount int, sessions map[string]*Da @@ -91,7 +91,7 @@ func (h *hub) ClientInfo(details bool) (clientCount int, sessions map[string]*Da
return
}
func (h *hub) CreateTurnData(session *Session) *DataTurn {
func (h *hub) CreateTurnData(sender Sender, session *Session) *DataTurn {
// Create turn data credentials for shared secret auth with TURN
// server. See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
// and https://code.google.com/p/rfc5766-turn-server/ REST API auth
@ -109,7 +109,12 @@ func (h *hub) CreateTurnData(session *Session) *DataTurn { @@ -109,7 +109,12 @@ func (h *hub) CreateTurnData(session *Session) *DataTurn {
foo.Write([]byte(user))
password := base64.StdEncoding.EncodeToString(foo.Sum(nil))
return &DataTurn{user, password, turnTTL, h.config.TurnURIs}
return &DataTurn{
Username: user,
Password: password,
Ttl: turnTTL,
Urls: h.config.TurnURIs,
}
}
func (h *hub) GetSession(id string) (session *Session, ok bool) {

4
go/channelling/pipeline.go

@ -112,6 +112,10 @@ func (pipeline *Pipeline) Send(b buffercache.Buffer) { @@ -112,6 +112,10 @@ func (pipeline *Pipeline) Send(b buffercache.Buffer) {
// Noop.
}
func (pipeline *Pipeline) Outgoing(m interface{}) {
// Noop.
}
func (pipeline *Pipeline) Index() uint64 {
return 0
}

2
go/channelling/turndata.go

@ -22,5 +22,5 @@ @@ -22,5 +22,5 @@
package channelling
type TurnDataCreator interface {
CreateTurnData(*Session) *DataTurn
CreateTurnData(Sender, *Session) *DataTurn
}

125
go/channelling/turnservice_manager.go

@ -0,0 +1,125 @@ @@ -0,0 +1,125 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2016 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package channelling
import (
"log"
"sync"
"time"
"github.com/strukturag/spreed-turnservicecli/turnservicecli"
)
type TURNServiceManager interface {
TurnDataCreator
}
type turnServiceManager struct {
sync.Mutex
pleaders map[uint64]Sender // Mapping of clients waiting to receive TURN data.
uri string
accessToken string
clientID string
turnService *turnservicecli.TURNService
}
func NewTURNServiceManager(uri string, accessToken string, clientID string) TURNServiceManager {
turnService := turnservicecli.NewTURNService(uri, 0, nil)
mgr := &turnServiceManager{
uri: uri,
accessToken: accessToken,
clientID: clientID,
turnService: turnService,
pleaders: make(map[uint64]Sender),
}
turnService.Open(accessToken, clientID, "")
turnService.BindOnCredentials(mgr.onCredentials)
log.Println("Fetching TURN credentials from service")
go func() {
//time.Sleep(10000 * time.Millisecond)
turnService.Autorefresh(true)
}()
// Wait a bit, to give TURN service some time to populate credentials, so
// we avoid to have send them as an update for fast reconnecting clients.
time.Sleep(500 * time.Millisecond)
if mgr.turnService.Credentials(false) == nil {
log.Println("No TURN credentials from service on startup - extra traffic for clients connecting before credentials have been received")
}
return mgr
}
func (mgr *turnServiceManager) CreateTurnData(sender Sender, session *Session) *DataTurn {
credentials := mgr.turnService.Credentials(false)
turn, err := mgr.turnData(credentials)
if err != nil || turn.Ttl == 0 {
// When no data was return from service, refresh quickly.
mgr.Lock()
mgr.pleaders[sender.Index()] = sender
mgr.Unlock()
// Have client come back early.
turn.Ttl = 300
}
return turn
}
func (mgr *turnServiceManager) turnData(credentials *turnservicecli.CachedCredentialsData) (*DataTurn, error) {
turn := &DataTurn{}
if credentials != nil {
ttl := credentials.TTL()
if ttl > 0 {
turn.Username = credentials.Turn.Username
turn.Password = credentials.Turn.Password
turn.Servers = credentials.Turn.Servers
turn.Ttl = int(ttl)
turn.GeoURI = credentials.Turn.GeoURI
}
}
return turn, nil
}
func (mgr *turnServiceManager) onCredentials(credentials *turnservicecli.CachedCredentialsData, err error) {
if err != nil {
log.Printf("TURN credentials service error: %s\n", err.Error())
return
}
log.Println("Received TURN credentials from service", credentials.Turn.Username)
mgr.Lock()
for _, sender := range mgr.pleaders {
if turn, err := mgr.turnData(credentials); err == nil {
sender.Outgoing(&DataTurnUpdate{
Type: "TurnUpdate",
Turn: turn,
})
}
}
mgr.pleaders = make(map[uint64]Sender) // Clear.
mgr.Unlock()
}

11
server.conf.in

@ -227,3 +227,14 @@ enabled = false @@ -227,3 +227,14 @@ enabled = false
; Example (all rooms below "conference/" are conference rooms):
;^conference/.+ = Conference
;
[turnService]
; To avoid the setup of a self-owned TURN server, a TURN service can be used.
; When a turnServiceURI is set, TurnURIs in the [app] section are ignored.
;turnServiceURI = https://turnservice.spreed.me
; Access token for the TURN service. This is usually required and provided with
; your TURN service subscription.
;turnServiceAccessToken = some-secret-value
; The ClientID can be used to specify additional credentials if required
; by the TURN service. If empty, the value of turnServiceAccessToken is used.
;turnServiceClientID =

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

@ -296,8 +296,18 @@ func runner(runtime phoenix.Runtime) error { @@ -296,8 +296,18 @@ func runner(runtime phoenix.Runtime) error {
return err
}
// TURN data support.
var turnDataCreator channelling.TurnDataCreator
if turnServiceURI, _ := runtime.GetString("turnService", "turnServiceURI"); turnServiceURI != "" {
log.Printf("Using TURN service: %s\n", turnServiceURI)
turnServiceManager := channelling.NewTURNServiceManager(turnServiceURI, runtime.GetStringDefault("turnService", "turnServiceAccessToken", ""), runtime.GetStringDefault("turnService", "turnServiceClientID", ""))
turnDataCreator = turnServiceManager
} else {
turnDataCreator = hub
}
// Create API.
channellingAPI := api.New(config, roomManager, tickets, sessionManager, statsManager, hub, hub, hub, busManager, pipelineManager)
channellingAPI := api.New(config, roomManager, tickets, sessionManager, statsManager, hub, turnDataCreator, hub, busManager, pipelineManager)
apiConsumer.SetChannellingAPI(channellingAPI)
// Start bus.

3
static/js/controllers/appcontroller.js

@ -57,6 +57,9 @@ define(["jquery", "angular", "underscore"], function($, angular, _) { @@ -57,6 +57,9 @@ define(["jquery", "angular", "underscore"], function($, angular, _) {
videoNoiseReduction: false,
preferVideoSendCodecVP9: false
},
turn: {
selectedRegion: null
},
sound: {
incomingMessages: true,
incomingCall: true,

24
static/js/controllers/uicontroller.js

@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
"use strict";
define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function($, _, BigScreen, moment, sjcl, Modernizr) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "localStatus", "dialogs", "rooms", "constraints", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, localStatus, dialogs, rooms, constraints) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "localStatus", "dialogs", "rooms", "constraints", "turnData", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, localStatus, dialogs, rooms, constraints, turnData) {
alertify.dialog.registerCustom({
baseType: 'notify',
@ -364,7 +364,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -364,7 +364,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
mediaStream.api.e.on("received.self", function(event, data) {
$timeout.cancel(ttlTimeout);
safeApply($scope, function(scope) {
scope.id = scope.myid = data.Id;
scope.userid = scope.myuserid = data.Userid ? data.Userid : null;
@ -372,8 +371,8 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -372,8 +371,8 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
});
// Set TURN and STUN data and refresh webrtc settings.
constraints.turn(data.Turn);
constraints.stun(data.Stun);
turnData.update(data.Turn);
$scope.refreshWebrtcSettings();
if (data.Version !== mediaStream.version) {
@ -410,14 +409,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -410,14 +409,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
}
}
// Support to upgrade stuff when ttl was reached.
if (data.Turn.ttl) {
ttlTimeout = $timeout(function() {
console.log("Ttl reached - sending refresh request.");
mediaStream.api.sendSelf();
}, data.Turn.ttl / 100 * 90 * 1000);
}
// Support resurrection shrine.
if (appData.flags.resurrect) {
var resurrection = appData.flags.resurrect;
@ -468,6 +459,12 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -468,6 +459,12 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
});
mediaStream.api.e.on("received.turnUpdate", function(event, data) {
// Set TURN data and refresh webrtc settings.
turnData.update(data.Turn);
$scope.refreshWebrtcSettings();
});
mediaStream.webrtc.e.on("peercall", function(event, peercall) {
// Kill timeout.
@ -775,6 +772,11 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -775,6 +772,11 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
}
});
turnData.e.on("apply", function(event, turnData) {
constraints.turn(turnData);
$scope.refreshWebrtcSettings()
});
$scope.$on("status", function(event, status) {
if (status === "connecting" && dialerEnabled) {
dialer.start();

20
static/js/directives/settings.js

@ -55,7 +55,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -55,7 +55,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
return ["$compile", "mediaStream", function($compile, mediaStream) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', 'userSettingsData', 'constraints', 'appData', '$timeout', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage, userSettingsData, constraints, appData, $timeout) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', 'userSettingsData', 'constraints', 'appData', '$timeout', 'turnData', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage, userSettingsData, constraints, appData, $timeout, turnData) {
$scope.layout.settings = false;
$scope.showAdvancedSettings = true;
@ -63,6 +63,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -63,6 +63,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
$scope.rememberSettings = true;
$scope.desktopNotify = desktopNotify;
$scope.mediaSources = mediaSources;
$scope.turnData = turnData;
$scope.availableLanguages = [{
code: "",
name: translation._("Use browser language")
@ -90,6 +91,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -90,6 +91,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
if (form.$valid && form.$dirty) {
var user = $scope.user;
$scope.update(user);
$scope.turnData.refresh();
if ($scope.rememberSettings) {
userSettingsData.save(user);
localStorage.setItem("mediastream-language", user.settings.language || "");
@ -148,6 +150,9 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -148,6 +150,9 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
});
$scope.refreshWebrtcSettings();
});
if ($scope.user.settings.turn.selectedRegion === null && $scope.turnData.data.geo_uri) {
$scope.user.settings.turn.selectedRegion = "auto";
}
} else if (!showSettings && oldValue) {
$scope.saveSettings();
}
@ -171,8 +176,19 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -171,8 +176,19 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
$timeout($scope.maybeShowSettings);
});
constraints.e.on("refresh", function(event, c) {
turnData.e.on("refresh", function(event, turn) {
var settings = $scope.master.settings;
if (turn && turn.servers) {
var selected = settings.turn.selectedRegion;
if (turn.geo_uri && selected === null) {
selected = "auto";
}
turn.selected = selected;
}
});
constraints.e.on("refresh", function(event, c) {
var settings = $scope.master.settings;
// Assert that selected devices are there.

4
static/js/mediastream/api.js

@ -159,6 +159,10 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { @@ -159,6 +159,10 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
this.sid = data.Sid;
this.e.triggerHandler("received.self", [data]);
break;
case "TurnUpdate":
//console.log("TURN update received", data);
this.e.triggerHandler("received.turnUpdate", [data]);
break;
case "Offer":
//console.log("Offer received", data.To, data.Offer);
this.e.triggerHandler("received.offer", [data.To, data.Offer, data.Type, d.To, d.From]);

2
static/js/services/constraints.js

@ -212,4 +212,4 @@ @@ -212,4 +212,4 @@
}];
});
});

9
static/js/services/services.js

@ -71,7 +71,8 @@ define([ @@ -71,7 +71,8 @@ define([
'services/sandbox',
'services/dummystream',
'services/usermedia',
'services/playpromise'], function(_,
'services/playpromise',
'services/turndata'], function(_,
desktopNotify,
playSound,
safeApply,
@ -120,7 +121,8 @@ mediaDevices, @@ -120,7 +121,8 @@ mediaDevices,
sandbox,
dummyStream,
userMedia,
playPromise) {
playPromise,
turnData) {
var services = {
desktopNotify: desktopNotify,
@ -171,7 +173,8 @@ playPromise) { @@ -171,7 +173,8 @@ playPromise) {
sandbox: sandbox,
dummyStream: dummyStream,
userMedia: userMedia,
playPromise: playPromise
playPromise: playPromise,
turnData: turnData
};
var initialize = function(angModule) {

180
static/js/services/turndata.js

@ -0,0 +1,180 @@ @@ -0,0 +1,180 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2016 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(["jquery"], function($) {
var geoRequestTimeout = 30000; // Timeout for geo requests in milliseconds.
var geoFastRetryTimeout = 45000; // Refresh timer in milliseconds, after which GEO requests should be retried if failed before.
var refreshPercentile = 90; // Percent of the TTL when TURN credentials should be refreshed.
// turnData
return ["$timeout", "$http", "api", "randomGen", "appData", function($timeout, $http, api, randomGen, appData) {
var ttlTimeout = null;
var geoRefresh = null;
var geoPreferred = null;
var service = this;
service.e = $({});
service.data = {};
service.apply = function() {
var turn = service.data;
var turnData = {
"username": turn.username,
"password": turn.password,
"ttl": turn.ttl
};
if (turn && turn.servers) {
// Multiple options, need to sort and use settings.
if (!turn.serverMap) {
var servers = {};
turn.servers.sort(function(a, b) {
servers[a.id] = a;
servers[b.id] = b;
return (a.prio > b.prio) ? 1 : ((a.prio < b.prio) ? -1 : 0);
});
turn.first = turn.servers[0];
if (turn.geo_uri) {
turn.servers.unshift({
"id": "auto"
})
}
turn.serverMap = servers;
}
var urls;
if (turn.preferred) {
for (var i=0; i<turn.preferred.length; i++) {
if (turn.serverMap.hasOwnProperty(turn.preferred[i])) {
urls = turn.serverMap[turn.preferred[i]].urns;
break;
}
}
}
if (!urls && turn.first) {
urls = turn.first.urns;
}
turnData.urls = urls;
} else if (turn && turn.urls) {
// Simple case, single region.
turnData.urls = turn.urls
} else {
// Unknown data.
turnData.urls = [];
}
console.log("TURN servers selected: ", turnData.urls, turn.preferred || null);
service.e.triggerHandler("apply", [turnData]);
return turnData;
};
service.refresh = function(withGeo) {
$timeout.cancel(geoRefresh);
var turn = service.data;
service.e.triggerHandler("refresh", [turn]);
if (turn.selected === "auto" && turn.geo_uri) {
if (geoPreferred !== null) {
// Use existing data.
turn.preferred = geoPreferred;
} else {
if (!withGeo) {
// Avoid triggering spurious GEO request for fast updates.
geoRefresh = $timeout(function() {
service.refresh(true);
}, 1000);
return;
}
// Run Geo request.
var nonce = randomGen.random({hex: true});
$http({
method: "POST",
url: turn.geo_uri,
headers: {"Content-Type": "application/x-www-form-urlencoded"},
data: "nonce="+encodeURIComponent(nonce)+"&username="+encodeURIComponent(turn.username)+"&password="+encodeURIComponent(turn.password),
timeout: geoRequestTimeout
}).then(function(response) {
// success
if (turn !== service.data) {
// No longer our data.
return;
}
if (response.status === 200) {
var data = response.data;
if (data.success && data.nonce === nonce) {
geoPreferred = turn.preferred = data.geo.prefer;
console.log("TURN GEO auto selected: ", turn.preferred);
service.apply();
}
}
}, function(response) {
// failed
if (turn !== service.data) {
// No longer our data.
return;
}
console.warn("TURN GEO failed:", response.status, response);
$timeout.cancel(ttlTimeout);
ttlTimeout = $timeout(function() {
// Fast retry.
console.warn("TURN GEO failed - refreshing early.")
api.sendSelf();
}, geoFastRetryTimeout)
})
}
} else {
// Set directly.
turn.preferred = [];
if (turn.selected) {
turn.preferred.push(turn.selected);
}
}
service.apply();
};
service.update = function(turn) {
$timeout.cancel(ttlTimeout);
if (service.data && service.data.preferred) {
// Keep preferred list if there is one.
turn.preferred = service.data.preferred;
}
service.data = turn;
service.refresh()
// Support to refresh TURN data when ttl was reached.
if (turn.ttl) {
ttlTimeout = $timeout(function() {
console.log("TURN TTL reached - sending refresh request.");
api.sendSelf();
}, turn.ttl * 0.01 * refreshPercentile * 1000);
}
};
service.cancel = function() {
$timeout.cancel(ttlTimeout);
}
appData.e.on("userSettingsLoaded", service.refresh);
return service;
}]
})

10
static/partials/settings.html

@ -239,6 +239,14 @@ @@ -239,6 +239,14 @@
</div>
</div>
<div class="form-group" ng-show="turnData.data.servers">
<label class="col-xs-4 control-label">{{_('TURN region')}}</label>
<div class="col-xs-8">
<select class="form-control" ng-model="user.settings.turn.selectedRegion" ng-options="region.id as region.id for region in turnData.data.servers"></select>
<span class="help-block">{{_('Geographic region for TURN service.')}}</span>
</div>
</div>
<div ng-show="user.settings.experimental.enabled">
<div class="form-group" ng-show="supported.constraints.chrome">
@ -318,4 +326,4 @@ @@ -318,4 +326,4 @@
</fieldset>
</div>
</div>
</div>
</div>

Loading…
Cancel
Save