Browse Source

Merge pull request #346 from strukturag/turnservice-support

Implement TURN service client
pull/374/head
Simon Eisenmann 10 years ago committed by GitHub
parent
commit
7aa6c204ed
  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
github.com/strukturag/httputils git afbf05c71ac03ee7989c96d033a9571ba4ded468 2014-07-02T01:35:33Z 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/phoenix git 31b7f25f4815e6e0b8e7c4010f6e9a71c4165b19 2016-06-01T11:34:58Z
github.com/strukturag/sloth git 74a8bcf67368de59baafe5d3e17aee9875564cfc 2015-04-22T08:59:42Z 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
Version : Server version number. Use this to detect server upgrades. Version : Server version number. Use this to detect server upgrades.
ApiVersion : Server channeling API base version. Use this version to select ApiVersion : Server channeling API base version. Use this version to select
client side compatibility with the connected server. client side compatibility with the connected server.
Turn : Mapping (interface{}) to contain TURN server details, like Turn : Mapping (interface{}) to contain TURN server details. See
urls, password and username. See TURN credentials and URNs section below for specification.
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.
Stun : Array with STUN server URLs. Stun : Array with STUN server URLs.
You can also send an empty Self document to the server to make the server 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). 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 User authorization and session authentication
@ -1035,6 +1044,58 @@ File sharing data channel protocol
message was received. 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. End of Channeling API.
For latest version of Spreed WebRTC check For latest version of Spreed WebRTC check
@ -1043,4 +1104,4 @@ https://github.com/strukturag/spreed-webrtc
For questions, contact mailto:opensource@struktur.de. 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,
func (api *channellingAPI) OnConnect(client *channelling.Client, session *channelling.Session) (interface{}, error) { func (api *channellingAPI) OnConnect(client *channelling.Client, session *channelling.Session) (interface{}, error) {
api.Unicaster.OnConnect(client, session) api.Unicaster.OnConnect(client, session)
self, err := api.HandleSelf(session) self, err := api.HandleSelf(client, session)
if err == nil { if err == nil {
api.BusManager.Trigger(channelling.BusManagerConnect, session.Id, "", nil, 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 var pipeline *channelling.Pipeline
switch msg.Type { switch msg.Type {
case "Self": case "Self":
return api.HandleSelf(session) return api.HandleSelf(sender, session)
case "Hello": case "Hello":
if msg.Hello == nil { if msg.Hello == nil {
return nil, channelling.NewDataError("bad_request", "message did not contain Hello") 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 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": case "Bye":
if msg.Bye == nil { if msg.Bye == nil {
log.Println("Received invalid bye message.", msg) log.Println("Received invalid bye message.", msg)

3
go/channelling/api/api_test.go

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

4
go/channelling/api/handle_authentication.go

@ -27,14 +27,14 @@ import (
"github.com/strukturag/spreed-webrtc/go/channelling" "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 { if err := api.SessionManager.Authenticate(session, st, ""); err != nil {
log.Println("Authentication failed", err, st.Userid, st.Nonce) log.Println("Authentication failed", err, st.Userid, st.Nonce)
return nil, err return nil, err
} }
log.Println("Authentication success", session.Userid()) log.Println("Authentication success", session.Userid())
self, err := api.HandleSelf(session) self, err := api.HandleSelf(sender, session)
if err == nil { if err == nil {
session.BroadcastStatus() session.BroadcastStatus()
} }

4
go/channelling/api/handle_self.go

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

7
go/channelling/client.go

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

19
go/channelling/data.go

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

9
go/channelling/hub.go

@ -91,7 +91,7 @@ func (h *hub) ClientInfo(details bool) (clientCount int, sessions map[string]*Da
return 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 // Create turn data credentials for shared secret auth with TURN
// server. See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 // 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 // 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)) foo.Write([]byte(user))
password := base64.StdEncoding.EncodeToString(foo.Sum(nil)) 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) { 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) {
// Noop. // Noop.
} }
func (pipeline *Pipeline) Outgoing(m interface{}) {
// Noop.
}
func (pipeline *Pipeline) Index() uint64 { func (pipeline *Pipeline) Index() uint64 {
return 0 return 0
} }

2
go/channelling/turndata.go

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

125
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 <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
; Example (all rooms below "conference/" are conference rooms): ; Example (all rooms below "conference/" are conference rooms):
;^conference/.+ = Conference ;^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 {
return err 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. // 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) apiConsumer.SetChannellingAPI(channellingAPI)
// Start bus. // Start bus.

3
static/js/controllers/appcontroller.js

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

24
static/js/controllers/uicontroller.js

@ -22,7 +22,7 @@
"use strict"; "use strict";
define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function($, _, BigScreen, moment, sjcl, Modernizr) { 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({ alertify.dialog.registerCustom({
baseType: 'notify', baseType: 'notify',
@ -364,7 +364,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
mediaStream.api.e.on("received.self", function(event, data) { mediaStream.api.e.on("received.self", function(event, data) {
$timeout.cancel(ttlTimeout);
safeApply($scope, function(scope) { safeApply($scope, function(scope) {
scope.id = scope.myid = data.Id; scope.id = scope.myid = data.Id;
scope.userid = scope.myuserid = data.Userid ? data.Userid : null; 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. // Set TURN and STUN data and refresh webrtc settings.
constraints.turn(data.Turn);
constraints.stun(data.Stun); constraints.stun(data.Stun);
turnData.update(data.Turn);
$scope.refreshWebrtcSettings(); $scope.refreshWebrtcSettings();
if (data.Version !== mediaStream.version) { 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. // Support resurrection shrine.
if (appData.flags.resurrect) { if (appData.flags.resurrect) {
var resurrection = 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) { mediaStream.webrtc.e.on("peercall", function(event, peercall) {
// Kill timeout. // 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) { $scope.$on("status", function(event, status) {
if (status === "connecting" && dialerEnabled) { if (status === "connecting" && dialerEnabled) {
dialer.start(); dialer.start();

20
static/js/directives/settings.js

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

4
static/js/mediastream/api.js

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

9
static/js/services/services.js

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

180
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 <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 @@
</div> </div>
</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 ng-show="user.settings.experimental.enabled">
<div class="form-group" ng-show="supported.constraints.chrome"> <div class="form-group" ng-show="supported.constraints.chrome">
@ -318,4 +326,4 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save