Browse Source

Merge pull request #284 from fancycode/serverside_conferences

Serverside conferences
pull/293/head
Joachim Bauch 9 years ago
parent
commit
3d2c6b6f4c
  1. 2
      dependencies.tsv
  2. 6
      go/channelling/api.go
  3. 9
      go/channelling/api/api.go
  4. 14
      go/channelling/api/api_test.go
  5. 5
      go/channelling/api/handle_conference.go
  6. 6
      go/channelling/api/handle_hello.go
  7. 23
      go/channelling/api/handle_room.go
  8. 4
      go/channelling/client.go
  9. 48
      go/channelling/config.go
  10. 4
      go/channelling/pipeline.go
  11. 17
      go/channelling/room_manager.go
  12. 24
      go/channelling/room_manager_test.go
  13. 29
      go/channelling/roomworker.go
  14. 6
      go/channelling/roomworker_test.go
  15. 42
      go/channelling/server/config.go
  16. 4
      go/channelling/server/pipelines.go
  17. 14
      server.conf.in
  18. 5
      src/app/spreed-webrtc-server/main.go
  19. 24
      src/styles/global/_base.scss
  20. 87
      static/js/controllers/uicontroller.js
  21. 46
      static/js/directives/audiovideo.js
  22. 28
      static/js/mediastream/peerconference.js
  23. 89
      static/js/mediastream/webrtc.js
  24. 4
      static/js/services/rooms.js
  25. 4
      static/partials/audiovideo.html
  26. 6
      static/partials/statusmessage.html
  27. 4
      static/partials/ui.html

2
dependencies.tsv

@ -8,5 +8,5 @@ github.com/nats-io/nats git 355b5b97e0842dc94f1106729aa88e33e06317ca 2015-12-09T @@ -8,5 +8,5 @@ github.com/nats-io/nats git 355b5b97e0842dc94f1106729aa88e33e06317ca 2015-12-09T
github.com/satori/go.uuid git afe1e2ddf0f05b7c29d388a3f8e76cb15c2231ca 2015-06-15T02:45:37Z
github.com/strukturag/goacceptlanguageparser git 68066e68c2940059aadc6e19661610cf428b6647 2014-02-13T13:31:23Z
github.com/strukturag/httputils git afbf05c71ac03ee7989c96d033a9571ba4ded468 2014-07-02T01:35:33Z
github.com/strukturag/phoenix git c3429c4e93588d848606263a7f96f91c90e43178 2016-03-02T12:52:52Z
github.com/strukturag/phoenix git 31b7f25f4815e6e0b8e7c4010f6e9a71c4165b19 2016-06-01T11:34:58Z
github.com/strukturag/sloth git 74a8bcf67368de59baafe5d3e17aee9875564cfc 2015-04-22T08:59:42Z

1 github.com/dlintw/goconf git dcc070983490608a14480e3bf943bad464785df5 2012-02-28T08:26:10Z
8 github.com/satori/go.uuid git afe1e2ddf0f05b7c29d388a3f8e76cb15c2231ca 2015-06-15T02:45:37Z
9 github.com/strukturag/goacceptlanguageparser git 68066e68c2940059aadc6e19661610cf428b6647 2014-02-13T13:31:23Z
10 github.com/strukturag/httputils git afbf05c71ac03ee7989c96d033a9571ba4ded468 2014-07-02T01:35:33Z
11 github.com/strukturag/phoenix git c3429c4e93588d848606263a7f96f91c90e43178 31b7f25f4815e6e0b8e7c4010f6e9a71c4165b19 2016-03-02T12:52:52Z 2016-06-01T11:34:58Z
12 github.com/strukturag/sloth git 74a8bcf67368de59baafe5d3e17aee9875564cfc 2015-04-22T08:59:42Z

6
go/channelling/api.go

