From e18dd5c93382f4e15fcd97e921a680cd2494622f Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Wed, 24 Aug 2016 17:02:07 +0200 Subject: [PATCH] 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). --- dependencies.tsv | 1 + doc/CHANNELING-API.txt | 75 +++++++- go/channelling/api/api.go | 6 +- go/channelling/api/api_test.go | 3 + go/channelling/api/handle_authentication.go | 4 +- go/channelling/api/handle_self.go | 4 +- go/channelling/client.go | 7 +- go/channelling/data.go | 19 ++- go/channelling/hub.go | 9 +- go/channelling/pipeline.go | 4 + go/channelling/turndata.go | 2 +- go/channelling/turnservice_manager.go | 125 ++++++++++++++ server.conf.in | 11 ++ src/app/spreed-webrtc-server/main.go | 12 +- static/js/controllers/appcontroller.js | 3 + static/js/controllers/uicontroller.js | 24 +-- static/js/directives/settings.js | 20 ++- static/js/mediastream/api.js | 4 + static/js/services/constraints.js | 2 +- static/js/services/services.js | 9 +- static/js/services/turndata.js | 180 ++++++++++++++++++++ static/partials/settings.html | 10 +- 22 files changed, 493 insertions(+), 41 deletions(-) create mode 100644 go/channelling/turnservice_manager.go create mode 100644 static/js/services/turndata.js diff --git a/dependencies.tsv b/dependencies.tsv index 9b46c45c..50801999 100644 --- a/dependencies.tsv +++ b/dependencies.tsv @@ -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 diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt index 8b64f148..4848b9ba 100644 --- a/doc/CHANNELING-API.txt +++ b/doc/CHANNELING-API.txt @@ -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 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 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 For questions, contact mailto:opensource@struktur.de. -(c)2014 struktur AG +(c)2016 struktur AG diff --git a/go/channelling/api/api.go b/go/channelling/api/api.go index 854dcb90..e75644d7 100644 --- a/go/channelling/api/api.go +++ b/go/channelling/api/api.go @@ -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 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 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) diff --git a/go/channelling/api/api_test.go b/go/channelling/api/api_test.go index 90787787..6e1745a8 100644 --- a/go/channelling/api/api_test.go +++ b/go/channelling/api/api_test.go @@ -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 diff --git a/go/channelling/api/handle_authentication.go b/go/channelling/api/handle_authentication.go index 5487d05b..6a84d903 100644 --- a/go/channelling/api/handle_authentication.go +++ b/go/channelling/api/handle_authentication.go @@ -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() } diff --git a/go/channelling/api/handle_self.go b/go/channelling/api/handle_self.go index 1c5e118d..10a87575 100644 --- a/go/channelling/api/handle_self.go +++ b/go/channelling/api/handle_self.go @@ -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 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) diff --git a/go/channelling/client.go b/go/channelling/client.go index c9b604a5..168b1489 100644 --- a/go/channelling/client.go +++ b/go/channelling/client.go @@ -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 { 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{}) { } } +func (client *Client) Outgoing(m interface{}) { + client.reply("", m) +} + func (client *Client) Session() *Session { return client.session } diff --git a/go/channelling/data.go b/go/channelling/data.go index 960231b7..65a4aba8 100644 --- a/go/channelling/data.go +++ b/go/channelling/data.go @@ -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 { 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 { diff --git a/go/channelling/hub.go b/go/channelling/hub.go index 057fcb60..913c01bd 100644 --- a/go/channelling/hub.go +++ b/go/channelling/hub.go @@ -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 { 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) { diff --git a/go/channelling/pipeline.go b/go/channelling/pipeline.go index 6f57303c..edca9580 100644 --- a/go/channelling/pipeline.go +++ b/go/channelling/pipeline.go @@ -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 } diff --git a/go/channelling/turndata.go b/go/channelling/turndata.go index 71002494..c6991ee9 100644 --- a/go/channelling/turndata.go +++ b/go/channelling/turndata.go @@ -22,5 +22,5 @@ package channelling type TurnDataCreator interface { - CreateTurnData(*Session) *DataTurn + CreateTurnData(Sender, *Session) *DataTurn } diff --git a/go/channelling/turnservice_manager.go b/go/channelling/turnservice_manager.go new file mode 100644 index 00000000..66e20614 --- /dev/null +++ b/go/channelling/turnservice_manager.go @@ -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 . + * + */ + +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() +} diff --git a/server.conf.in b/server.conf.in index 08365caf..369f3e3c 100644 --- a/server.conf.in +++ b/server.conf.in @@ -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 = diff --git a/src/app/spreed-webrtc-server/main.go b/src/app/spreed-webrtc-server/main.go index b1ebbc4e..44b83dcd 100644 --- a/src/app/spreed-webrtc-server/main.go +++ b/src/app/spreed-webrtc-server/main.go @@ -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. diff --git a/static/js/controllers/appcontroller.js b/static/js/controllers/appcontroller.js index 65e10948..d1b07b74 100644 --- a/static/js/controllers/appcontroller.js +++ b/static/js/controllers/appcontroller.js @@ -57,6 +57,9 @@ define(["jquery", "angular", "underscore"], function($, angular, _) { videoNoiseReduction: false, preferVideoSendCodecVP9: false }, + turn: { + selectedRegion: null + }, sound: { incomingMessages: true, incomingCall: true, diff --git a/static/js/controllers/uicontroller.js b/static/js/controllers/uicontroller.js index 89a04dee..595dbd2c 100644 --- a/static/js/controllers/uicontroller.js +++ b/static/js/controllers/uicontroller.js @@ -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 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 }); // 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 } } - // 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 }); + 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 } }); + turnData.e.on("apply", function(event, turnData) { + constraints.turn(turnData); + $scope.refreshWebrtcSettings() + }); + $scope.$on("status", function(event, status) { if (status === "connecting" && dialerEnabled) { dialer.start(); diff --git a/static/js/directives/settings.js b/static/js/directives/settings.js index 43c9f9a2..d1d30a9d 100644 --- a/static/js/directives/settings.js +++ b/static/js/directives/settings.js @@ -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 $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 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 }); $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 $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. diff --git a/static/js/mediastream/api.js b/static/js/mediastream/api.js index 058b523c..ee5e576d 100644 --- a/static/js/mediastream/api.js +++ b/static/js/mediastream/api.js @@ -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]); diff --git a/static/js/services/constraints.js b/static/js/services/constraints.js index 99dbc27d..80061204 100644 --- a/static/js/services/constraints.js +++ b/static/js/services/constraints.js @@ -212,4 +212,4 @@ }]; - }); \ No newline at end of file + }); diff --git a/static/js/services/services.js b/static/js/services/services.js index a42445f9..b32f196a 100644 --- a/static/js/services/services.js +++ b/static/js/services/services.js @@ -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, sandbox, dummyStream, userMedia, -playPromise) { +playPromise, +turnData) { var services = { desktopNotify: desktopNotify, @@ -171,7 +173,8 @@ playPromise) { sandbox: sandbox, dummyStream: dummyStream, userMedia: userMedia, - playPromise: playPromise + playPromise: playPromise, + turnData: turnData }; var initialize = function(angModule) { diff --git a/static/js/services/turndata.js b/static/js/services/turndata.js new file mode 100644 index 00000000..22f11a54 --- /dev/null +++ b/static/js/services/turndata.js @@ -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 . + * + */ + +"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 +
+ +
+ + {{_('Geographic region for TURN service.')}} +
+
+
@@ -318,4 +326,4 @@
- \ No newline at end of file +