@ -21,10 +21,16 @@ @@ -21,10 +21,16 @@
package channelling
const (
RoomTypeConference = "Conference"
RoomTypeRoom = "Room"
)
type ChannellingAPI interface {
OnConnect(*Client, *Session) (interface{}, error)
OnDisconnect(*Client, *Session)
OnIncoming(Sender, *Session, *DataIncoming) (interface{}, error)
OnIncomingProcessed(Sender, *Session, *DataIncoming, interface{}, error)
}
type ChannellingAPIConsumer interface {

9
go/channelling/api/api.go

@ -194,3 +194,12 @@ func (api *channellingAPI) OnIncoming(sender channelling.Sender, session *channe @@ -194,3 +194,12 @@ func (api *channellingAPI) OnIncoming(sender channelling.Sender, session *channe
return nil, nil
}
func (api *channellingAPI) OnIncomingProcessed(sender channelling.Sender, session *channelling.Session, msg *channelling.DataIncoming, reply interface{}, err error) {
switch msg.Type {
case "Hello":
api.HelloProcessed(sender, session, msg, reply, err)
case "Room":
api.RoomProcessed(sender, session, msg, reply, err)
}
}

14
go/channelling/api/api_test.go

@ -77,11 +77,15 @@ func (fake *fakeRoomManager) UpdateRoom(_ *channelling.Session, _ *channelling.D @@ -77,11 +77,15 @@ func (fake *fakeRoomManager) UpdateRoom(_ *channelling.Session, _ *channelling.D
func (fake *fakeRoomManager) MakeRoomID(roomName, roomType string) string {
if roomType == "" {
roomType = "Room"
roomType = channelling.RoomTypeRoom
}
return fmt.Sprintf("%s:%s", roomType, roomName)
}
func (fake *fakeRoomManager) Get(roomID string) (room channelling.RoomWorker, ok bool) {
return nil, false
}
func NewTestChannellingAPI() (channelling.ChannellingAPI, *fakeClient, *channelling.Session, *fakeRoomManager) {
apiConsumer := channelling.NewChannellingAPIConsumer()
client, roomManager := &fakeClient{}, &fakeRoomManager{}
@ -94,7 +98,7 @@ func NewTestChannellingAPI() (channelling.ChannellingAPI, *fakeClient, *channell @@ -94,7 +98,7 @@ func NewTestChannellingAPI() (channelling.ChannellingAPI, *fakeClient, *channell
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing.T) {
roomID, roomName, ua := "Room:foobar", "foobar", "unit tests"
roomID, roomName, ua := channelling.RoomTypeRoom+":foobar", "foobar", "unit tests"
api, client, session, roomManager := NewTestChannellingAPI()
api.OnIncoming(client, session, &channelling.DataIncoming{Type: "Hello", Hello: &channelling.DataHello{Id: roomName, Ua: ua}})
@ -118,7 +122,7 @@ func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing @@ -118,7 +122,7 @@ func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_LeavesAnyPreviouslyJoinedRooms(t *testing.T) {
roomID, roomName := "Room:foobar", "foobar"
roomID, roomName := channelling.RoomTypeRoom+":foobar", "foobar"
api, client, session, roomManager := NewTestChannellingAPI()
api.OnIncoming(client, session, &channelling.DataIncoming{Type: "Hello", Hello: &channelling.DataHello{Id: roomName}})
@ -200,7 +204,7 @@ func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpda @@ -200,7 +204,7 @@ func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpda
t.Fatalf("Unexpected error %v", err)
}
reply, err := api.OnIncoming(client, session, &channelling.DataIncoming{Type: "Room", Room: &channelling.DataRoom{Name: roomName}})
reply, err := api.OnIncoming(client, session, &channelling.DataIncoming{Type: channelling.RoomTypeRoom, Room: &channelling.DataRoom{Name: roomName}})
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
@ -232,7 +236,7 @@ func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAnErrorIfUpdatingThe @@ -232,7 +236,7 @@ func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAnErrorIfUpdatingThe
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
_, err = api.OnIncoming(client, session, &channelling.DataIncoming{Type: "Room", Room: &channelling.DataRoom{Name: roomName}})
_, err = api.OnIncoming(client, session, &channelling.DataIncoming{Type: channelling.RoomTypeRoom, Room: &channelling.DataRoom{Name: roomName}})
assertDataError(t, err, "a_room_error")
}

5
go/channelling/api/handle_conference.go

@ -28,6 +28,11 @@ import ( @@ -28,6 +28,11 @@ import (
)
func (api *channellingAPI) HandleConference(session *channelling.Session, conference *channelling.DataConference) {
if room, ok := api.RoomStatusManager.Get(session.Roomid); ok && room.GetType() == channelling.RoomTypeConference {
log.Println("Refusing client-side conference update for server-managed conferences.")
return
}
// Check conference maximum size.
if len(conference.Conference) > maxConferenceSize {
log.Println("Refusing to create conference above limit.", len(conference.Conference))

6
go/channelling/api/handle_hello.go

@ -46,3 +46,9 @@ func (api *channellingAPI) HandleHello(session *channelling.Session, hello *chan @@ -46,3 +46,9 @@ func (api *channellingAPI) HandleHello(session *channelling.Session, hello *chan
Users: api.RoomStatusManager.RoomUsers(session),
}, nil
}
func (api *channellingAPI) HelloProcessed(sender channelling.Sender, session *channelling.Session, msg *channelling.DataIncoming, reply interface{}, err error) {
if err == nil {
api.SendConferenceRoomUpdate(session)
}
}

23
go/channelling/api/handle_room.go

@ -33,3 +33,26 @@ func (api *channellingAPI) HandleRoom(session *channelling.Session, room *channe @@ -33,3 +33,26 @@ func (api *channellingAPI) HandleRoom(session *channelling.Session, room *channe
return room, err
}
func (api *channellingAPI) RoomProcessed(sender channelling.Sender, session *channelling.Session, msg *channelling.DataIncoming, reply interface{}, err error) {
if err == nil {
api.SendConferenceRoomUpdate(session)
}
}
func (api *channellingAPI) SendConferenceRoomUpdate(session *channelling.Session) {
// If user joined a server-managed conference room, send list of session ids to all participants.
if room, ok := api.RoomStatusManager.Get(session.Roomid); ok && room.GetType() == channelling.RoomTypeConference {
if sessionids := room.SessionIDs(); len(sessionids) > 1 {
cid := session.Roomid
session.Broadcaster.Broadcast("", session.Roomid, &channelling.DataOutgoing{
To: cid,
Data: &channelling.DataConference{
Type: "Conference",
Id: cid,
Conference: sessionids,
},
})
}
}
}

4
go/channelling/client.go

@ -68,11 +68,13 @@ func (client *Client) OnText(b buffercache.Buffer) { @@ -68,11 +68,13 @@ func (client *Client) OnText(b buffercache.Buffer) {
return
}
if reply, err := client.ChannellingAPI.OnIncoming(client, client.session, incoming); err != nil {
var reply interface{}
if reply, err = client.ChannellingAPI.OnIncoming(client, client.session, incoming); err != nil {
client.reply(incoming.Iid, err)
} else if reply != nil {
client.reply(incoming.Iid, reply)
}
client.ChannellingAPI.OnIncomingProcessed(client, client.session, incoming, reply, err)
}
func (client *Client) reply(iid string, m interface{}) {

48
go/channelling/config.go

@ -2,32 +2,34 @@ package channelling @@ -2,32 +2,34 @@ package channelling
import (
"net/http"
"regexp"
)
type Config struct {
Title string // Title
Ver string `json:"-"` // Version (not exported to Javascript)
S string // Static URL prefix with version
B string // Base URL
Token string // Server token
Renegotiation bool // Renegotiation flag
StunURIs []string // STUN server URIs
TurnURIs []string // TURN server URIs
Tokens bool // True when we got a tokens file
Version string // Server version number
UsersEnabled bool // Flag if users are enabled
UsersAllowRegistration bool // Flag if users can register
UsersMode string // Users mode string
DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load
AuthorizeRoomCreation bool // Whether a user account is required to create rooms
AuthorizeRoomJoin bool // Whether a user account is required to join rooms
Modules []string // List of enabled modules
ModulesTable map[string]bool `json:"-"` // Map of enabled modules
GlobalRoomID string `json:"-"` // Id of the global room (not exported to Javascript)
ContentSecurityPolicy string `json:"-"` // HTML content security policy
ContentSecurityPolicyReportOnly string `json:"-"` // HTML content security policy in report only mode
RoomTypeDefault string `json:"-"` // New rooms default to this type
Title string // Title
Ver string `json:"-"` // Version (not exported to Javascript)
S string // Static URL prefix with version
B string // Base URL
Token string // Server token
Renegotiation bool // Renegotiation flag
StunURIs []string // STUN server URIs
TurnURIs []string // TURN server URIs
Tokens bool // True when we got a tokens file
Version string // Server version number
UsersEnabled bool // Flag if users are enabled
UsersAllowRegistration bool // Flag if users can register
UsersMode string // Users mode string
DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load
AuthorizeRoomCreation bool // Whether a user account is required to create rooms
AuthorizeRoomJoin bool // Whether a user account is required to join rooms
Modules []string // List of enabled modules
ModulesTable map[string]bool `json:"-"` // Map of enabled modules
GlobalRoomID string `json:"-"` // Id of the global room (not exported to Javascript)
ContentSecurityPolicy string `json:"-"` // HTML content security policy
ContentSecurityPolicyReportOnly string `json:"-"` // HTML content security policy in report only mode
RoomTypeDefault string `json:"-"` // New rooms default to this type
RoomTypes map[*regexp.Regexp]string `json:"-"` // Map of regular expression -> room type
}
func (config *Config) WithModule(m string) bool {

4
go/channelling/pipeline.go

@ -72,11 +72,13 @@ func (pipeline *Pipeline) receive() { @@ -72,11 +72,13 @@ func (pipeline *Pipeline) receive() {
// TODO(longsleep): Call to ToSession() should be avoided because it locks.
api := pipeline.PipelineManager.GetChannellingAPI()
for data := range pipeline.recvQueue {
_, err := api.OnIncoming(nil, pipeline.ToSession(), data)
session := pipeline.ToSession()
reply, err := api.OnIncoming(nil, session, data)
if err != nil {
// TODO(longsleep): Handle reply and error.
log.Println("Pipeline receive incoming error", err)
}
api.OnIncomingProcessed(nil, session, data, reply, err)
}
log.Println("Pipeline receive done")
}

17
go/channelling/room_manager.go

@ -33,6 +33,7 @@ type RoomStatusManager interface { @@ -33,6 +33,7 @@ type RoomStatusManager interface {
LeaveRoom(roomID, sessionID string)
UpdateRoom(*Session, *DataRoom) (*DataRoom, error)
MakeRoomID(roomName, roomType string) string
Get(roomID string) (room RoomWorker, ok bool)
}
type Broadcaster interface {
@ -169,6 +170,10 @@ func (rooms *roomManager) GetOrCreate(roomID, roomName, roomType string, credent @@ -169,6 +170,10 @@ func (rooms *roomManager) GetOrCreate(roomID, roomName, roomType string, credent
return room, nil
}
if roomType == "" {
roomType = rooms.getConfiguredRoomType(roomName)
}
rooms.Lock()
// Need to re-check, another thread might have created the room
// while we waited for the lock.
@ -214,8 +219,18 @@ func (rooms *roomManager) GlobalUsers() []*roomUser { @@ -214,8 +219,18 @@ func (rooms *roomManager) GlobalUsers() []*roomUser {
func (rooms *roomManager) MakeRoomID(roomName, roomType string) string {
if roomType == "" {
roomType = rooms.RoomTypeDefault
roomType = rooms.getConfiguredRoomType(roomName)
}
return fmt.Sprintf("%s:%s", roomType, roomName)
}
func (rooms *roomManager) getConfiguredRoomType(roomName string) string {
for re, roomType := range rooms.RoomTypes {
if re.MatchString(roomName) {
return roomType
}
}
return rooms.RoomTypeDefault
}

24
go/channelling/room_manager_test.go

@ -23,11 +23,13 @@ package channelling @@ -23,11 +23,13 @@ package channelling
import (
"testing"
"github.com/strukturag/spreed-webrtc/go/channelling"
)
func NewTestRoomManager() (RoomManager, *Config) {
config := &Config{
RoomTypeDefault: "Room",
RoomTypeDefault: channelling.RoomTypeRoom,
}
return NewRoomManager(config, nil), config
}
@ -38,16 +40,16 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenCreat @@ -38,16 +40,16 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenCreat
config.AuthorizeRoomCreation = true
unauthenticatedSession := &Session{}
_, err := roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil)
_, err := roomManager.JoinRoom(channelling.RoomTypeRoom+":foo", "foo", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
assertDataError(t, err, "room_join_requires_account")
authenticatedSession := &Session{userid: "9870457"}
_, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, authenticatedSession, true, nil)
_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":foo", "foo", channelling.RoomTypeRoom, nil, authenticatedSession, true, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while authenticated", err)
}
_, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil)
_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":foo", "foo", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while unauthenticated", err)
}
@ -59,16 +61,16 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenJoinR @@ -59,16 +61,16 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenJoinR
config.AuthorizeRoomJoin = true
unauthenticatedSession := &Session{}
_, err := roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil)
_, err := roomManager.JoinRoom(channelling.RoomTypeRoom+":foo", "foo", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
assertDataError(t, err, "room_join_requires_account")
authenticatedSession := &Session{userid: "9870457"}
_, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, authenticatedSession, true, nil)
_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":foo", "foo", channelling.RoomTypeRoom, nil, authenticatedSession, true, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while authenticated", err)
}
_, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil)
_, err = roomManager.JoinRoom(channelling.RoomTypeRoom+":foo", "foo", channelling.RoomTypeRoom, nil, unauthenticatedSession, false, nil)
assertDataError(t, err, "room_join_requires_account")
}
@ -81,20 +83,20 @@ func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing. @@ -81,20 +83,20 @@ func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfUpdatingAnUnjoinedRoom(t *testing.T) {
roomManager, _ := NewTestRoomManager()
session := &Session{Hello: true, Roomid: "Room:foo"}
session := &Session{Hello: true, Roomid: channelling.RoomTypeRoom + ":foo"}
_, err := roomManager.UpdateRoom(session, &DataRoom{Name: "bar"})
assertDataError(t, err, "not_in_room")
}
func Test_RoomManager_UpdateRoom_ReturnsACorrectlyTypedDocument(t *testing.T) {
roomManager, _ := NewTestRoomManager()
session := &Session{Hello: true, Roomid: "Room:foo"}
session := &Session{Hello: true, Roomid: channelling.RoomTypeRoom + ":foo"}
room, err := roomManager.UpdateRoom(session, &DataRoom{Name: "foo"})
if err != nil {
t.Fatalf("Unexpected error %v updating room", err)
}
if room.Type != "Room" {
t.Errorf("Expected document type to be Room, but was %v", room.Type)
if room.Type != channelling.RoomTypeRoom {
t.Errorf("Expected document type to be %s, but was %v", channelling.RoomTypeRoom, room.Type)
}
}

29
go/channelling/roomworker.go

@ -45,6 +45,7 @@ type RoomWorker interface { @@ -45,6 +45,7 @@ type RoomWorker interface {
Broadcast(sessionID string, buf buffercache.Buffer)
Join(*DataRoomCredentials, *Session, Sender) (*DataRoom, error)
Leave(sessionID string)
GetType() string
}
type roomWorker struct {
@ -60,8 +61,8 @@ type roomWorker struct { @@ -60,8 +61,8 @@ type roomWorker struct {
// Metadata.
id string
Name string
Type string
name string
roomType string
credentials *DataRoomCredentials
}
@ -74,13 +75,13 @@ func NewRoomWorker(manager *roomManager, roomID, roomName, roomType string, cred @@ -74,13 +75,13 @@ func NewRoomWorker(manager *roomManager, roomID, roomName, roomType string, cred
log.Printf("Creating worker for room '%s'\n", roomID)
r := &roomWorker{
manager: manager,
id: roomID,
Name: roomName,
Type: roomType,
workers: make(chan func(), roomMaxWorkers),
expired: make(chan bool),
users: make(map[string]*roomUser),
manager: manager,
id: roomID,
name: roomName,
roomType: roomType,
workers: make(chan func(), roomMaxWorkers),
expired: make(chan bool),
users: make(map[string]*roomUser),
}
if credentials != nil && len(credentials.PIN) > 0 {
@ -146,6 +147,10 @@ func (r *roomWorker) Users() []*roomUser { @@ -146,6 +147,10 @@ func (r *roomWorker) Users() []*roomUser {
return users
}
func (r *roomWorker) GetType() string {
return r.roomType
}
func (r *roomWorker) Run(f func()) bool {
select {
case r.workers <- f:
@ -161,8 +166,8 @@ func (r *roomWorker) Update(room *DataRoom) error { @@ -161,8 +166,8 @@ func (r *roomWorker) Update(room *DataRoom) error {
worker := func() {
r.mutex.Lock()
// Enforce room type and name.
room.Type = r.Type
room.Name = r.Name
room.Type = r.roomType
room.Name = r.name
// Update credentials.
if room.Credentials != nil {
if len(room.Credentials.PIN) > 0 {
@ -270,7 +275,7 @@ func (r *roomWorker) Join(credentials *DataRoomCredentials, session *Session, se @@ -270,7 +275,7 @@ func (r *roomWorker) Join(credentials *DataRoomCredentials, session *Session, se
r.users[session.Id] = &roomUser{session, sender}
// NOTE(lcooper): Needs to be a copy, else we risk races with
// a subsequent modification of room properties.
result := joinResult{&DataRoom{Name: r.Name, Type: r.Type}, nil}
result := joinResult{&DataRoom{Name: r.name, Type: r.roomType}, nil}
r.mutex.Unlock()
results <- result
}

6
go/channelling/roomworker_test.go

@ -23,12 +23,14 @@ package channelling @@ -23,12 +23,14 @@ package channelling
import (
"testing"
"github.com/strukturag/spreed-webrtc/go/channelling"
)
const (
testRoomID string = "Room:a-room-name"
testRoomID string = channelling.RoomTypeRoom + ":a-room-name"
testRoomName string = "a-room-name"
testRoomType string = "Room"
testRoomType string = channelling.RoomTypeRoom
)
func NewTestRoomWorker() RoomWorker {

42
go/channelling/server/config.go

@ -24,6 +24,7 @@ package server @@ -24,6 +24,7 @@ package server
import (
"fmt"
"log"
"regexp"
"strings"
"time"
@ -32,7 +33,17 @@ import ( @@ -32,7 +33,17 @@ import (
"github.com/strukturag/phoenix"
)
func NewConfig(container phoenix.Container, tokens bool) *channelling.Config {
const (
defaultRoomType = channelling.RoomTypeRoom
)
var (
knownRoomTypes = map[string]bool{
channelling.RoomTypeConference: true,
}
)
func NewConfig(container phoenix.Container, tokens bool) (*channelling.Config, error) {
ver := container.GetStringDefault("app", "ver", "")
version := container.Version()
@ -83,6 +94,30 @@ func NewConfig(container phoenix.Container, tokens bool) *channelling.Config { @@ -83,6 +94,30 @@ func NewConfig(container phoenix.Container, tokens bool) *channelling.Config {
}
log.Println("Enabled modules:", modules)
roomTypes := make(map[*regexp.Regexp]string)
if options, _ := container.GetOptions("roomtypes"); len(options) > 0 {
for _, option := range options {
rt := container.GetStringDefault("roomtypes", option, "")
if len(rt) == 0 {
continue
}
if rt != defaultRoomType {
if !knownRoomTypes[rt] {
return nil, fmt.Errorf("Unsupported room type '%s' with expression %s", rt, option)
}
re, err := regexp.Compile(option)
if err != nil {
return nil, fmt.Errorf("Invalid regular expression '%s' for type %s: %s", option, rt, err)
}
roomTypes[re] = rt
}
log.Printf("Using room type %s for %s\n", rt, option)
}
}
return &channelling.Config{
Title: container.GetStringDefault("app", "title", "Spreed WebRTC"),
Ver: ver,
@ -106,8 +141,9 @@ func NewConfig(container phoenix.Container, tokens bool) *channelling.Config { @@ -106,8 +141,9 @@ func NewConfig(container phoenix.Container, tokens bool) *channelling.Config {
GlobalRoomID: container.GetStringDefault("app", "globalRoom", ""),
ContentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""),
ContentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""),
RoomTypeDefault: "Room",
}
RoomTypeDefault: defaultRoomType,
RoomTypes: roomTypes,
}, nil
}
// Helper function to clean up string arrays.

4
go/channelling/server/pipelines.go

@ -87,12 +87,14 @@ func (pipelines *Pipelines) Post(request *http.Request) (int, interface{}, http. @@ -87,12 +87,14 @@ func (pipelines *Pipelines) Post(request *http.Request) (int, interface{}, http.
From: pipeline.FromSession().Id,
Iid: incoming.Iid,
}
reply, err := pipelines.API.OnIncoming(pipeline, pipeline.ToSession(), &incoming)
session := pipeline.ToSession()
reply, err := pipelines.API.OnIncoming(pipeline, session, &incoming)
if err == nil {
result.Data = reply
} else {
result.Data = err
}
pipelines.API.OnIncomingProcessed(pipeline, session, &incoming, reply, err)
return http.StatusOK, result, nil
}

14
server.conf.in

@ -208,3 +208,17 @@ enabled = false @@ -208,3 +208,17 @@ enabled = false
; Use client_id to distinguish between multipe servers. The value is sent
; together with every NATS request. Defaults to empty.
;client_id =
[roomtypes]
; You can define room types that should be used for given room names instead of
; the default type "Room". Use format "RegularExpression = RoomType" and make
; sure the regular expression doesn't contain any "=" or ":".
;
; Available room types:
; "Conference"
; All participants joining the room automatically call each other and are in
; a conference.
;
; Example (all rooms below "conference/" are conference rooms):
;^conference/.+ = Conference
;

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

@ -170,7 +170,10 @@ func runner(runtime phoenix.Runtime) error { @@ -170,7 +170,10 @@ func runner(runtime phoenix.Runtime) error {
natsClientId, _ := runtime.GetString("nats", "client_id")
// Load remaining configuration items.
config = server.NewConfig(runtime, tokenProvider != nil)
config, err = server.NewConfig(runtime, tokenProvider != nil)
if err != nil {
return err
}
// Load templates.
templates = template.New("")

24
src/styles/global/_base.scss

@ -80,3 +80,27 @@ a { @@ -80,3 +80,27 @@ a {
:fullscreen {
background: #000;
}
.visibleRoomTypeRoom {
display: none;
}
.roomTypeRoom .visibleRoomTypeRoom {
display: block;
}
.roomTypeRoom .hiddenRoomTypeRoom {
display: none;
}
.visibleRoomTypeConference {
display: none;
}
.roomTypeConference .visibleRoomTypeConference {
display: block;
}
.roomTypeConference .hiddenRoomTypeConference {
display: none;
}

87
static/js/controllers/uicontroller.js

@ -165,6 +165,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -165,6 +165,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
$scope.peer = null;
$scope.dialing = null;
$scope.conference = null;
$scope.conferenceObject = null;
$scope.conferencePeers = [];
$scope.incoming = null;
$scope.microphoneMute = false;
@ -199,6 +200,50 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -199,6 +200,50 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
localStatus.update(status);
};
$scope.isConferenceRoom = function() {
return mediaStream.webrtc.isConferenceRoom();
};
$scope.updatePeerFromConference = function() {
if (!$scope.conferenceObject) {
return;
}
var peerIds = $scope.conferenceObject.peerIds();
if ($scope.peer && peerIds.indexOf($scope.peer) === -1) {
$scope.peer = null;
}
if (!$scope.peer) {
$scope.peer = peerIds.length > 0 ? peerIds.shift() : null;
} else {
peerIds = _.without(peerIds, $scope.peer);
}
$scope.conferencePeers = peerIds;
};
$scope.setConnectedStatus = function() {
// Don't set connected states if no peer is known yet. Otherwise
// there would be "Someone" visible in the UI.
$scope.updatePeerFromConference();
if (!$scope.peer) {
return;
}
if ($scope.conference) {
$scope.setStatus("conference");
} else {
$scope.setStatus("connected");
}
};
$scope.clearConnectedStatus = function() {
if (mediaStream.connector.connected) {
$scope.setStatus("waiting");
} else {
$scope.setStatus("closed");
}
};
$scope.refreshWebrtcSettings = function() {
var settings = $scope.master.settings;
// Refresh SDP params.
@ -440,14 +485,32 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -440,14 +485,32 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
}
// Apply peer call to scope.
safeApply($scope, function(scope) {
// NOTE: the internal call will have a "id" of "null".
scope.peer = peercall ? peercall.id : null;
scope.setConnectedStatus();
});
});
mediaStream.webrtc.e.on("peerconference", function(event, peerconference) {
safeApply($scope, function(scope) {
scope.conference = peerconference ? peerconference.id : null;
scope.conferencePeers = peerconference ? peerconference.peerIds() : [];
scope.conferenceObject = peerconference ? peerconference : null;
scope.updatePeerFromConference();
scope.setConnectedStatus();
if (!peerconference) {
scope.peer = null;
if (scope.usermedia) {
$timeout(function() {
scope.usermedia = null;
mediaStream.webrtc.stop();
if (mediaStream.webrtc.isConferenceRoom()) {
mediaStream.webrtc.doUserMediaWithInternalCall();
}
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
}, 0);
}
}
});
});
@ -568,6 +631,10 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -568,6 +631,10 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
mediaStream.webrtc.e.on("waitforusermedia connecting", function(event, currentcall) {
var t = event.type;
if (currentcall && currentcall.isinternal && t === "connecting") {
// Don't show "Calling Someone" for the internal call.
return;
}
safeApply($scope, function(scope) {
scope.dialing = currentcall ? currentcall.id : null;
scope.setStatus(t);
@ -586,11 +653,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -586,11 +653,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
/* falls through */
case "completed":
case "connected":
if ($scope.conference) {
$scope.setStatus('conference');
} else {
$scope.setStatus('connected');
}
$scope.setConnectedStatus();
break;
case "failed":
mediaStream.webrtc.doHangup("failed", currentcall.id);
@ -607,11 +670,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -607,11 +670,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
$scope.$on("active", function(event, currentcall) {
console.info("Video state active (assuming connected)", currentcall.id);
if ($scope.conference) {
$scope.setStatus('conference');
} else {
$scope.setStatus('connected');
}
$scope.setConnectedStatus();
$timeout(function() {
if ($scope.peer) {
$scope.layout.buddylist = false;
@ -681,11 +740,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -681,11 +740,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
mediaStream.webrtc.e.on("done stop", function() {
safeApply($scope, function(scope) {
if (mediaStream.connector.connected) {
scope.setStatus("waiting");
} else {
scope.setStatus("closed");
}
scope.clearConnectedStatus();
});
});

46
static/js/directives/audiovideo.js

@ -289,34 +289,42 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ @@ -289,34 +289,42 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
});
mediaStream.webrtc.e.on("done", function() {
mediaStream.webrtc.e.on("done stop", function(event) {
$scope.$apply(function() {
$scope.hasUsermedia = false;
$scope.isActive = false;
$scope.peersTalking = {};
safeApply($scope, function(scope) {
if (!scope.isActive) {
return;
}
scope.hasUsermedia = false;
scope.isActive = false;
scope.peersTalking = {};
if (BigScreen.enabled) {
BigScreen.exit();
}
_.delay(function() {
if ($scope.isActive) {
var removeVideos = function() {
if (scope.isActive) {
return;
}
$scope.localVideo.src = '';
$scope.miniVideo.src = '';
$($scope.remoteVideos).children(".remoteVideo").remove();
}, 1500);
$($scope.mini).removeClass("visible");
$scope.localVideos.style.opacity = 1;
$scope.localVideo.style.opacity = 0;
$scope.remoteVideos.style.opacity = 0;
scope.localVideo.src = '';
scope.miniVideo.src = '';
$(scope.remoteVideos).children(".remoteVideo").remove();
};
if (event.type === "stop") {
removeVideos();
} else {
$timeout(removeVideos, 1500);
}
$(scope.mini).removeClass("visible");
scope.localVideos.style.opacity = 1;
scope.localVideo.style.opacity = 0;
scope.remoteVideos.style.opacity = 0;
$element.removeClass('active');
_.each(streams, function(scope, k) {
scope.$destroy();
_.each(streams, function(streamscope, k) {
streamscope.$destroy();
delete streams[k];
});
$scope.rendererName = $scope.defaultRendererName;
$scope.haveStreams = false;
scope.rendererName = scope.defaultRendererName;
scope.haveStreams = false;
});
});

28
static/js/mediastream/peerconference.js

@ -40,6 +40,14 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall @@ -40,6 +40,14 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall
this.id = id;
}
if (!webrtc.usermedia) {
// Conference was started without getUM being called before. This
// happens for server-manager conference rooms. Create internal
// dummy call to trigger getUM, so actual conference calls can
// be established.
webrtc.doUserMediaWithInternalCall();
}
this.usermedia = webrtc.usermedia;
webrtc.e.on("usermedia", _.bind(function(event, um) {
console.log("Conference user media changed", um);
@ -51,7 +59,7 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall @@ -51,7 +59,7 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall
};
PeerConference.prototype.checkEmpty = function() {
if (!_.isEmpty(this.calls)) {
if (!_.isEmpty(this.calls) || (this.currentcall && this.currentcall.id)) {
return false;
}
@ -87,7 +95,7 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall @@ -87,7 +95,7 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall
PeerConference.prototype.doCall = function(id, autocall) {
if (id === this.currentcall.id || this.calls.hasOwnProperty(id)) {
if ((this.currentcall && id === this.currentcall.id) || this.calls.hasOwnProperty(id)) {
// Ignore calls which we already have.
//console.debug("Already got a call to this id (doCall)", id, this.calls, this.currentcall);
return;
@ -146,7 +154,7 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall @@ -146,7 +154,7 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall
PeerConference.prototype.autoAnswer = function(from, rtcsdp) {
if (from === this.currentcall.id || this.calls.hasOwnProperty(from)) {
if ((this.currentcall && from === this.currentcall.id) || this.calls.hasOwnProperty(from)) {
console.warn("Already got a call to this id (autoAnswer)", from, this.calls);
return;
}
@ -220,6 +228,10 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall @@ -220,6 +228,10 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall
};
PeerConference.prototype.pushUpdate = function() {
if (this.webrtc.isConferenceRoom()) {
// Conference is managed on the server.
return;
}
var calls = _.keys(this.callsIn);
if (calls) {
@ -249,7 +261,15 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall @@ -249,7 +261,15 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall
PeerConference.prototype.peerIds = function() {
return _.keys(this.calls);
var result = _.keys(this.calls);
// "peerIds" returns the session ids of all participants in the
// conference, so we need to add the id of the peer the user called
// manually before migrating to a conference (but only if it has an id,
// i.e. is not an internal call object).
if (this.currentcall && this.currentcall.id && result.indexOf(this.currentcall.id) === -1) {
result.push(this.currentcall.id);
}
return result;
};

89
static/js/mediastream/webrtc.js

@ -42,6 +42,45 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -42,6 +42,45 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
console.log("This seems to be Android");
}
var roomTypeConference = "Conference";
var InternalPC = function(call) {
this.currentcall = call;
this.isinternal = true;
};
InternalPC.prototype.close = function() {
this.currentcall.e.triggerHandler("connectionStateChange", ["closed", this.currentcall]);
};
InternalPC.prototype.addStream = function() {
};
InternalPC.prototype.negotiationNeeded = function() {
};
var InternalCall = function(webrtc) {
this.id = null;
this.webrtc = webrtc;
this.e = $({});
this.isinternal = true;
this.pc = new InternalPC(this);
this.mediaConstraints = $.extend(true, {}, this.webrtc.settings.mediaConstraints);
};
InternalCall.prototype.setInitiate = function(initiate) {
};
InternalCall.prototype.createPeerConnection = function(success_cb, error_cb) {
success_cb(this.pc);
};
InternalCall.prototype.close = function() {
this.pc.close();
this.e.triggerHandler("closed", [this]);
};
var WebRTC = function(api) {
this.api = api;
@ -50,6 +89,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -50,6 +89,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
this.currentcall = null;
this.currentconference = null;
this.currentroom = null;
this.msgQueue = [];
this.started = false;
@ -116,7 +156,24 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -116,7 +156,24 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
};
this.api.e.bind("received.offer received.candidate received.answer received.bye received.conference", _.bind(this.processReceived, this));
this.api.e.bind("received.room", _.bind(this.receivedRoom, this));
};
WebRTC.prototype.receivedRoom = function(event, room) {
this.currentroom = room;
if (this.isConferenceRoom()) {
if (!this.usermedia) {
this.doUserMediaWithInternalCall();
}
} else {
if (this.currentcall && this.currentcall.isinternal) {
this.stop();
}
}
};
WebRTC.prototype.isConferenceRoom = function() {
return this.currentroom && this.currentroom.Type === roomTypeConference;
};
WebRTC.prototype.processReceived = function(event, to, data, type, to2, from) {
@ -137,13 +194,18 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -137,13 +194,18 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
if (!this.initiator && !this.started) {
switch (type) {
case "Offer":
if (this.currentcall) {
if (this.currentcall && !this.currentcall.isinternal) {
console.warn("Received Offer while not started and with current call -> busy.", from);
this.api.sendBye(from, "busy");
this.e.triggerHandler("busy", [from, to2, to]);
return;
}
this.msgQueue.unshift([to, data, type, to2, from]);
if (this.currentcall && this.currentcall.isinternal) {
// Internal getUM is currently in progress, defer
// evaluation of "Offer" until that is completed.
return;
}
// Create call.
this.currentcall = this.createCall(from, from, from);
// Delegate next steps to UI.
@ -163,6 +225,15 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -163,6 +225,15 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Delegate bye to UI.
this.e.triggerHandler("bye", [data.Reason, from, to, to2]);
break;
case "Conference":
// No existing call yet, only supported for server-managed
// conference.
if (!this.isConferenceRoom()) {
console.warn("Received Conference outside call for invalid room type.");
return;
}
this.processReceivedMessage(to, data, type, to2, from);
break;
default:
this.msgQueue.push([to, data, type, to2, from]);
break;
@ -218,7 +289,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -218,7 +289,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
WebRTC.prototype.processReceivedMessage = function(to, data, type, to2, from) {
if (!this.started) {
if (!this.started && type !== "Conference") {
console.log('PeerConnection has not been created yet!');
return;
}
@ -304,6 +375,8 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -304,6 +375,8 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
if (newcurrentcall && newcurrentcall != this.currentcall) {
this.currentcall = newcurrentcall;
this.e.triggerHandler("peercall", [newcurrentcall]);
} else if (!newcurrentcall) {
this.doHangup("receivedbye", targetcall.id);
}
if (this.currentconference && !this.currentconference.checkEmpty()) {
this.e.triggerHandler("peerconference", [this.currentconference]);
@ -314,7 +387,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -314,7 +387,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
this.e.triggerHandler("bye", [data.Reason, from, to, to2]);
break;
case "Conference":
if (!this.currentcall || data.indexOf(this.currentcall.id) === -1) {
if ((!this.currentcall || data.indexOf(this.currentcall.id) === -1) && !this.isConferenceRoom()) {
console.warn("Received Conference for unknown call -> ignore.", to, data);
return;
} else {
@ -379,6 +452,16 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -379,6 +452,16 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
};
WebRTC.prototype.doUserMediaWithInternalCall = function() {
if (this.currentcall && !this.currentcall.isinternal) {
console.warn("Already have a current call, not doing internal getUM", this.currentcall);
return;
}
var currentcall = this.currentcall = new InternalCall(this);
this.e.triggerHandler("peercall", [currentcall]);
this.doUserMedia(currentcall);
};
WebRTC.prototype.doUserMedia = function(currentcall) {
// Create default media (audio/video).

4
static/js/services/rooms.js

@ -28,6 +28,8 @@ define([ @@ -28,6 +28,8 @@ define([
return ["$window", "$location", "$timeout", "$q", "$route", "$rootScope", "$http", "globalContext", "safeApply", "connector", "api", "restURL", "roompin", "appData", "alertify", "translation", "mediaStream", function($window, $location, $timeout, $q, $route, $rootScope, $http, globalContext, safeApply, connector, api, restURL, roompin, appData, alertify, translation, mediaStream) {
var body = $("body");
var url = restURL.api("rooms");
var requestedRoomName = "";
var priorRoomName = null;
@ -109,11 +111,13 @@ define([ @@ -109,11 +111,13 @@ define([
var priorRoom = currentRoom;
currentRoom = room;
if (priorRoom) {
body.removeClass("roomType" + priorRoom.Type);
priorRoomName = priorRoom.Name;
console.log("Left room", [priorRoom.Name]);
$rootScope.$broadcast("room.left", priorRoom.Name);
}
if (currentRoom) {
body.addClass("roomType" + currentRoom.Type);
console.log("Joined room", [currentRoom.Name]);
$rootScope.$broadcast("room.joined", currentRoom.Name);
}

4
static/partials/audiovideo.html

@ -15,10 +15,10 @@ @@ -15,10 +15,10 @@
</div>
</div>
</div>
<div class="overlayActions">
<div class="overlayActions" ng-show="peer">
<button class="btn btn-link renderer-democrazy" title="{{_('Standard view')}}" ng-click="setRenderer('democrazy')"><i class="fa fa-table"></i></button>
<button class="btn btn-link renderer-onepeople" title="{{_('Large view')}}" ng-click="setRenderer('onepeople')"><i class="fa fa-plus-square" ></i></button>
<button class="btn btn-link renderer-conferencekiosk" title="{{_('Kiosk view')}}" ng-click="setRenderer('conferencekiosk')"><i class="fa fa-user"></i></button>
<button class="btn btn-link renderer-auditorium" title="{{_('Auditorium')}}" ng-click="setRenderer('auditorium')"><i class="fa fa-male"></i><span class="fa"></span></button>
</div>
</div>
</div>

6
static/partials/statusmessage.html

@ -2,11 +2,11 @@ @@ -2,11 +2,11 @@
<span ng-switch-when="initializing">{{_("Initializing")}} <i class="fa fa-circle-o-notch fa-spin"></i></span>
<span ng-switch-when="waiting" ng-controller="UsersettingsController as usersettings"><span class="status-indicator"><i style="color:rgb(132,184,25)" class="fa fa-dot-circle-o" title="{{_('Online')}}"></i> {{id|displayName}}</span> <img class="userpicture" ng-show="master.buddyPicture" ng-src="{{master.buddyPicture}}" alt="" /><button ng-if="!authorizing && !myuserid && usersettings.loginUserid" type="button" class="btn btn-default" ng-click="usersettings.loginUserid()">{{_("Sign in")}}</button></span>
<span ng-switch-when="connecting"><span class="msg">{{_("Calling")}} {{dialing|displayName}}</span> <a class="btn btn-small btn-danger" ng-click="doAbort()"><i class="fa fa-circle-o-notch fa-spin"></i> {{_("Hangup")}}</a></span>
<span ng-switch-when="connected"><span class="msg">{{_("In call with")}} {{peer|displayName}}</span> <a class="btn btn-small btn-danger" ng-click="doHangup()"><i class="fa fa-sign-out"></i> {{_("Hangup")}}</a></span>
<span ng-switch-when="conference"><span class="msg">{{_("Conference with")}} {{peer|displayName}}<span>{{conferencePeers|displayConference}}</span></span> <a class="btn btn-small btn-danger" ng-click="doHangup()"><i class="fa fa-sign-out"></i> {{_("Hangup")}}</a></span>
<span ng-switch-when="connected"><span class="msg">{{_("In call with")}} {{peer|displayName}}</span> <a class="btn btn-small btn-danger hiddenRoomTypeConference" ng-click="doHangup()"><i class="fa fa-sign-out"></i> {{_("Hangup")}}</a></span>
<span ng-switch-when="conference"><span class="msg">{{_("Conference with")}} {{peer|displayName}}<span>{{conferencePeers|displayConference}}</span></span> <a class="btn btn-small btn-danger hiddenRoomTypeConference" ng-click="doHangup()"><i class="fa fa-sign-out"></i> {{_("Hangup")}}</a></span>
<span ng-switch-when="closed"><span class="msg">{{_("Your are offline")}}</span> <a class="btn btn-small btn-success" ng-click="doReconnect()"><i class="fa fa-sign-in"></i> {{_("Go online")}}</a></span>
<span ng-switch-when="reconnecting">{{_("Connection interrupted")}} <i class="text-warning fa fa-circle-o-notch fa-spin"></i></span>
<span ng-switch-when="error"><span class="msg">{{_("An error occured")}}</span> <a class="btn btn-small btn-success" ng-click="doReconnect()"><i class="fa fa-refresh"></i> {{_("Retry")}}</a></span>
<span ng-switch-when="ringing"><span class="msg long">{{_("Incoming call")}} {{_("from")}} {{incoming|displayName}}</span> <span class="actions"><a class="btn btn-small btn-success btn-shakeityeah" ng-click="doAccept()"><i class="fa fa-phone"></i> {{_("Accept call")}}</a> <a class="btn btn-small btn-danger" ng-click="doReject()"><i class="fa fa-sign-out"></i> {{_("Reject")}}</a></span></span>
<span ng-switch-when="waitforusermedia"><span class="msg">{{_("Waiting for camera/microphone access")}}</span> <a class="btn btn-small btn-danger" ng-click="doHangup()"><i class="fa fa-circle-o-notch fa-spin"></i> {{_("Hangup")}}</a></span>
<span ng-switch-when="waitforusermedia"><span class="msg">{{_("Waiting for camera/microphone access")}}</span> <a class="btn btn-small btn-danger hiddenRoomTypeConference" ng-click="doHangup()"><i class="fa fa-circle-o-notch fa-spin"></i> {{_("Hangup")}}</a></span>
</span>

4
static/partials/ui.html

@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
<div id="audiolevel" class="ng-cloak">
<div class="audio-level" title="{{_('Your audio level')}}"></div>
</div>
<div id="audiovideo" class="ng-cloak" ng-show="peer">
<div id="audiovideo" class="ng-cloak" ng-show="peer || isConferenceRoom()">
<audio-video/>
</div>
<div id="screenshare" class="ng-cloak mainview">
@ -49,4 +49,4 @@ @@ -49,4 +49,4 @@
</div>
</div>
<div class="ng-cloak" id="settings" ng-class="{show: layout.settings}"><settings/></div>
</chat>
</chat>

Loading…
Cancel
Save