diff --git a/.travis.yml b/.travis.yml index abd40504..bd8c9558 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ go: - 1.1 - 1.2 - 1.3 + - 1.4 - tip env: @@ -25,7 +26,11 @@ script: - ./autogen.sh - ./configure - make get + - make styleshint + # TODO(fancycode): enable styleslint once all styles have been fixed + # - make styleslint - make styles + - make jshint - make javascript - make binary - make build-i18n diff --git a/Makefile.am b/Makefile.am index 80b82790..6af530b1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -131,6 +131,7 @@ install: $(INSTALL) -d $(SHARE)/www/static/translation $(INSTALL) -d $(SHARE)/www/static/css $(INSTALL) -d $(SHARE)/www/static/js/libs/pdf + $(INSTALL) -d $(SHARE)/www/static/js/sandboxes $(INSTALL) bin/$(EXENAME) $(BIN) $(INSTALL) html/* $(SHARE)/www/html $(INSTALL) static/img/* $(SHARE)/www/static/img @@ -142,6 +143,7 @@ install: $(INSTALL) $(OUTPUT_JS)/*.js $(SHARE)/www/static/js $(INSTALL) $(OUTPUT_JS)/libs/pdf/*.js $(SHARE)/www/static/js/libs/pdf $(INSTALL) -D static/js/libs/webodf.js $(SHARE)/www/static/js/libs/webodf.js + $(INSTALL) $(OUTPUT_JS)/sandboxes/*.js $(SHARE)/www/static/js/sandboxes clean: $(GO) clean -i -r app/... 2>/dev/null || true diff --git a/README.md b/README.md index 58622c80..a3f9f712 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ locally by running `npm install` from the project root. Consult the `package.json` file for more details. - [autoprefixer](https://www.npmjs.org/package/autoprefixer) >= 1.1 - - [po2json](https://github.com/mikeedwards/po2json) + - [po2json](https://github.com/mikeedwards/po2json) >= 0.4.1 - [JSHint](http://www.jshint.com/) >= 2.0.0 - [scss-lint](https://github.com/causes/scss-lint) >= 0.33.0 diff --git a/build/build.js b/build/build.js index ec6c1eb9..1f348e09 100644 --- a/build/build.js +++ b/build/build.js @@ -76,6 +76,27 @@ override: { skipModuleInsertion: true } + }, + { + name: 'sandboxes/youtube', + dir: './out/sandboxes', + override: { + skipModuleInsertion: true + } + }, + { + name: 'sandboxes/pdf', + dir: './out/sandboxes', + override: { + skipModuleInsertion: true + } + }, + { + name: 'sandboxes/webodf', + dir: './out/sandboxes', + override: { + skipModuleInsertion: true + } } ] }) diff --git a/configure.ac b/configure.ac index bb66d12a..a068e3f0 100644 --- a/configure.ac +++ b/configure.ac @@ -38,6 +38,7 @@ NODEJS_VERSION_MIN=0.6.0 NODEJS_VERSION_STYLES_MIN=0.10.0 SASS_VERSION_MIN=3.3.0 SCSS_LINT_VERSION_MIN=0.33.0 +PO2JSON_VERSION_MIN=0.4.1 AC_CONFIG_SRCDIR([src/app/spreed-webrtc-server/main.go]) AC_CONFIG_MACRO_DIR([m4]) @@ -183,9 +184,11 @@ if test x"${NPM}" != x"" ; then else AC_MSG_RESULT([ok]) AC_MSG_CHECKING([for version of po2json]) - PO2JSON_VERSION=`{ $NPM list --global & $NPM list; } 2>&1 | $GREP -v 'required' | $GREP po2json@ | $SED 's/^.*po2json@//'` + PO2JSON_VERSION=`{ $NPM list --global & $NPM list; } 2>&1 | $GREP -v 'required' | $GREP po2json@ | tail -n1 | $SED 's/^.*po2json@//'` AC_MSG_RESULT([$PO2JSON_VERSION]) - NODEJS_SUPPORT_PO2JSON=yes + AX_COMPARE_VERSION([$PO2JSON_VERSION], [lt], [$PO2JSON_VERSION_MIN], + [AC_MSG_WARN([Please install po2json version $PO2JSON_VERSION_MIN or newer before trying to build translations (found po2json $PO2JSON_VERSION).]) + NODEJS_SUPPORT_PO2JSON=no],[NODEJS_SUPPORT_PO2JSON=yes]) fi else AC_MSG_WARN([Please install npm and the the node.js module po2json to build i18n.]) diff --git a/debian/changelog b/debian/changelog index 41f4b257..9570eefa 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,44 @@ +spreed-webrtc-server (0.24.0) precise; urgency=low + + * Added hover actions on buddy picture in group chat. + * Jed.js was updated to 1.1.0 including API update for translations. + * Fixed replaced session data receive problem. + * Chat rooms are now reenabled on certain conditions. + * Session close notification is now always sent both directions. + * Reorganized scss. + * Improved null pointer handling in server code. + * Improved server API names to follow general rules. + * TURN and STUN data is now created in constraints service. + * Added screen sharing support for Firefox >= 38. + * Added video resolution selection for Firefox >= 38. + * Split up mediastreamcontroller in multiple parts. + * Reconnect delay is now gradually increased. + * Added basic romm type support. + * Server API was bumped to 1.2. + * Added room name support (Server API 1.2). + * Slashes are now allowed unquoted in room names. + * Spaces are no longer stripped in room path parts. + * Sleepy was replaced by external library Sloth. + * Authorizing flag is now available in scope to avoid flash of sign-in button. + * Copyright was bumped to 2015. + * Youtube player now runs in sandboxed iframe. + * Allow HD video constraints for Firefox >= 38. + * Presentaion (WebODF) now runs in sandboxed iframe. + * Example CSP was updated to work with sandboxed iframe of Youtube and WebODF. + * Load of web fonts is now detected to avoid fouf. + * Added support to enable Opus DTX constraint. + * Fixed problem where a stream without audio was added to audio processor. + * Added support for renegotiation to web client. + * Added audio only styles in web client. + * Receiver can now receive a connection without a stream. + * Youtube playback now has error handling. + * Avoid some fout. + * Firefox will now hang up on renegotiation (if enabled). + * Styles were split up, so they can be built seperately. + * Fixed a problem, where Chrome thought it already had an offer. + + -- Simon Eisenmann Tue, 16 Jun 2015 22:50:46 +0200 + spreed-webrtc-server (0.23.8) precise; urgency=low * Session subscriptions now notify close both ways. diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt index 4427ec7a..1d4e03be 100644 --- a/doc/CHANNELING-API.txt +++ b/doc/CHANNELING-API.txt @@ -1,7 +1,7 @@ - Spreed WebRTC Channeling API v1.3.0 + Spreed WebRTC Channeling API v1.4.0 ================================================= - (c)2014 struktur AG + (c)2015 struktur AG The server provides a Websocket connection end point as channeling API to share peer information for peer to peer connectivity. @@ -98,6 +98,13 @@ Error returns "Message": "A description of the error condition" } + The following predefined error codes may implicitly be returned by any call + which returns an error document: + + unknown: An internal server error, the message may provide more information. + bad_request: The structure or content of the client's request was invalid, + the message may contain specifics. + Special purpose documents for channling Self @@ -110,6 +117,7 @@ Special purpose documents for channling "Suserid": "", "Token": "some-very-long-string", "Version": "server-version-number", + "ApiVersion": 1.4, "Turn": { "username": "turn-username", "password": "turn-password", @@ -129,21 +137,26 @@ Special purpose documents for channling Keys: - Type : Self (string) - Id : Public Session id for this connection (string). - Sid : Secure (non public) id for this session (string). - Userid : User id if this session belongs to an authenticated user. Else empty. - Suserid : Secure (non public) user id if session has an user id. Else empty. - Token : Security token (string), to restablish connection with the same - session. Pass the value as URL query parameter t, to the websocket URL. - Version : Server version number. Use this to detect server upgrades. - 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. - Stun : Array with STUN server URLs. + Type : Self (string) + Id : Public Session id for this connection (string). + Sid : Secure (non public) id for this session (string). + Userid : User id if this session belongs to an authenticated user. + Else empty. + Suserid : Secure (non public) user id if session has an user id. + Else empty. + Token : Security token (string), to restablish connection with the + same session. Pass the value as URL query parameter t, to + the websocket URL. + 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. + Stun : Array with STUN server URLs. You can also send an empty Self document to the server to make the server transmit a fresh Self document (eg. to refresh when ttl was reached). Please @@ -155,9 +168,10 @@ Special purpose documents for channling { Type: "Hello", Hello: { - Version: "1.0.0", - Ua: "Test client 1.0", - Id: "", + "Version": "1.0.0", + "Ua": "Test client 1.0", + "Name": "", + "Type": "", "Credentials": {...} } } @@ -172,7 +186,10 @@ Special purpose documents for channling Version : Channel protocol version (string). Ua : User agent description (string). - Id : Room id. The default Room has the empty string Id ("") (string). + Name : Room name. The default Room has the empty string name ("") (string). + Type : Room type. Use empty string to let the server select the + default type. + Id : Same as 'Name' (kept for compatibility). Credentials : An optional RoomCredentials document containing room authentication information. See the Room document for information on how such credentials should be handled after @@ -211,10 +228,10 @@ Special purpose documents for channling Keys under Welcome: - Room: Contains the current state of the room, see the description of - the Room document for more details. - Users: Contains the user list for the room, see the description of - the Users document for more details. + Room : Contains the current state of the room, see the description of + the Room document for more details. + Users : Contains the user list for the room, see the description of + the Users document for more details. RoomCredentials @@ -234,7 +251,7 @@ Special purpose documents for channling Room { - "Type": "Room", + "Type": "room-type", "Name": "room-name-here" "Credentials": {...} } @@ -249,8 +266,10 @@ Special purpose documents for channling Keys under Room: - Name : The human readable ID of the room, currently must be globally - unique. + Type : The room type. This field should only be send to alter + the room type. It will always contain the type of the room + when returned by the server. + Name : The human readable name of the room. Credentials : Optional authentication information for the room, see the documentation of the RoomCredentials document for more details. This field shall only be present when sending or @@ -416,8 +435,8 @@ Additional types for session listing and notifications } - 'buddyPicture' can be in next formats: - 1. Base64 encoded string of an image. + 'buddyPicture' can be in next formats: + 1. Base64 encoded string of an image. Example: data:image/jpeg;base64,/9j/4... 2. url subpath to query REST API. Please refer to REST API for more information Example: img:8nG33oDk8Yv8fvK6IphL/6vjI2NLigcET/picture.jpg @@ -427,9 +446,10 @@ Additional types for session listing and notifications Rev is the status update sequence for this status update entry. It is a positive integer. Higher numbers are later status updates. - When the current session is in a room (means sent Hello), a Users request - can be sent, to receive a list of sessions in that particular room. This - always returns the sessions in the same room as the calling session. + When the current session has successfully joined a room (see Hello for more + details), a Users request will return a Users document containing session + details for the current room. An Error document will be returned if no room + has been joined or session information cannot be retrieved. Users (Request uses empty data) @@ -470,6 +490,10 @@ Additional types for session listing and notifications Note: The Userid field is only present, if that session belongs to a known user. + Error codes: + + not_in_room: Clients must join a room before requesting users. + Alive { @@ -506,12 +530,19 @@ User authorization and session authentication The Authentication document binds a userid to the current session. The Nonce and Userid need to be validateable by the server. If Authentication - was successfull, a new Self document will be sent. The Nonce value can - be generated by using the REST API (sessions end point). + was successful, a new Self document will be sent. Otherwise an Error + document will be returned describing why authentication failed. Note that + the Nonce value can be generated by using the REST API (sessions end point). There is no way to undo authentication for a session. For log out, close the session (disconnect) and forget the token. + Error codes: + + already_authenticated: This session has already authenticated, follow + the reauthentication procedure above. + invalid_session_token: The provided session token information is invalid, + the error message may contain more information. Information retrieval @@ -532,6 +563,9 @@ Information retrieval Token data retrieved on incoming messages as A field (attestation token). + If session information retrieval fails, an Error document with one of the + listed codes will be returned. + Sessions (Response with Id, Token and Type from request and populated Session list). @@ -558,6 +592,11 @@ Information retrieval ] } + Error codes: + + contacts_not_enabled: Requests with subtype `contact` are not enabled. + bad_attestation: The requested session attestation is invalid. + no_such_session: The requested session could not be found. Chat messages and status information diff --git a/html/logo.html b/html/logo.html deleted file mode 100644 index ab7b9a7b..00000000 --- a/html/logo.html +++ /dev/null @@ -1 +0,0 @@ -<%define "logo"%><%end%> \ No newline at end of file diff --git a/html/main.html b/html/main.html index f485bc1d..9cf277e3 100644 --- a/html/main.html +++ b/html/main.html @@ -1,62 +1,13 @@ <%define "mainPage"%> - ng-csp<%end%>> + ng-csp<%end%>> <%template "head" .%>
- - -
- -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
-
-
+ <%template "extra-body" .%> -<%end%> +<%end%> \ No newline at end of file diff --git a/package.json b/package.json index c4839aca..167ee1f8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "dependencies": { "autoprefixer": ">= 3.1.0", - "po2json": ">= 0.3.0", + "po2json": ">= 0.4.1", "jshint": ">= 2.5.5" } } diff --git a/server.conf.in b/server.conf.in index e6b143d3..a66af10f 100644 --- a/server.conf.in +++ b/server.conf.in @@ -57,6 +57,10 @@ listen = 127.0.0.1:8080 ; See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 for details. ; A supported TURN server is https://code.google.com/p/rfc5766-turn-server/. ;turnSecret = the-default-turn-shared-secret-do-not-keep +; Enable renegotiation support. Set to true to tell clients that they can +; renegotiate peer connections when required. Firefox support is not complete, +; so do not enable if you want compatibility with Firefox clients. +;renegotiation = false ; Session secret to use for session id generator. 32 or 64 bytes of random data ; are recommented (hex encoded). A warning will be logged if hex decode fails. ; You can generate a secret easily with "xxd -ps -l 32 -c 32 /dev/random". @@ -109,6 +113,7 @@ serverRealm = local ; data: URL for images. ; The currently recommended CSP is: ; default-src 'self'; +; frame-src 'self' blob:; ; style-src 'self' 'unsafe-inline'; ; img-src 'self' data: blob:; ; connect-src 'self' wss://server:port/ws blob:; diff --git a/src/app/spreed-webrtc-server/api.go b/src/app/spreed-webrtc-server/api.go index 61d1bb2f..0fa83666 100644 --- a/src/app/spreed-webrtc-server/api.go +++ b/src/app/spreed-webrtc-server/api.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/buffercache.go b/src/app/spreed-webrtc-server/buffercache.go index 44a797e9..d19ba72d 100644 --- a/src/app/spreed-webrtc-server/buffercache.go +++ b/src/app/spreed-webrtc-server/buffercache.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/channelling.go b/src/app/spreed-webrtc-server/channelling.go index 5099b114..c37629ba 100644 --- a/src/app/spreed-webrtc-server/channelling.go +++ b/src/app/spreed-webrtc-server/channelling.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -42,7 +42,9 @@ type DataRoomCredentials struct { type DataHello struct { Version string Ua string - Id string + Id string // Compatibility with old clients. + Name string // Room name. + Type string // Room type. Credentials *DataRoomCredentials } @@ -53,8 +55,8 @@ type DataWelcome struct { } type DataRoom struct { - Type string - Name string + Type string // Room type. + Name string // Room name. Credentials *DataRoomCredentials } @@ -77,15 +79,16 @@ type DataAnswer struct { } type DataSelf struct { - Type string - Id string - Sid string - Userid string - Suserid string - Token string - Version string - Turn *DataTurn - Stun []string + Type string + Id string + Sid string + Userid string + Suserid string + Token string + Version string // Server version. + ApiVersion float64 // Server channelling API version. + Turn *DataTurn + Stun []string } type DataTurn struct { diff --git a/src/app/spreed-webrtc-server/channelling_api.go b/src/app/spreed-webrtc-server/channelling_api.go index 8c42dab1..f44e4671 100644 --- a/src/app/spreed-webrtc-server/channelling_api.go +++ b/src/app/spreed-webrtc-server/channelling_api.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -28,12 +28,13 @@ import ( const ( maxConferenceSize = 100 + apiVersion = 1.4 // Keep this in sync with CHANNELING-API docs.Hand ) type ChannellingAPI interface { - OnConnect(Client, *Session) + OnConnect(Client, *Session) (interface{}, error) OnDisconnect(Client, *Session) - OnIncoming(ResponseSender, *Session, *DataIncoming) + OnIncoming(Sender, *Session, *DataIncoming) (interface{}, error) } type channellingAPI struct { @@ -60,182 +61,269 @@ func NewChannellingAPI(config *Config, roomStatus RoomStatusManager, sessionEnco } } -func (api *channellingAPI) OnConnect(client Client, session *Session) { +func (api *channellingAPI) OnConnect(client Client, session *Session) (interface{}, error) { api.Unicaster.OnConnect(client, session) - api.SendSelf(client, session) + return api.HandleSelf(session) } func (api *channellingAPI) OnDisconnect(client Client, session *Session) { api.Unicaster.OnDisconnect(client, session) } -func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *DataIncoming) { +func (api *channellingAPI) OnIncoming(sender Sender, session *Session, msg *DataIncoming) (interface{}, error) { switch msg.Type { case "Self": - api.SendSelf(c, session) + return api.HandleSelf(session) case "Hello": - //log.Println("Hello", msg.Hello, c.Index()) - // TODO(longsleep): Filter room id and user agent. - session.Update(&SessionUpdate{Types: []string{"Ua"}, Ua: msg.Hello.Ua}) - - room, err := session.JoinRoom(msg.Hello.Id, msg.Hello.Credentials, c) - // NOTE(lcooper): Iid filtered for compatibility's sake. - // Evaluate sending unconditionally when supported by all clients. - if msg.Iid != "" { - if err == nil { - c.Reply(msg.Iid, &DataWelcome{ - Type: "Welcome", - Room: room, - Users: api.RoomUsers(session), - }) - } else { - c.Reply(msg.Iid, err) - } + if msg.Hello == nil { + return nil, NewDataError("bad_request", "message did not contain Hello") } + + return api.HandleHello(session, msg.Hello, sender) case "Offer": + if msg.Offer == nil { + log.Println("Received invalid offer message.", msg) + break + } + // TODO(longsleep): Validate offer session.Unicast(msg.Offer.To, msg.Offer) case "Candidate": + if msg.Candidate == nil { + log.Println("Received invalid candidate message.", msg) + break + } + // TODO(longsleep): Validate candidate session.Unicast(msg.Candidate.To, msg.Candidate) case "Answer": + if msg.Answer == nil { + log.Println("Received invalid answer message.", msg) + break + } + // TODO(longsleep): Validate Answer session.Unicast(msg.Answer.To, msg.Answer) case "Users": - if session.Hello { - sessions := &DataSessions{Type: "Users", Users: api.RoomUsers(session)} - c.Reply(msg.Iid, sessions) - } + return api.HandleUsers(session) case "Authentication": - st := msg.Authentication.Authentication - if st == nil { - return + if msg.Authentication == nil || msg.Authentication.Authentication == nil { + return nil, NewDataError("bad_request", "message did not contain Authentication") } - if err := api.Authenticate(session, st, ""); err == nil { - log.Println("Authentication success", session.Userid()) - api.SendSelf(c, session) - session.BroadcastStatus() - } else { - log.Println("Authentication failed", err, st.Userid, st.Nonce) - } + return api.HandleAuthentication(session, msg.Authentication.Authentication) case "Bye": + if msg.Bye == nil { + log.Println("Received invalid bye message.", msg) + break + } + session.Unicast(msg.Bye.To, msg.Bye) case "Status": + if msg.Status == nil { + log.Println("Received invalid status message.", msg) + break + } + //log.Println("Status", msg.Status) session.Update(&SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) session.BroadcastStatus() case "Chat": - // TODO(longsleep): Limit sent chat messages per incoming connection. - if !msg.Chat.Chat.NoEcho { - session.Unicast(session.Id, msg.Chat) - } - msg.Chat.Chat.Time = time.Now().Format(time.RFC3339) - if msg.Chat.To == "" { - // TODO(longsleep): Check if chat broadcast is allowed. - if session.Hello { - api.CountBroadcastChat() - session.Broadcast(msg.Chat) - } - } else { - if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil { - if !api.Config.WithModule("contacts") { - return - } - if err := api.contactrequestHandler(session, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest); err != nil { - log.Println("Ignoring invalid contact request.", err) - return - } - msg.Chat.Chat.Status.ContactRequest.Userid = session.Userid() - } - if msg.Chat.Chat.Status == nil { - api.CountUnicastChat() - } - - session.Unicast(msg.Chat.To, msg.Chat) - if msg.Chat.Chat.Mid != "" { - // Send out delivery confirmation status chat message. - session.Unicast(session.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}}) - } + if msg.Chat == nil || msg.Chat.Chat == nil { + log.Println("Received invalid chat message.", msg) + break } + + api.HandleChat(session, msg.Chat) case "Conference": - // Check conference maximum size. - if len(msg.Conference.Conference) > maxConferenceSize { - log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference)) - } else { - // Send conference update to anyone. - for _, id := range msg.Conference.Conference { - if id != session.Id { - session.Unicast(id, msg.Conference) - } - } + if msg.Conference == nil { + log.Println("Received invalid conference message.", msg) + break } + + api.HandleConference(session, msg.Conference) case "Alive": - c.Reply(msg.Iid, msg.Alive) + return msg.Alive, nil case "Sessions": - var users []*DataSession - switch msg.Sessions.Sessions.Type { - case "contact": - if api.Config.WithModule("contacts") { - if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil { - users = api.GetUserSessions(session, userID) - } else { - log.Printf(err.Error()) - } - } else { - log.Printf("Incoming contacts session request with contacts disabled") - } - case "session": - id, err := session.attestation.Decode(msg.Sessions.Sessions.Token) - if err != nil { - log.Printf("Failed to decode incoming attestation", err, msg.Sessions.Sessions.Token) - break - } - session, ok := api.GetSession(id) - if !ok { - log.Printf("Cannot retrieve session for id %s", id) - break - } - users = make([]*DataSession, 1, 1) - users[0] = session.Data() - default: - log.Printf("Unkown incoming sessions request type %s", msg.Sessions.Sessions.Type) + if msg.Sessions == nil || msg.Sessions.Sessions == nil { + return nil, NewDataError("bad_request", "message did not contain Sessions") } - // TODO(lcooper): We ought to reply with a *DataError here if failed. - if users != nil { - c.Reply(msg.Iid, &DataSessions{Type: "Sessions", Users: users, Sessions: msg.Sessions.Sessions}) - } + return api.HandleSessions(session, msg.Sessions.Sessions) case "Room": - if room, err := api.UpdateRoom(session, msg.Room); err == nil { - session.Broadcast(room) - c.Reply(msg.Iid, room) - } else { - c.Reply(msg.Iid, err) + if msg.Room == nil { + return nil, NewDataError("bad_request", "message did not contain Room") } + + return api.HandleRoom(session, msg.Room) default: log.Println("OnText unhandled message type", msg.Type) } + + return nil, nil } -func (api *channellingAPI) SendSelf(c Responder, session *Session) { +func (api *channellingAPI) HandleSelf(session *Session) (*DataSelf, error) { token, err := api.EncodeSessionToken(session) if err != nil { log.Println("Error in OnRegister", err) - return + return nil, err } log.Println("Created new session token", len(token), token) self := &DataSelf{ - Type: "Self", - Id: session.Id, - Sid: session.Sid, - Userid: session.Userid(), - Suserid: api.EncodeSessionUserID(session), - Token: token, - Version: api.Version, - Turn: api.CreateTurnData(session), - Stun: api.StunURIs, + Type: "Self", + Id: session.Id, + Sid: session.Sid, + Userid: session.Userid(), + Suserid: api.EncodeSessionUserID(session), + Token: token, + Version: api.Version, + ApiVersion: apiVersion, + Turn: api.CreateTurnData(session), + Stun: api.StunURIs, + } + + return self, nil +} + +func (api *channellingAPI) HandleHello(session *Session, hello *DataHello, sender Sender) (*DataWelcome, error) { + // TODO(longsleep): Filter room id and user agent. + session.Update(&SessionUpdate{Types: []string{"Ua"}, Ua: hello.Ua}) + + // Compatibily for old clients. + roomName := hello.Name + if roomName == "" { + roomName = hello.Id + } + + room, err := session.JoinRoom(roomName, hello.Type, hello.Credentials, sender) + if err != nil { + return nil, err + } + return &DataWelcome{ + Type: "Welcome", + Room: room, + Users: api.RoomUsers(session), + }, nil +} + +func (api *channellingAPI) HandleUsers(session *Session) (sessions *DataSessions, err error) { + if session.Hello { + sessions = &DataSessions{Type: "Users", Users: api.RoomUsers(session)} + } else { + err = NewDataError("not_in_room", "Cannot list users without a current room") + } + return +} + +func (api *channellingAPI) HandleAuthentication(session *Session, st *SessionToken) (*DataSelf, error) { + if err := api.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) + if err == nil { + session.BroadcastStatus() + } + + return self, err +} + +func (api *channellingAPI) HandleChat(session *Session, chat *DataChat) { + // TODO(longsleep): Limit sent chat messages per incoming connection. + msg := chat.Chat + to := chat.To + + if !msg.NoEcho { + session.Unicast(session.Id, chat) + } + msg.Time = time.Now().Format(time.RFC3339) + if to == "" { + // TODO(longsleep): Check if chat broadcast is allowed. + if session.Hello { + api.CountBroadcastChat() + session.Broadcast(chat) + } + } else { + if msg.Status != nil { + if msg.Status.ContactRequest != nil { + if !api.Config.WithModule("contacts") { + return + } + if err := api.contactrequestHandler(session, to, msg.Status.ContactRequest); err != nil { + log.Println("Ignoring invalid contact request.", err) + return + } + msg.Status.ContactRequest.Userid = session.Userid() + } + } else { + api.CountUnicastChat() + } + + session.Unicast(to, chat) + if msg.Mid != "" { + // Send out delivery confirmation status chat message. + session.Unicast(session.Id, &DataChat{To: to, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Mid, Status: &DataChatStatus{State: "sent"}}}) + } + } +} + +func (api *channellingAPI) HandleConference(session *Session, conference *DataConference) { + // Check conference maximum size. + if len(conference.Conference) > maxConferenceSize { + log.Println("Refusing to create conference above limit.", len(conference.Conference)) + return + } + + // Send conference update to anyone. + for _, id := range conference.Conference { + if id != session.Id { + session.Unicast(id, conference) + } + } +} + +func (api *channellingAPI) HandleSessions(session *Session, sessions *DataSessionsRequest) (*DataSessions, error) { + switch sessions.Type { + case "contact": + if !api.Config.WithModule("contacts") { + return nil, NewDataError("contacts_not_enabled", "incoming contacts session request with contacts disabled") + } + userID, err := api.getContactID(session, sessions.Token) + if err != nil { + return nil, err + } + return &DataSessions{ + Type: "Sessions", + Users: api.GetUserSessions(session, userID), + Sessions: sessions, + }, nil + case "session": + id, err := session.attestation.Decode(sessions.Token) + if err != nil { + return nil, NewDataError("bad_attestation", err.Error()) + } + session, ok := api.GetSession(id) + if !ok { + return nil, NewDataError("no_such_session", "cannot retrieve session") + } + return &DataSessions{ + Type: "Sessions", + Users: []*DataSession{session.Data()}, + Sessions: sessions, + }, nil + default: + return nil, NewDataError("bad_request", "unknown sessions request type") + } +} + +func (api *channellingAPI) HandleRoom(session *Session, room *DataRoom) (*DataRoom, error) { + room, err := api.UpdateRoom(session, room) + if err == nil { + session.Broadcast(room) } - c.Reply("", self) + return room, err } diff --git a/src/app/spreed-webrtc-server/channelling_api_test.go b/src/app/spreed-webrtc-server/channelling_api_test.go index ebf004fd..2a2d66c7 100644 --- a/src/app/spreed-webrtc-server/channelling_api_test.go +++ b/src/app/spreed-webrtc-server/channelling_api_test.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -23,24 +23,16 @@ package main import ( "errors" + "fmt" "testing" ) type fakeClient struct { - replies map[string]interface{} } func (fake *fakeClient) Send(_ Buffer) { } -func (fake *fakeClient) Reply(iid string, msg interface{}) { - if fake.replies == nil { - fake.replies = make(map[string]interface{}) - } - - fake.replies[iid] = msg -} - type fakeRoomManager struct { joinedRoomID string leftRoomID string @@ -57,9 +49,9 @@ func (fake *fakeRoomManager) RoomUsers(session *Session) []*DataSession { return fake.roomUsers } -func (fake *fakeRoomManager) JoinRoom(id string, _ *DataRoomCredentials, session *Session, sessionAuthenticated bool, _ Sender) (*DataRoom, error) { +func (fake *fakeRoomManager) JoinRoom(id, roomName, roomType string, _ *DataRoomCredentials, session *Session, sessionAuthenticated bool, _ Sender) (*DataRoom, error) { fake.joinedID = id - return &DataRoom{Name: id}, fake.joinError + return &DataRoom{Name: roomName, Type: roomType}, fake.joinError } func (fake *fakeRoomManager) LeaveRoom(roomID, sessionID string) { @@ -74,27 +66,11 @@ func (fake *fakeRoomManager) UpdateRoom(_ *Session, _ *DataRoom) (*DataRoom, err return fake.updatedRoom, fake.updateError } -func assertReply(t *testing.T, client *fakeClient, iid string) interface{} { - msg, ok := client.replies[iid] - if !ok { - t.Fatalf("No response received for Iid %v", iid) - } - return msg -} - -func assertErrorReply(t *testing.T, client *fakeClient, iid, code string) { - err, ok := assertReply(t, client, iid).(*DataError) - if !ok { - t.Fatalf("Expected response message to be an Error") - } - - if err.Type != "Error" { - t.Error("Message did not have the correct type") - } - - if err.Code != code { - t.Errorf("Expected error code to be %v, but was %v", code, err.Code) +func (fake *fakeRoomManager) MakeRoomID(roomName, roomType string) string { + if roomType == "" { + roomType = "Room" } + return fmt.Sprintf("%s:%s", roomType, roomName) } func NewTestChannellingAPI() (ChannellingAPI, *fakeClient, *Session, *fakeRoomManager) { @@ -109,10 +85,10 @@ func NewTestChannellingAPI() (ChannellingAPI, *fakeClient, *Session, *fakeRoomMa } func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing.T) { - roomID, ua := "foobar", "unit tests" + roomID, roomName, ua := "Room:foobar", "foobar", "unit tests" api, client, session, roomManager := NewTestChannellingAPI() - api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID, Ua: ua}}) + api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomName, Ua: ua}}) if roomManager.joinedID != roomID { t.Errorf("Expected to have joined room %v, but got %v", roomID, roomManager.joinedID) @@ -133,10 +109,10 @@ func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing } func Test_ChannellingAPI_OnIncoming_HelloMessage_LeavesAnyPreviouslyJoinedRooms(t *testing.T) { - roomID := "foobar" + roomID, roomName := "Room:foobar", "foobar" api, client, session, roomManager := NewTestChannellingAPI() - api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID}}) + api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomName}}) api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: "baz"}}) if roomManager.leftID != roomID { @@ -168,21 +144,19 @@ func Test_ChannellingAPI_OnIncoming_HelloMessage_DoesNotJoinIfNotPermitted(t *te } } -func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAWelcome(t *testing.T) { - iid, roomID := "foo", "a-room" +func Test_ChannellingAPI_OnIncoming_HelloMessage_RespondsWithAWelcome(t *testing.T) { + roomID := "a-room" api, client, session, roomManager := NewTestChannellingAPI() roomManager.roomUsers = []*DataSession{&DataSession{}} - api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{Id: roomID}}) - - msg, ok := client.replies[iid] - if !ok { - t.Fatalf("No response received for Iid %v", iid) + reply, err := api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID}}) + if err != nil { + t.Fatalf("Unexpected error %v", err) } - welcome, ok := msg.(*DataWelcome) + welcome, ok := reply.(*DataWelcome) if !ok { - t.Fatalf("Expected response message %#v to be a Welcome", msg) + t.Fatalf("Expected response %#v to be a Welcome", reply) } if welcome.Type != "Welcome" { @@ -198,25 +172,31 @@ func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAWelcome(t } } -func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAnErrorIfTheRoomCannotBeJoined(t *testing.T) { - iid := "foo" +func Test_ChannellingAPI_OnIncoming_HelloMessage_RespondsWithAnErrorIfTheRoomCannotBeJoined(t *testing.T) { api, client, session, roomManager := NewTestChannellingAPI() roomManager.joinError = NewDataError("bad_join", "") - api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{}}) + _, err := api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{}}) - assertErrorReply(t, client, iid, "bad_join") + assertDataError(t, err, "bad_join") } func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpdatedRoom(t *testing.T) { - iid, roomName := "123", "foo" + roomName := "foo" api, client, session, roomManager := NewTestChannellingAPI() roomManager.updatedRoom = &DataRoom{Name: "FOO"} - api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}}) - api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}}) + _, err := api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomName}}) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } - room, ok := assertReply(t, client, iid).(*DataRoom) + reply, err := api.OnIncoming(client, session, &DataIncoming{Type: "Room", Room: &DataRoom{Name: roomName}}) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + room, ok := reply.(*DataRoom) if !ok { t.Fatalf("Expected response message to be a Room") } @@ -235,12 +215,15 @@ func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpda } func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAnErrorIfUpdatingTheRoomFails(t *testing.T) { - iid, roomName := "123", "foo" + roomName := "foo" api, client, session, roomManager := NewTestChannellingAPI() roomManager.updateError = NewDataError("a_room_error", "") - api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}}) - api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}}) + _, err := api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomName}}) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + _, err = api.OnIncoming(client, session, &DataIncoming{Type: "Room", Room: &DataRoom{Name: roomName}}) - assertErrorReply(t, client, iid, "a_room_error") + assertDataError(t, err, "a_room_error") } diff --git a/src/app/spreed-webrtc-server/client.go b/src/app/spreed-webrtc-server/client.go index b858176b..7e6269f6 100644 --- a/src/app/spreed-webrtc-server/client.go +++ b/src/app/spreed-webrtc-server/client.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -29,17 +29,8 @@ type Sender interface { Send(Buffer) } -type ResponseSender interface { - Sender - Responder -} - -type Responder interface { - Reply(iid string, m interface{}) -} - type Client interface { - ResponseSender + Sender Session() *Session Index() uint64 Close() @@ -59,7 +50,11 @@ func NewClient(codec Codec, api ChannellingAPI, session *Session) *client { func (client *client) OnConnect(conn Connection) { client.Connection = conn - client.ChannellingAPI.OnConnect(client, client.session) + if reply, err := client.ChannellingAPI.OnConnect(client, client.session); err == nil { + client.reply("", reply) + } else { + log.Println("OnConnect error", err) + } } func (client *client) OnDisconnect() { @@ -68,14 +63,20 @@ func (client *client) OnDisconnect() { } func (client *client) OnText(b Buffer) { - if incoming, err := client.DecodeIncoming(b); err == nil { - client.OnIncoming(client, client.session, incoming) - } else { + incoming, err := client.DecodeIncoming(b) + if err != nil { log.Println("OnText error while processing incoming message", err) + return + } + + if reply, err := client.OnIncoming(client, client.session, incoming); err != nil { + client.reply(incoming.Iid, err) + } else if reply != nil { + client.reply(incoming.Iid, reply) } } -func (client *client) Reply(iid string, m interface{}) { +func (client *client) reply(iid string, m interface{}) { outgoing := &DataOutgoing{From: client.session.Id, Iid: iid, Data: m} if b, err := client.EncodeOutgoing(outgoing); err == nil { client.Send(b) diff --git a/src/app/spreed-webrtc-server/common_test.go b/src/app/spreed-webrtc-server/common_test.go index 395e2a9c..66bf4822 100644 --- a/src/app/spreed-webrtc-server/common_test.go +++ b/src/app/spreed-webrtc-server/common_test.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/config.go b/src/app/spreed-webrtc-server/config.go index 0e250c61..89a14c54 100644 --- a/src/app/spreed-webrtc-server/config.go +++ b/src/app/spreed-webrtc-server/config.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -36,6 +36,7 @@ type Config struct { 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 @@ -52,6 +53,7 @@ type Config struct { globalRoomID string // Id of the global room (not exported to Javascript) contentSecurityPolicy string // HTML content security policy contentSecurityPolicyReportOnly string // HTML content security policy in report only mode + roomTypeDefault string // New rooms default to this type } func NewConfig(container phoenix.Container, tokens bool) *Config { @@ -96,7 +98,7 @@ func NewConfig(container phoenix.Container, tokens bool) *Config { "contacts": true, } modules := []string{} - for module, _ := range modulesTable { + for module := range modulesTable { if container.GetBoolDefault("modules", module, true) { modules = append(modules, module) } else { @@ -111,6 +113,7 @@ func NewConfig(container phoenix.Container, tokens bool) *Config { S: fmt.Sprintf("static/ver=%s", ver), B: basePath, Token: serverToken, + Renegotiation: container.GetBoolDefault("app", "renegotiation", false), StunURIs: stunURIs, TurnURIs: turnURIs, Tokens: tokens, @@ -127,6 +130,7 @@ func NewConfig(container phoenix.Container, tokens bool) *Config { globalRoomID: container.GetStringDefault("app", "globalRoom", ""), contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""), contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""), + roomTypeDefault: "Room", } } diff --git a/src/app/spreed-webrtc-server/connection.go b/src/app/spreed-webrtc-server/connection.go index 47f29970..dc3fece1 100644 --- a/src/app/spreed-webrtc-server/connection.go +++ b/src/app/spreed-webrtc-server/connection.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/contact.go b/src/app/spreed-webrtc-server/contact.go index 995f92d6..09588a33 100644 --- a/src/app/spreed-webrtc-server/contact.go +++ b/src/app/spreed-webrtc-server/contact.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/context.go b/src/app/spreed-webrtc-server/context.go index cb944c33..fee07996 100644 --- a/src/app/spreed-webrtc-server/context.go +++ b/src/app/spreed-webrtc-server/context.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/hub.go b/src/app/spreed-webrtc-server/hub.go index 6a7dcd26..6c4f9386 100644 --- a/src/app/spreed-webrtc-server/hub.go +++ b/src/app/spreed-webrtc-server/hub.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/images.go b/src/app/spreed-webrtc-server/images.go index 36d66dab..d18e4ce2 100644 --- a/src/app/spreed-webrtc-server/images.go +++ b/src/app/spreed-webrtc-server/images.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/incoming_codec.go b/src/app/spreed-webrtc-server/incoming_codec.go index fb8911bf..d3d0c3f0 100644 --- a/src/app/spreed-webrtc-server/incoming_codec.go +++ b/src/app/spreed-webrtc-server/incoming_codec.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/main.go b/src/app/spreed-webrtc-server/main.go index 86a157a8..13fd051a 100644 --- a/src/app/spreed-webrtc-server/main.go +++ b/src/app/spreed-webrtc-server/main.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -22,7 +22,6 @@ package main import ( - "app/spreed-webrtc-server/sleepy" "bytes" "crypto/rand" "encoding/hex" @@ -32,6 +31,7 @@ import ( "github.com/strukturag/goacceptlanguageparser" "github.com/strukturag/httputils" "github.com/strukturag/phoenix" + "github.com/strukturag/sloth" "html/template" "log" "net/http" @@ -196,15 +196,14 @@ func runner(runtime phoenix.Runtime) error { sessionSecretString, err := runtime.GetString("app", "sessionSecret") if err != nil { return fmt.Errorf("No sessionSecret in config file.") - } else { - sessionSecret, err = hex.DecodeString(sessionSecretString) - if err != nil { - log.Println("Warning: sessionSecret value is not a hex encoded", err) - sessionSecret = []byte(sessionSecretString) - } - if len(sessionSecret) < 32 { - return fmt.Errorf("Length of sessionSecret must be at least 32 bytes.") - } + } + sessionSecret, err = hex.DecodeString(sessionSecretString) + if err != nil { + log.Println("Warning: sessionSecret value is not a hex encoded", err) + sessionSecret = []byte(sessionSecretString) + } + if len(sessionSecret) < 32 { + return fmt.Errorf("Length of sessionSecret must be at least 32 bytes.") } if len(sessionSecret) < 32 { @@ -215,19 +214,18 @@ func runner(runtime phoenix.Runtime) error { encryptionSecretString, err := runtime.GetString("app", "encryptionSecret") if err != nil { return fmt.Errorf("No encryptionSecret in config file.") - } else { - encryptionSecret, err = hex.DecodeString(encryptionSecretString) - if err != nil { - log.Println("Warning: encryptionSecret value is not a hex encoded", err) - encryptionSecret = []byte(encryptionSecretString) - } - switch l := len(encryptionSecret); { - case l == 16: - case l == 24: - case l == 32: - default: - return fmt.Errorf("Length of encryptionSecret must be exactly 16, 24 or 32 bytes to select AES-128, AES-192 or AES-256.") - } + } + encryptionSecret, err = hex.DecodeString(encryptionSecretString) + if err != nil { + log.Println("Warning: encryptionSecret value is not a hex encoded", err) + encryptionSecret = []byte(encryptionSecretString) + } + switch l := len(encryptionSecret); { + case l == 16: + case l == 24: + case l == 32: + default: + return fmt.Errorf("Length of encryptionSecret must be exactly 16, 24 or 32 bytes to select AES-128, AES-192 or AES-256.") } var turnSecret []byte @@ -352,10 +350,12 @@ func runner(runtime phoenix.Runtime) error { r.Handle("/robots.txt", http.StripPrefix(config.B, http.FileServer(http.Dir(path.Join(rootFolder, "static"))))) r.Handle("/favicon.ico", http.StripPrefix(config.B, http.FileServer(http.Dir(path.Join(rootFolder, "static", "img"))))) r.Handle("/ws", makeWSHandler(statsManager, sessionManager, codec, channellingAPI)) + + // Simple room handler. r.HandleFunc("/{room}", httputils.MakeGzipHandler(roomHandler)) // Add API end points. - api := sleepy.NewAPI() + api := sloth.NewAPI() api.SetMux(r.PathPrefix("/api/v1/").Subrouter()) api.AddResource(&Rooms{}, "/rooms") api.AddResource(config, "/config") @@ -382,6 +382,10 @@ func runner(runtime phoenix.Runtime) error { } } + // Map everything else to a room when it is a GET. + rooms := r.PathPrefix("/").Methods("GET").Subrouter() + rooms.HandleFunc("/{room:.*}", httputils.MakeGzipHandler(roomHandler)) + return runtime.Start() } diff --git a/src/app/spreed-webrtc-server/random.go b/src/app/spreed-webrtc-server/random.go index 4001a917..a884cd5b 100644 --- a/src/app/spreed-webrtc-server/random.go +++ b/src/app/spreed-webrtc-server/random.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/room_manager.go b/src/app/spreed-webrtc-server/room_manager.go index 01bd9094..76a77faa 100644 --- a/src/app/spreed-webrtc-server/room_manager.go +++ b/src/app/spreed-webrtc-server/room_manager.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -22,15 +22,17 @@ package main import ( + "fmt" "log" "sync" ) type RoomStatusManager interface { RoomUsers(*Session) []*DataSession - JoinRoom(roomID string, credentials *DataRoomCredentials, session *Session, sessionAuthenticated bool, sender Sender) (*DataRoom, error) + JoinRoom(roomID, roomName, roomType string, credentials *DataRoomCredentials, session *Session, sessionAuthenticated bool, sender Sender) (*DataRoom, error) LeaveRoom(roomID, sessionID string) UpdateRoom(*Session, *DataRoom) (*DataRoom, error) + MakeRoomID(roomName, roomType string) string } type Broadcaster interface { @@ -51,16 +53,21 @@ type roomManager struct { sync.RWMutex *Config OutgoingEncoder - roomTable map[string]RoomWorker + roomTable map[string]RoomWorker + globalRoomID string + defaultRoomID string } func NewRoomManager(config *Config, encoder OutgoingEncoder) RoomManager { - return &roomManager{ - sync.RWMutex{}, - config, - encoder, - make(map[string]RoomWorker), - } + rm := &roomManager{ + RWMutex: sync.RWMutex{}, + Config: config, + OutgoingEncoder: encoder, + roomTable: make(map[string]RoomWorker), + } + rm.globalRoomID = rm.MakeRoomID(config.globalRoomID, "") + rm.defaultRoomID = rm.MakeRoomID("", "") + return rm } func (rooms *roomManager) RoomUsers(session *Session) []*DataSession { @@ -71,12 +78,12 @@ func (rooms *roomManager) RoomUsers(session *Session) []*DataSession { return []*DataSession{} } -func (rooms *roomManager) JoinRoom(roomID string, credentials *DataRoomCredentials, session *Session, sessionAuthenticated bool, sender Sender) (*DataRoom, error) { - if roomID == "" && !rooms.DefaultRoomEnabled { +func (rooms *roomManager) JoinRoom(roomID, roomName, roomType string, credentials *DataRoomCredentials, session *Session, sessionAuthenticated bool, sender Sender) (*DataRoom, error) { + if roomID == rooms.defaultRoomID && !rooms.DefaultRoomEnabled { return nil, NewDataError("default_room_disabled", "The default room is not enabled") } - roomWorker, err := rooms.GetOrCreate(roomID, credentials, sessionAuthenticated) + roomWorker, err := rooms.GetOrCreate(roomID, roomName, roomType, credentials, sessionAuthenticated) if err != nil { return nil, err } @@ -91,15 +98,18 @@ func (rooms *roomManager) LeaveRoom(roomID, sessionID string) { } func (rooms *roomManager) UpdateRoom(session *Session, room *DataRoom) (*DataRoom, error) { - if !session.Hello || session.Roomid != room.Name { + var roomID string + if room != nil { + roomID = rooms.MakeRoomID(room.Name, room.Type) + } + if !session.Hello || session.Roomid != roomID { return nil, NewDataError("not_in_room", "Cannot update other rooms") } - // XXX(lcooper): We'll process and send documents without this field - // correctly, however clients cannot not handle it currently. - room.Type = "Room" if roomWorker, ok := rooms.Get(session.Roomid); ok { return room, roomWorker.Update(room) } + // Set default room type if room was not found. + room.Type = rooms.roomTypeDefault // TODO(lcooper): We should almost certainly return an error in this case. return room, nil } @@ -145,7 +155,7 @@ func (rooms *roomManager) Get(roomID string) (room RoomWorker, ok bool) { return } -func (rooms *roomManager) GetOrCreate(roomID string, credentials *DataRoomCredentials, sessionAuthenticated bool) (RoomWorker, error) { +func (rooms *roomManager) GetOrCreate(roomID, roomName, roomType string, credentials *DataRoomCredentials, sessionAuthenticated bool) (RoomWorker, error) { if rooms.AuthorizeRoomJoin && rooms.UsersEnabled && !sessionAuthenticated { return nil, NewDataError("room_join_requires_account", "Room join requires a user account") } @@ -167,7 +177,7 @@ func (rooms *roomManager) GetOrCreate(roomID string, credentials *DataRoomCreden return nil, NewDataError("room_join_requires_account", "Room creation requires a user account") } - room := NewRoomWorker(rooms, roomID, credentials) + room := NewRoomWorker(rooms, roomID, roomName, roomType, credentials) rooms.roomTable[roomID] = room rooms.Unlock() go func() { @@ -196,3 +206,10 @@ func (rooms *roomManager) GlobalUsers() []*roomUser { rooms.RUnlock() return make([]*roomUser, 0) } + +func (rooms *roomManager) MakeRoomID(roomName, roomType string) string { + if roomType == "" { + roomType = rooms.roomTypeDefault + } + return fmt.Sprintf("%s:%s", roomType, roomName) +} diff --git a/src/app/spreed-webrtc-server/room_manager_test.go b/src/app/spreed-webrtc-server/room_manager_test.go index e87c934f..fd4ff413 100644 --- a/src/app/spreed-webrtc-server/room_manager_test.go +++ b/src/app/spreed-webrtc-server/room_manager_test.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -36,16 +36,16 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenCreat config.AuthorizeRoomCreation = true unauthenticatedSession := &Session{} - _, err := roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil) + _, err := roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil) assertDataError(t, err, "room_join_requires_account") authenticatedSession := &Session{userid: "9870457"} - _, err = roomManager.JoinRoom("foo", nil, authenticatedSession, true, nil) + _, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, authenticatedSession, true, nil) if err != nil { t.Fatalf("Unexpected error %v joining room while authenticated", err) } - _, err = roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil) + _, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil) if err != nil { t.Fatalf("Unexpected error %v joining room while unauthenticated", err) } @@ -57,16 +57,16 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenJoinR config.AuthorizeRoomJoin = true unauthenticatedSession := &Session{} - _, err := roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil) + _, err := roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil) assertDataError(t, err, "room_join_requires_account") authenticatedSession := &Session{userid: "9870457"} - _, err = roomManager.JoinRoom("foo", nil, authenticatedSession, true, nil) + _, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, authenticatedSession, true, nil) if err != nil { t.Fatalf("Unexpected error %v joining room while authenticated", err) } - _, err = roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil) + _, err = roomManager.JoinRoom("Room:foo", "foo", "Room", nil, unauthenticatedSession, false, nil) assertDataError(t, err, "room_join_requires_account") } @@ -79,15 +79,15 @@ func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing. func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfUpdatingAnUnjoinedRoom(t *testing.T) { roomManager, _ := NewTestRoomManager() - session := &Session{Hello: true, Roomid: "foo"} + session := &Session{Hello: true, Roomid: "Room: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: "foo"} - room, err := roomManager.UpdateRoom(session, &DataRoom{Name: session.Roomid}) + session := &Session{Hello: true, Roomid: "Room:foo"} + room, err := roomManager.UpdateRoom(session, &DataRoom{Name: "foo"}) if err != nil { t.Fatalf("Unexpected error %v updating room", err) } diff --git a/src/app/spreed-webrtc-server/rooms.go b/src/app/spreed-webrtc-server/rooms.go index a10e3ca2..bea8b90d 100644 --- a/src/app/spreed-webrtc-server/rooms.go +++ b/src/app/spreed-webrtc-server/rooms.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/roomworker.go b/src/app/spreed-webrtc-server/roomworker.go index f0dea820..95355309 100644 --- a/src/app/spreed-webrtc-server/roomworker.go +++ b/src/app/spreed-webrtc-server/roomworker.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -56,7 +56,9 @@ type roomWorker struct { mutex sync.RWMutex // Metadata. - Id string + id string + Name string + Type string credentials *DataRoomCredentials } @@ -65,13 +67,15 @@ type roomUser struct { Sender } -func NewRoomWorker(manager *roomManager, id string, credentials *DataRoomCredentials) RoomWorker { +func NewRoomWorker(manager *roomManager, roomID, roomName, roomType string, credentials *DataRoomCredentials) RoomWorker { - log.Printf("Creating worker for room '%s'\n", id) + log.Printf("Creating worker for room '%s'\n", roomID) r := &roomWorker{ manager: manager, - Id: id, + id: roomID, + Name: roomName, + Type: roomType, workers: make(chan func(), roomMaxWorkers), expired: make(chan bool), users: make(map[string]*roomUser), @@ -107,7 +111,7 @@ L: if len(r.users) == 0 { // Cleanup room when it is empty. r.mutex.RUnlock() - log.Printf("Room worker not in use - cleaning up '%s'\n", r.Id) + log.Printf("Room worker not in use - cleaning up '%s'\n", r.id) break L } else { r.mutex.RUnlock() @@ -149,7 +153,7 @@ func (r *roomWorker) Run(f func()) bool { case r.workers <- f: return true default: - log.Printf("Room worker channel full or closed '%s'\n", r.Id) + log.Printf("Room worker channel full or closed '%s'\n", r.id) return false } @@ -159,6 +163,10 @@ func (r *roomWorker) Update(room *DataRoom) error { fault := make(chan error, 1) worker := func() { r.mutex.Lock() + // Enforce room type and name. + room.Type = r.Type + room.Name = r.Name + // Update credentials. if room.Credentials != nil { if len(room.Credentials.PIN) > 0 { r.credentials = room.Credentials @@ -184,7 +192,7 @@ func (r *roomWorker) GetUsers() []*DataSession { session.Type = "Online" sl = append(sl, session) if len(sl) > maxUsersLength { - log.Println("Limiting users response length in channel", r.Id) + log.Println("Limiting users response length in channel", r.id) return false } } @@ -264,7 +272,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.Id}, nil} + result := joinResult{&DataRoom{Name: r.Name, Type: r.Type}, nil} r.mutex.Unlock() results <- result } diff --git a/src/app/spreed-webrtc-server/roomworker_test.go b/src/app/spreed-webrtc-server/roomworker_test.go index f963c5cd..1ada20b3 100644 --- a/src/app/spreed-webrtc-server/roomworker_test.go +++ b/src/app/spreed-webrtc-server/roomworker_test.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -26,18 +26,20 @@ import ( ) const ( + testRoomID string = "Room:a-room-name" testRoomName string = "a-room-name" + testRoomType string = "Room" ) func NewTestRoomWorker() RoomWorker { - worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomName, nil) + worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomID, testRoomName, testRoomType, nil) go worker.Start() return worker } func NewTestRoomWorkerWithPIN(t *testing.T) (RoomWorker, string) { pin := "asdf" - worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomName, &DataRoomCredentials{PIN: pin}) + worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomID, testRoomName, testRoomType, &DataRoomCredentials{PIN: pin}) go worker.Start() return worker, pin } diff --git a/src/app/spreed-webrtc-server/session.go b/src/app/spreed-webrtc-server/session.go index c226e6c0..84043147 100644 --- a/src/app/spreed-webrtc-server/session.go +++ b/src/app/spreed-webrtc-server/session.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -22,7 +22,6 @@ package main import ( - "errors" "fmt" "github.com/gorilla/securecookie" "strings" @@ -117,7 +116,8 @@ func (s *Session) RemoveSubscriber(id string) { s.mutex.Unlock() } -func (s *Session) JoinRoom(roomID string, credentials *DataRoomCredentials, sender Sender) (*DataRoom, error) { +func (s *Session) JoinRoom(roomName, roomType string, credentials *DataRoomCredentials, sender Sender) (*DataRoom, error) { + roomID := s.RoomStatusManager.MakeRoomID(roomName, roomType) s.mutex.Lock() defer s.mutex.Unlock() @@ -134,7 +134,7 @@ func (s *Session) JoinRoom(roomID string, credentials *DataRoomCredentials, send }) } - room, err := s.RoomStatusManager.JoinRoom(roomID, credentials, s, s.authenticated(), sender) + room, err := s.RoomStatusManager.JoinRoom(roomID, roomName, roomType, credentials, s, s.authenticated(), sender) if err == nil { s.Hello = true s.Roomid = roomID @@ -310,15 +310,18 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) { defer s.mutex.Unlock() if s.Id != st.Id || s.Sid != st.Sid { - return "", errors.New("session id mismatch") + return "", NewDataError("invalid_session_token", "session id mismatch") } if s.userid != "" { - return "", errors.New("session already authenticated") + return "", NewDataError("already_authenticated", "session already authenticated") } // Create authentication nonce. var err error s.Nonce, err = sessionNonces.Encode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Userid) + if err != nil { + err = NewDataError("unknown", err.Error()) + } return s.Nonce, err @@ -330,18 +333,18 @@ func (s *Session) Authenticate(realm string, st *SessionToken, userid string) er defer s.mutex.Unlock() if s.userid != "" { - return errors.New("session already authenticated") + return NewDataError("already_authenticated", "session already authenticated") } if userid == "" { if s.Nonce == "" || s.Nonce != st.Nonce { - return errors.New("nonce validation failed") + return NewDataError("invalid_session_token", "nonce validation failed") } err := sessionNonces.Decode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Nonce, &userid) if err != nil { - return err + return NewDataError("invalid_session_token", err.Error()) } if st.Userid != userid { - return errors.New("user id mismatch") + return NewDataError("invalid_session_token", "user id mismatch") } s.Nonce = "" } diff --git a/src/app/spreed-webrtc-server/session_manager.go b/src/app/spreed-webrtc-server/session_manager.go index 43360f7d..3ba91525 100644 --- a/src/app/spreed-webrtc-server/session_manager.go +++ b/src/app/spreed-webrtc-server/session_manager.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/sessions.go b/src/app/spreed-webrtc-server/sessions.go index 6ea0b452..1c22c2ea 100644 --- a/src/app/spreed-webrtc-server/sessions.go +++ b/src/app/spreed-webrtc-server/sessions.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/sleepy/core.go b/src/app/spreed-webrtc-server/sleepy/core.go deleted file mode 100644 index 465e497b..00000000 --- a/src/app/spreed-webrtc-server/sleepy/core.go +++ /dev/null @@ -1,234 +0,0 @@ -/** - * A RESTful framework for Go - * - * Modified version of sleepy to support Gorilla muxers. - * https://github.com/strukturag/sleepy - * - * Copyright (c) 2014 struktur AG - * Copyright (c) 2013-2014 Doug Black and the Sleepy authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * -**/ - -package sleepy - -import ( - "encoding/json" - "errors" - "fmt" - "github.com/gorilla/mux" - "net/http" -) - -const ( - GET = "GET" - POST = "POST" - PUT = "PUT" - DELETE = "DELETE" - HEAD = "HEAD" - PATCH = "PATCH" -) - -// GetSupported is the interface that provides the Get -// method a resource must support to receive HTTP GETs. -type GetSupported interface { - Get(*http.Request) (int, interface{}, http.Header) -} - -// PostSupported is the interface that provides the Post -// method a resource must support to receive HTTP POSTs. -type PostSupported interface { - Post(*http.Request) (int, interface{}, http.Header) -} - -// PutSupported is the interface that provides the Put -// method a resource must support to receive HTTP PUTs. -type PutSupported interface { - Put(*http.Request) (int, interface{}, http.Header) -} - -// DeleteSupported is the interface that provides the Delete -// method a resource must support to receive HTTP DELETEs. -type DeleteSupported interface { - Delete(*http.Request) (int, interface{}, http.Header) -} - -// HeadSupported is the interface that provides the Head -// method a resource must support to receive HTTP HEADs. -type HeadSupported interface { - Head(*http.Request) (int, interface{}, http.Header) -} - -// PatchSupported is the interface that provides the Patch -// method a resource must support to receive HTTP PATCHs. -type PatchSupported interface { - Patch(*http.Request) (int, interface{}, http.Header) -} - -// Interface for arbitrary muxer support (like http.ServeMux). -type APIMux interface { - HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) *mux.Route - ServeHTTP(w http.ResponseWriter, r *http.Request) -} - -// An API manages a group of resources by routing requests -// to the correct method on a matching resource and marshalling -// the returned data to JSON for the HTTP response. -// -// You can instantiate multiple APIs on separate ports. Each API -// will manage its own set of resources. -type API struct { - mux APIMux - muxInitialized bool -} - -// NewAPI allocates and returns a new API. -func NewAPI() *API { - return &API{} -} - -func (api *API) requestHandler(resource interface{}) http.HandlerFunc { - return func(rw http.ResponseWriter, request *http.Request) { - - if request.ParseForm() != nil { - rw.WriteHeader(http.StatusBadRequest) - return - } - - var handler func(*http.Request) (int, interface{}, http.Header) - - switch request.Method { - case GET: - if resource, ok := resource.(GetSupported); ok { - handler = resource.Get - } - case POST: - if resource, ok := resource.(PostSupported); ok { - handler = resource.Post - } - case PUT: - if resource, ok := resource.(PutSupported); ok { - handler = resource.Put - } - case DELETE: - if resource, ok := resource.(DeleteSupported); ok { - handler = resource.Delete - } - case HEAD: - if resource, ok := resource.(HeadSupported); ok { - handler = resource.Head - } - case PATCH: - if resource, ok := resource.(PatchSupported); ok { - handler = resource.Patch - } - } - - if handler == nil { - rw.WriteHeader(http.StatusMethodNotAllowed) - return - } - - code, data, header := handler(request) - - var content []byte - var err error - - switch data.(type) { - case string: - content = []byte(data.(string)) - case []byte: - content = data.([]byte) - default: - // Encode JSON. - content, err = json.MarshalIndent(data, "", " ") - if err != nil { - if header == nil { - header = http.Header{"Content-Type": {"application/json"}} - } else if header.Get("Content-Type") == "" { - header.Set("Content-Type", "application/json") - } - } - } - - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - return - } - for name, values := range header { - for _, value := range values { - rw.Header().Add(name, value) - } - } - rw.WriteHeader(code) - rw.Write(content) - } -} - -// Mux returns the muxer used by an API. If a ServeMux does not -// yet exist, a new *http.ServeMux will be created and returned. -func (api *API) Mux() APIMux { - if api.muxInitialized { - return api.mux - } else { - api.mux = mux.NewRouter() - api.muxInitialized = true - return api.mux - } -} - -// SetMux sets the muxer to use by an API. A muxer needs to -// implement the APIMux interface (eg. http.ServeMux). -func (api *API) SetMux(mux APIMux) error { - if api.muxInitialized { - return errors.New("You cannot set a muxer when already initialized.") - } else { - api.mux = mux - api.muxInitialized = true - return nil - } -} - -// AddResource adds a new resource to an API. The API will route -// requests that match one of the given paths to the matching HTTP -// method on the resource. -func (api *API) AddResource(resource interface{}, paths ...string) { - for _, path := range paths { - api.Mux().HandleFunc(path, api.requestHandler(resource)) - } -} - -// AddResourceWithWrapper behaves exactly like AddResource but wraps -// the generated handler function with a give wrapper function to allow -// to hook in Gzip support and similar. -func (api *API) AddResourceWithWrapper(resource interface{}, wrapper func(handler http.HandlerFunc) http.HandlerFunc, paths ...string) { - for _, path := range paths { - api.Mux().HandleFunc(path, wrapper(api.requestHandler(resource))) - } -} - -// Start causes the API to begin serving requests on the given port. -func (api *API) Start(port int) error { - if !api.muxInitialized { - return errors.New("You must add at least one resource to this API.") - } - portString := fmt.Sprintf(":%d", port) - return http.ListenAndServe(portString, api.Mux()) -} diff --git a/src/app/spreed-webrtc-server/stats.go b/src/app/spreed-webrtc-server/stats.go index ac9ac0e5..5318175c 100644 --- a/src/app/spreed-webrtc-server/stats.go +++ b/src/app/spreed-webrtc-server/stats.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/stats_manager.go b/src/app/spreed-webrtc-server/stats_manager.go index 59163839..d2507db0 100644 --- a/src/app/spreed-webrtc-server/stats_manager.go +++ b/src/app/spreed-webrtc-server/stats_manager.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/tickets.go b/src/app/spreed-webrtc-server/tickets.go index b44f8bef..f75cc1d4 100644 --- a/src/app/spreed-webrtc-server/tickets.go +++ b/src/app/spreed-webrtc-server/tickets.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/tls.go b/src/app/spreed-webrtc-server/tls.go index 92a082c4..a431c72c 100644 --- a/src/app/spreed-webrtc-server/tls.go +++ b/src/app/spreed-webrtc-server/tls.go @@ -1,7 +1,7 @@ /* * TLS helpers for Go based on crypto/tls package. * - * Copyright (C) 2014 struktur AG. All rights reserved. + * Copyright (C) 2015 struktur AG. All rights reserved. * Copyright 2011 The Go Authors. All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/app/spreed-webrtc-server/tokenprovider.go b/src/app/spreed-webrtc-server/tokenprovider.go index 2d6ec585..f93fb853 100644 --- a/src/app/spreed-webrtc-server/tokenprovider.go +++ b/src/app/spreed-webrtc-server/tokenprovider.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/app/spreed-webrtc-server/tokens.go b/src/app/spreed-webrtc-server/tokens.go index c1993eaf..d492d265 100644 --- a/src/app/spreed-webrtc-server/tokens.go +++ b/src/app/spreed-webrtc-server/tokens.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -49,9 +49,8 @@ func (tokens Tokens) Post(request *http.Request) (int, interface{}, http.Header) if valid != "" { log.Printf("Good incoming token request: %s\n", auth) return 200, &Token{Token: valid, Success: true}, http.Header{"Content-Type": {"application/json"}} - } else { - log.Printf("Wrong incoming token request: %s\n", auth) - return 403, NewApiError("invalid_token", "Invalid token"), http.Header{"Content-Type": {"application/json"}} } + log.Printf("Wrong incoming token request: %s\n", auth) + return 403, NewApiError("invalid_token", "Invalid token"), http.Header{"Content-Type": {"application/json"}} } diff --git a/src/app/spreed-webrtc-server/user.go b/src/app/spreed-webrtc-server/user.go index fa838f05..c6e2b279 100644 --- a/src/app/spreed-webrtc-server/user.go +++ b/src/app/spreed-webrtc-server/user.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -43,7 +43,8 @@ func NewUser(id string) *User { } -// Return true if first session. +// AddSession adds a session to the session table and returns true if +// s is the first session. func (u *User) AddSession(s *Session) bool { first := false u.mutex.Lock() @@ -56,7 +57,8 @@ func (u *User) AddSession(s *Session) bool { return first } -// Return true if no session left. +// RemoveSession removes a session from the session table abd returns +// true if no session is left left. func (u *User) RemoveSession(sessionID string) bool { last := false u.mutex.Lock() diff --git a/src/app/spreed-webrtc-server/users.go b/src/app/spreed-webrtc-server/users.go index c10fa512..6dac32bb 100644 --- a/src/app/spreed-webrtc-server/users.go +++ b/src/app/spreed-webrtc-server/users.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -46,7 +46,7 @@ import ( ) var ( - serialNumberLimit *big.Int = new(big.Int).Lsh(big.NewInt(1), 128) + serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 128) ) type UsersHandler interface { @@ -229,12 +229,12 @@ func (uh *UsersCertificateHandler) Create(un *UserNonce, request *http.Request) } spkacDerBytes, err := base64.StdEncoding.DecodeString(spkac) if err != nil { - return nil, errors.New(fmt.Sprintf("spkac invalid: %s", err)) + return nil, fmt.Errorf("spkac invalid: %s", err) } publicKey, err := pkac.ParseSPKAC(spkacDerBytes) if err != nil { - return nil, errors.New(fmt.Sprintf("unable to parse spkac: %s", err)) + return nil, fmt.Errorf("unable to parse spkac: %s", err) } template, err := uh.makeTemplate(un.Userid) @@ -244,7 +244,7 @@ func (uh *UsersCertificateHandler) Create(un *UserNonce, request *http.Request) certDerBytes, err := x509.CreateCertificate(rand.Reader, template, uh.certificate, publicKey, uh.privateKey) if err != nil { - return nil, errors.New(fmt.Sprintf("failed to create certificate: %s", err)) + return nil, fmt.Errorf("failed to create certificate: %s", err) } log.Println("Generated new certificate", un.Userid) @@ -284,10 +284,9 @@ func (un *UserNonce) Response() (int, interface{}, http.Header) { if un.contentType != "" { header.Set("Content-Type", un.contentType) return 200, un.raw, header - } else { - header.Set("Content-Type", "application/json") - return 200, un, header } + header.Set("Content-Type", "application/json") + return 200, un, header } type Users struct { diff --git a/src/app/spreed-webrtc-server/ws.go b/src/app/spreed-webrtc-server/ws.go index 70ad83fd..f43f01d1 100644 --- a/src/app/spreed-webrtc-server/ws.go +++ b/src/app/spreed-webrtc-server/ws.go @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/i18n/helpers/po2json b/src/i18n/helpers/po2json index c23f6572..f46f00a6 100755 --- a/src/i18n/helpers/po2json +++ b/src/i18n/helpers/po2json @@ -7,7 +7,7 @@ var po2json = require('po2json'), assert.equal(argv.length, 4, 'Usage: po2json '); -var result = po2json.parseFileSync(argv[2], { stringify: true, format: 'jed', pretty: false }), +var result = po2json.parseFileSync(argv[2], { stringify: true, format: 'jed1.x', pretty: false }), stream = fs.createWriteStream(argv[3], {}); stream.write(result); diff --git a/src/i18n/messages-de.po b/src/i18n/messages-de.po index 9fc5e658..f713a1ee 100644 --- a/src/i18n/messages-de.po +++ b/src/i18n/messages-de.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Spreed WebRTC 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2015-02-18 14:46+0100\n" -"PO-Revision-Date: 2015-02-18 14:49+0100\n" +"POT-Creation-Date: 2015-04-30 19:22+0200\n" +"PO-Revision-Date: 2015-04-30 19:27+0100\n" "Last-Translator: Simon Eisenmann \n" "Language-Team: struktur AG \n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" @@ -18,9 +18,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" -msgid "Your audio level" -msgstr "Ihr Audio-Pegel" - msgid "Standard view" msgstr "Standardansicht" @@ -246,6 +243,42 @@ msgstr "Optionen für Bildschirmfreigabe" msgid "Fit screen." msgstr "Bildschirm einpassen." +msgid "Share screen" +msgstr "Bildschirm teilen" + +msgid "Please select what to share." +msgstr "Bitte wählen Sie aus, was geteilt werden soll." + +msgid "Screen" +msgstr "Bildschirm" + +msgid "Window" +msgstr "Fenster" + +msgid "Application" +msgstr "Anwendung" + +msgid "Share the whole screen. Click share to select the screen." +msgstr "" +"Gesamten Bildschirm teilen. Klicken Sie auf Teilen um den Bildschirm " +"auszuwählen." + +msgid "Share a single window. Click share to select the window." +msgstr "" +"Einzelnes Fenster teilen. Klicken Sie auf Teilen um das Fenster " +"auszuwählen." + +msgid "" +"Share all windows of a application. This can leak content behind windows " +"when windows get moved. Click share to select the application." +msgstr "" +"Alle Fenster einer Anwendung teilen. Es wird u.U. Inhalt hinter Fenstern " +"der Anwendung geteilt, wenn diese verschoben werden. Klicken Sie auf " +"Teilen um die Anwendung auszuwählen." + +msgid "Share" +msgstr "Teilen" + msgid "Profile" msgstr "Profil" @@ -466,6 +499,9 @@ msgstr "Anruf annehmen" msgid "Waiting for camera/microphone access" msgstr "Warte auf Kamera/Mikrofon Freigabe" +msgid "Your audio level" +msgstr "Ihr Audio-Pegel" + msgid "Checking camera and microphone access." msgstr "Prüfe Zugriff auf Kamera und Mikrofon." @@ -519,9 +555,6 @@ msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt." msgid "YouTube URL" msgstr "YouTube URL" -msgid "Share" -msgstr "Teilen" - msgid "" "Could not load YouTube player API, please check your network / firewall " "settings." @@ -711,6 +744,48 @@ msgstr "Meeting:" msgid "Room name" msgstr "Raum-Name" +msgid "" +"The request contains an invalid parameter value. Please check the URL of " +"the video you want to share and try again." +msgstr "" +"Die Anfrage enthält falsche Parameter. Bitte prüfen Sie die URL des " +"Videos." + +msgid "" +"The requested content cannot be played in an HTML5 player or another " +"error related to the HTML5 player has occurred. Please try again later." +msgstr "" +"Dieser Inhalt kann nicht im HTML5-Player abgespielt werden oder ein " +"anderer HTML5-Player-Fehler ist aufgetreten. Bitte versuchen Sie es " +"später wieder." + +msgid "" +"The video requested was not found. Please check the URL of the video you " +"want to share and try again." +msgstr "Das Video wurde nicht gefunden. Bitte prüfen Sie die URL des Videos." + +msgid "" +"The owner of the requested video does not allow it to be played in " +"embedded players." +msgstr "" +"Der Eigentümer des Videos hat das Video nicht für eingebettete Anzeige " +"freigegeben." + +#, python-format +msgid "" +"An unknown error occurred while playing back the video (%s). Please try " +"again later." +msgstr "" +"Beim Abspielen des Videos ist ein unbekannter Fehler aufgetreten (%s). " +"Bitte versuchen Sie es später wieder." + +msgid "" +"An unknown error occurred while playing back the video. Please try again " +"later." +msgstr "" +"Beim Abspielen des Videos ist ein unbekannter Fehler aufgetreten. Bitte " +"versuchen Sie es später wieder." + msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgstr "Unbekanntes URL-Format. Bitte geben Sie eine gültige YouTube URL ein." diff --git a/src/i18n/messages-ja.po b/src/i18n/messages-ja.po index beaa7a57..c6fbb36d 100644 --- a/src/i18n/messages-ja.po +++ b/src/i18n/messages-ja.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed WebRTC 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2015-02-18 14:46+0100\n" +"POT-Creation-Date: 2015-04-30 19:22+0200\n" "PO-Revision-Date: 2014-04-23 22:25+0100\n" "Last-Translator: Curt Frisemo \n" "Language-Team: Curt Frisemo \n" @@ -18,9 +18,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" -msgid "Your audio level" -msgstr "あなたの音量" - msgid "Standard view" msgstr "" @@ -246,6 +243,38 @@ msgstr "画面共有オプション" msgid "Fit screen." msgstr "画面に合わせる" +#, fuzzy +msgid "Share screen" +msgstr "画面を共有する." + +msgid "Please select what to share." +msgstr "" + +#, fuzzy +msgid "Screen" +msgstr "画面に合わせる" + +msgid "Window" +msgstr "" + +msgid "Application" +msgstr "" + +msgid "Share the whole screen. Click share to select the screen." +msgstr "" + +msgid "Share a single window. Click share to select the window." +msgstr "" + +msgid "" +"Share all windows of a application. This can leak content behind windows " +"when windows get moved. Click share to select the application." +msgstr "" + +#, fuzzy +msgid "Share" +msgstr "共有取り消し" + msgid "Profile" msgstr "" @@ -460,6 +489,9 @@ msgstr "通話" msgid "Waiting for camera/microphone access" msgstr "カメラ・マイクの接続待ち." +msgid "Your audio level" +msgstr "あなたの音量" + msgid "Checking camera and microphone access." msgstr "カメラ・マイクの接続確認中." @@ -510,10 +542,6 @@ msgstr "" msgid "YouTube URL" msgstr "" -#, fuzzy -msgid "Share" -msgstr "共有取り消し" - msgid "" "Could not load YouTube player API, please check your network / firewall " "settings." @@ -694,6 +722,37 @@ msgstr "ここで私と会う:" msgid "Room name" msgstr "あなたの名前" +msgid "" +"The request contains an invalid parameter value. Please check the URL of " +"the video you want to share and try again." +msgstr "" + +msgid "" +"The requested content cannot be played in an HTML5 player or another " +"error related to the HTML5 player has occurred. Please try again later." +msgstr "" + +msgid "" +"The video requested was not found. Please check the URL of the video you " +"want to share and try again." +msgstr "" + +msgid "" +"The owner of the requested video does not allow it to be played in " +"embedded players." +msgstr "" + +#, python-format +msgid "" +"An unknown error occurred while playing back the video (%s). Please try " +"again later." +msgstr "" + +msgid "" +"An unknown error occurred while playing back the video. Please try again " +"later." +msgstr "" + msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgstr "" diff --git a/src/i18n/messages-ko.po b/src/i18n/messages-ko.po index 596e8133..d9a4e3c5 100644 --- a/src/i18n/messages-ko.po +++ b/src/i18n/messages-ko.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed WebRTC 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2015-02-18 14:46+0100\n" +"POT-Creation-Date: 2015-04-30 19:22+0200\n" "PO-Revision-Date: 2014-04-13 20:30+0900\n" "Last-Translator: Curt Frisemo \n" "Language-Team: Curt Frisemo \n" @@ -18,9 +18,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" -msgid "Your audio level" -msgstr "음성크기" - msgid "Standard view" msgstr "" @@ -246,6 +243,38 @@ msgstr "화면 공유 옵션" msgid "Fit screen." msgstr "화면에 맟춤" +#, fuzzy +msgid "Share screen" +msgstr "화면 공유하기" + +msgid "Please select what to share." +msgstr "" + +#, fuzzy +msgid "Screen" +msgstr "화면에 맟춤" + +msgid "Window" +msgstr "" + +msgid "Application" +msgstr "" + +msgid "Share the whole screen. Click share to select the screen." +msgstr "" + +msgid "Share a single window. Click share to select the window." +msgstr "" + +msgid "" +"Share all windows of a application. This can leak content behind windows " +"when windows get moved. Click share to select the application." +msgstr "" + +#, fuzzy +msgid "Share" +msgstr "비공유" + msgid "Profile" msgstr "" @@ -460,6 +489,9 @@ msgstr "전화 받음" msgid "Waiting for camera/microphone access" msgstr "카메라/마이크 사용을 기다림" +msgid "Your audio level" +msgstr "음성크기" + msgid "Checking camera and microphone access." msgstr "카메라와 마이크의 사용을 확인 하세요" @@ -510,10 +542,6 @@ msgstr "" msgid "YouTube URL" msgstr "" -#, fuzzy -msgid "Share" -msgstr "비공유" - msgid "" "Could not load YouTube player API, please check your network / firewall " "settings." @@ -694,6 +722,37 @@ msgstr "나를 여기서 만납니다:" msgid "Room name" msgstr "사용자 이름" +msgid "" +"The request contains an invalid parameter value. Please check the URL of " +"the video you want to share and try again." +msgstr "" + +msgid "" +"The requested content cannot be played in an HTML5 player or another " +"error related to the HTML5 player has occurred. Please try again later." +msgstr "" + +msgid "" +"The video requested was not found. Please check the URL of the video you " +"want to share and try again." +msgstr "" + +msgid "" +"The owner of the requested video does not allow it to be played in " +"embedded players." +msgstr "" + +#, python-format +msgid "" +"An unknown error occurred while playing back the video (%s). Please try " +"again later." +msgstr "" + +msgid "" +"An unknown error occurred while playing back the video. Please try again " +"later." +msgstr "" + msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgstr "" diff --git a/src/i18n/messages-zh-cn.po b/src/i18n/messages-zh-cn.po index abb8904b..c154f344 100644 --- a/src/i18n/messages-zh-cn.po +++ b/src/i18n/messages-zh-cn.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed WebRTC 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2015-02-18 14:46+0100\n" +"POT-Creation-Date: 2015-04-30 19:22+0200\n" "PO-Revision-Date: 2014-05-21 09:54+0800\n" "Last-Translator: Michael P.\n" "Language-Team: Curt Frisemo \n" @@ -18,9 +18,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" -msgid "Your audio level" -msgstr "您的通话音量" - msgid "Standard view" msgstr "" @@ -246,6 +243,38 @@ msgstr "屏幕共享设置" msgid "Fit screen." msgstr "适合屏幕" +#, fuzzy +msgid "Share screen" +msgstr "共享您的屏幕" + +msgid "Please select what to share." +msgstr "" + +#, fuzzy +msgid "Screen" +msgstr "适合屏幕" + +msgid "Window" +msgstr "" + +msgid "Application" +msgstr "" + +msgid "Share the whole screen. Click share to select the screen." +msgstr "" + +msgid "Share a single window. Click share to select the window." +msgstr "" + +msgid "" +"Share all windows of a application. This can leak content behind windows " +"when windows get moved. Click share to select the application." +msgstr "" + +#, fuzzy +msgid "Share" +msgstr "停止分享" + msgid "Profile" msgstr "" @@ -460,6 +489,9 @@ msgstr "接受通话" msgid "Waiting for camera/microphone access" msgstr "等待摄像头/麦克风连接" +msgid "Your audio level" +msgstr "您的通话音量" + msgid "Checking camera and microphone access." msgstr "正在检查摄像头及麦克风连接" @@ -510,10 +542,6 @@ msgstr "" msgid "YouTube URL" msgstr "" -#, fuzzy -msgid "Share" -msgstr "停止分享" - msgid "" "Could not load YouTube player API, please check your network / firewall " "settings." @@ -693,6 +721,37 @@ msgstr "我们这里见:" msgid "Room name" msgstr "您的名字" +msgid "" +"The request contains an invalid parameter value. Please check the URL of " +"the video you want to share and try again." +msgstr "" + +msgid "" +"The requested content cannot be played in an HTML5 player or another " +"error related to the HTML5 player has occurred. Please try again later." +msgstr "" + +msgid "" +"The video requested was not found. Please check the URL of the video you " +"want to share and try again." +msgstr "" + +msgid "" +"The owner of the requested video does not allow it to be played in " +"embedded players." +msgstr "" + +#, python-format +msgid "" +"An unknown error occurred while playing back the video (%s). Please try " +"again later." +msgstr "" + +msgid "" +"An unknown error occurred while playing back the video. Please try again " +"later." +msgstr "" + msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgstr "" diff --git a/src/i18n/messages-zh-tw.po b/src/i18n/messages-zh-tw.po index 0d4abd54..5cadd480 100644 --- a/src/i18n/messages-zh-tw.po +++ b/src/i18n/messages-zh-tw.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed WebRTC 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2015-02-18 14:46+0100\n" +"POT-Creation-Date: 2015-04-30 19:22+0200\n" "PO-Revision-Date: 2014-05-21 09:55+0800\n" "Last-Translator: Michael P.\n" "Language-Team: Curt Frisemo \n" @@ -18,9 +18,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" -msgid "Your audio level" -msgstr "您的通話音量" - msgid "Standard view" msgstr "" @@ -246,6 +243,38 @@ msgstr "屏幕共享設置" msgid "Fit screen." msgstr "適合屏幕" +#, fuzzy +msgid "Share screen" +msgstr "共享您的屏幕" + +msgid "Please select what to share." +msgstr "" + +#, fuzzy +msgid "Screen" +msgstr "適合屏幕" + +msgid "Window" +msgstr "" + +msgid "Application" +msgstr "" + +msgid "Share the whole screen. Click share to select the screen." +msgstr "" + +msgid "Share a single window. Click share to select the window." +msgstr "" + +msgid "" +"Share all windows of a application. This can leak content behind windows " +"when windows get moved. Click share to select the application." +msgstr "" + +#, fuzzy +msgid "Share" +msgstr "停止分享" + msgid "Profile" msgstr "" @@ -460,6 +489,9 @@ msgstr "接受通話" msgid "Waiting for camera/microphone access" msgstr "等待攝像頭/麥克風連接" +msgid "Your audio level" +msgstr "您的通話音量" + msgid "Checking camera and microphone access." msgstr "正在檢查攝像頭及麥克風連接" @@ -510,10 +542,6 @@ msgstr "" msgid "YouTube URL" msgstr "" -#, fuzzy -msgid "Share" -msgstr "停止分享" - msgid "" "Could not load YouTube player API, please check your network / firewall " "settings." @@ -693,6 +721,37 @@ msgstr "我們這裡見:" msgid "Room name" msgstr "您的名字" +msgid "" +"The request contains an invalid parameter value. Please check the URL of " +"the video you want to share and try again." +msgstr "" + +msgid "" +"The requested content cannot be played in an HTML5 player or another " +"error related to the HTML5 player has occurred. Please try again later." +msgstr "" + +msgid "" +"The video requested was not found. Please check the URL of the video you " +"want to share and try again." +msgstr "" + +msgid "" +"The owner of the requested video does not allow it to be played in " +"embedded players." +msgstr "" + +#, python-format +msgid "" +"An unknown error occurred while playing back the video (%s). Please try " +"again later." +msgstr "" + +msgid "" +"An unknown error occurred while playing back the video. Please try again " +"later." +msgstr "" + msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgstr "" diff --git a/src/i18n/messages.pot b/src/i18n/messages.pot index b856e386..584420a1 100644 --- a/src/i18n/messages.pot +++ b/src/i18n/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Spreed WebRTC 1.0\n" "Report-Msgid-Bugs-To: simon@struktur.de\n" -"POT-Creation-Date: 2015-02-18 14:46+0100\n" +"POT-Creation-Date: 2015-04-30 19:22+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,9 +18,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" -msgid "Your audio level" -msgstr "" - msgid "Standard view" msgstr "" @@ -242,6 +239,35 @@ msgstr "" msgid "Fit screen." msgstr "" +msgid "Share screen" +msgstr "" + +msgid "Please select what to share." +msgstr "" + +msgid "Screen" +msgstr "" + +msgid "Window" +msgstr "" + +msgid "Application" +msgstr "" + +msgid "Share the whole screen. Click share to select the screen." +msgstr "" + +msgid "Share a single window. Click share to select the window." +msgstr "" + +msgid "" +"Share all windows of a application. This can leak content behind windows " +"when windows get moved. Click share to select the application." +msgstr "" + +msgid "Share" +msgstr "" + msgid "Profile" msgstr "" @@ -454,6 +480,9 @@ msgstr "" msgid "Waiting for camera/microphone access" msgstr "" +msgid "Your audio level" +msgstr "" + msgid "Checking camera and microphone access." msgstr "" @@ -503,9 +532,6 @@ msgstr "" msgid "YouTube URL" msgstr "" -msgid "Share" -msgstr "" - msgid "" "Could not load YouTube player API, please check your network / firewall " "settings." @@ -679,6 +705,37 @@ msgstr "" msgid "Room name" msgstr "" +msgid "" +"The request contains an invalid parameter value. Please check the URL of " +"the video you want to share and try again." +msgstr "" + +msgid "" +"The requested content cannot be played in an HTML5 player or another " +"error related to the HTML5 player has occurred. Please try again later." +msgstr "" + +msgid "" +"The video requested was not found. Please check the URL of the video you " +"want to share and try again." +msgstr "" + +msgid "" +"The owner of the requested video does not allow it to be played in " +"embedded players." +msgstr "" + +#, python-format +msgid "" +"An unknown error occurred while playing back the video (%s). Please try " +"again later." +msgstr "" + +msgid "" +"An unknown error occurred while playing back the video. Please try again " +"later." +msgstr "" + msgid "Unknown URL format. Please make sure to enter a valid YouTube URL." msgstr "" diff --git a/src/styles/Makefile.am b/src/styles/Makefile.am index 1a8f2980..d47a9be7 100644 --- a/src/styles/Makefile.am +++ b/src/styles/Makefile.am @@ -23,19 +23,29 @@ SASSFLAGS = --style=compressed --no-cache --sourcemap=none AUTOPREFIXER_BROWSER_SUPPORT := "> 1%, last 2 versions, Firefox ESR, Opera 12.1" STATIC = ../../static -styles: +pre: @if [ "$(SASS)" = "" ]; then echo "Command 'sass' not found, required when building styles"; exit 1; fi - @if [ "$(AUTOPREFIXER)" = "" ]; then echo "Command 'autoprefixer' not found, required when building styles"; exit 1; fi @if [ "$(NODEJS_SUPPORT_STYLES)" = "no" ]; then echo "Your version of node.js does not support building styles"; exit 1; fi @if [ "$(SASS_SUPPORT_STYLES)" = "no" ]; then echo "Your version of sass does not support building styles"; exit 1; fi $(MKDIR_P) $(STATIC)/css + +styles: bootstrap.min.css font-awesome.min.css csp.min.css main.min.css + +main.min.css: pre + @if [ "$(AUTOPREFIXER)" = "" ]; then echo "Command 'autoprefixer' not found, required when building main.css styles"; exit 1; fi $(SASS) --compass --scss $(SASSFLAGS) \ $(CURDIR)/main.scss:$(STATIC)/css/main.min.css $(AUTOPREFIXER) --browsers $(AUTOPREFIXER_BROWSER_SUPPORT) $(STATIC)/css/main.min.css + +bootstrap.min.css: pre $(SASS) --compass --scss $(SASSFLAGS) \ $(CURDIR)/bootstrap.scss:$(STATIC)/css/bootstrap.min.css + +font-awesome.min.css: pre $(SASS) --compass --scss $(SASSFLAGS) \ $(CURDIR)/font-awesome.scss:$(STATIC)/css/font-awesome.min.css + +csp.min.css: pre $(SASS) --compass --scss $(SASSFLAGS) \ $(CURDIR)/csp.scss:$(STATIC)/css/csp.min.css @@ -46,4 +56,4 @@ styleshint: styleslint: @if [ "$(SCSS_LINT)" = "" ]; then echo "Command 'scss-lint' not found, required when linting styles"; exit 1; fi - $(SCSS_LINT) -c scss.yml + $(FIND) ./ -not -path "./libs/*" -name "*.scss" -print0 | xargs -0 -n1 $(SCSS_LINT) -c scss.yml diff --git a/src/styles/_shame.scss b/src/styles/_shame.scss index 9822aa0f..a2f16225 100644 --- a/src/styles/_shame.scss +++ b/src/styles/_shame.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/components/_audiolevel.scss b/src/styles/components/_audiolevel.scss index 81d2f253..b632c10f 100644 --- a/src/styles/components/_audiolevel.scss +++ b/src/styles/components/_audiolevel.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,7 @@ * */ -#audiolevel { +#audiolevel { // scss-lint:disable IdSelector left: 0; margin: 0 auto; position: fixed; diff --git a/src/styles/components/_audiovideo.scss b/src/styles/components/_audiovideo.scss index 59a2027e..5766ff84 100644 --- a/src/styles/components/_audiovideo.scss +++ b/src/styles/components/_audiovideo.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,16 +19,36 @@ * */ -#audiovideo { +.mainScreenshare, +.mainPresentation { + // scss-lint:disable IdSelector + #audiovideo { + @include breakpt($breakpoint-video-medium, max-width, only screen) { + display: none; + } + } +} + +.withChat, +.withBuddylist { + // scss-lint:disable IdSelector + #audiovideo { + right: 260px; + } +} + +.withBuddylist.withChat #audiovideo { // scss-lint:disable IdSelector + right: 520px; +} + +#audiovideo { // scss-lint:disable IdSelector bottom: 0; left: 0; position: absolute; right: 0; top: $minbarheight; user-select: none; -} -#audiovideo { @include breakpt($breakpoint-video-small, max-width, only screen) { right: 0; } @@ -45,22 +65,6 @@ } } -.mainScreenshare #audiovideo, -.mainPresentation #audiovideo { - @include breakpt($breakpoint-video-medium, max-width, only screen) { - display: none; - } -} - -.withChat #audiovideo, -.withBuddylist #audiovideo { - right: 260px; -} - -.withBuddylist.withChat #audiovideo { - right: 520px; -} - .audiovideo { bottom: 0; left: 0; @@ -109,9 +113,33 @@ video { object-fit: cover; } -} -.audiovideo { + .onlyaudio { + bottom: 0; + color: $video-onlyaudio; + display: none; + font-size: 1em; + left: 0; + pointer-events: auto; + position: absolute; + right: 0; + text-align: center; + top: 0; + + &:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + } + + > * { + font-size: 6em; + vertical-align: middle; + } + } + + .remoteContainer { bottom: 0; left: 0; @@ -142,6 +170,19 @@ } } + &.cameraMute .miniContainer, + &.cameraMute .localVideos { + background: $video-onlyaudio-background; + + .onlyaudio { + display: block; + } + + video { + visibility: hidden; + } + } + .miniVideo { display: block; height: 100%; @@ -159,131 +200,128 @@ transition-property: opacity; width: 100%; } -} - -.audiovideo .remoteVideos { - bottom: 0; - left: 0; - opacity: 0; - position: absolute; - right: 0; - top: 0; - transition-duration: 2s; - transition-property: opacity; - video { - display: block; - height: 100%; - width: 100%; + .localVideos { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + transition-duration: 2s; + transition-property: opacity; } -} - -.audiovideo .remoteVideo { - background: $video-background; - display: inline-block; - max-height: 100%; - max-width: 100%; - overflow: hidden; - position: relative; - vertical-align: bottom; - //visibility: hidden; - width: 100%; - &.withvideo { - //visibility: visible; - } + .remoteVideos { + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; + transition-duration: 2s; + transition-property: opacity; - &.onlyaudio { - background: $video-onlyaudio-background; - //visibility: visible; + video { + display: block; + height: 100%; + width: 100%; + } } - .onlyaudio { - color: $video-onlyaudio; - display: none; - font-size: 80px; + .overlayActions { + background: $video-overlayactions; + bottom: 0; + height: 140px; left: 0; - margin-top: -40px; - pointer-events: auto; + margin: auto 0; + opacity: 0; + padding: 3px 0; position: absolute; - right: 0; - text-align: center; - top: 45%; - } + top: 0; + width: 40px; + z-index: 5; - &.onlyaudio video, - &.dummy video { - visibility: hidden; + .btn { + color: #ccc; + cursor: pointer; + display: block; + outline: 0; + text-shadow: 0 0 5px #000; + width: 40px; + } } - &.onlyaudio .onlyaudio { - display: block; - } + .remoteVideo { + background: $video-background; + display: inline-block; + max-height: 100%; + max-width: 100%; + overflow: hidden; + position: relative; + vertical-align: bottom; + //visibility: hidden; + width: 100%; - &.dummy .onlyaudio { - display: block; - } + &.withvideo { + //visibility: visible; + } - .peerActions { - bottom: 5%; - left: 40px; - opacity: 0; - pointer-events: auto; - position: absolute; - right: 40px; - text-align: center; - transition-duration: .2s; - transition-property: opacity; - z-index: 10; + &.onlyaudioVideo { + background: $video-onlyaudio-background; + //visibility: visible; - &:hover { - opacity: .5; + .onlyaudio { + display: block; + } } - i { - font-size: 3vw; + &.onlyaudioVideo video, + &.dummy video { + visibility: hidden; } - } - .peerLabel { - bottom: 4%; - color: #fff; - font-size: 2.5vw; - left: 4%; - max-width: 30%; - opacity: .7; - overflow: hidden; - padding: 4px; - position: absolute; - text-overflow: ellipsis; - text-shadow: 0 0 4px #000; - white-space: nowrap; - z-index: 8; - } -} + &.dummy .onlyaudio { + display: block; + } -.audiovideo .overlayActions { - background: $video-overlayactions; - bottom: 0; - height: 140px; - left: 0; - margin: auto 0; - opacity: 0; - padding: 3px 0; - position: absolute; - top: 0; - width: 40px; - z-index: 5; + .peerActions { + // scss:lint:disable NestingDepth, SelectorDepth + bottom: 5%; + left: 40px; + opacity: 0; + pointer-events: auto; + position: absolute; + right: 40px; + text-align: center; + transition-duration: .2s; + transition-property: opacity; + z-index: 10; + + &:hover { + opacity: .5; + } + + i { + font-size: 3vw; + } + } - .btn { - color: #ccc; - cursor: pointer; - display: block; - outline: 0; - text-shadow: 0 0 5px #000; - width: 40px; + .peerLabel { + bottom: 4%; + color: #fff; + font-size: 2.5vw; + left: 4%; + max-width: 30%; + opacity: .7; + overflow: hidden; + padding: 4px; + position: absolute; + text-overflow: ellipsis; + text-shadow: 0 0 4px #000; + white-space: nowrap; + z-index: 8; + } } - } .remoteVideo { @@ -326,29 +364,8 @@ } } -.renderer-auditorium { - position: relative; - - span:before { - content: '\f183'; - left: 50%; - margin-left: -.8em; - margin-top: -.5em; - position: absolute; - top: 50%; - } - - span:after { - content: '\f183'; - margin-right: -.9em; - margin-top: -.5em; - position: absolute; - right: 50%; - top: 50%; - } -} - .renderer-smally { + // scss-lint:disable SelectorDepth background: #000; border-right: 0; border-top: 0; @@ -379,10 +396,12 @@ } .renderer-onepeople { + .miniContainer .onlyaudio { + font-size: .4em; + } } .renderer-democrazy { - .remoteVideos .miniContainer { bottom: auto; display: inline-block; @@ -396,10 +415,10 @@ .active .miniContainer { opacity: 1; } - } .renderer-conferencekiosk { + // scss-lint:disable SelectorDepth .remoteVideos { background: $video-background; bottom: 2px; @@ -454,6 +473,26 @@ } .renderer-auditorium { + position: relative; + + span:before { + content: '\f183'; + left: 50%; + margin-left: -.8em; + margin-top: -.5em; + position: absolute; + top: 50%; + } + + span:after { + content: '\f183'; + margin-right: -.9em; + margin-top: -.5em; + position: absolute; + right: 50%; + top: 50%; + } + .remoteContainer { border-left: 40px solid #000; } @@ -464,10 +503,6 @@ top: 180px; width: 320px; - .overlayLogo { - display: none; - } - video { height: 100%; margin-top: -9px; @@ -511,7 +546,8 @@ height: 180px; width: 320px; - video { + .remoteVideo, + .video { height: 100%; width: 100%; } diff --git a/src/styles/components/_bar.scss b/src/styles/components/_bar.scss index f1903eea..366b929f 100644 --- a/src/styles/components/_bar.scss +++ b/src/styles/components/_bar.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -37,42 +37,43 @@ padding: 2px 5px 0 11px; padding: 2px 5px 0 11px; } +} - .logo { - background: $logo no-repeat; - background-size: 100%; - color: #000; - display: inline-block; - font: normal 11px/11px $font-sans-serif; - height: 32px; - text-align: left; - vertical-align: middle; - width: 90px; +.logo { + background: $logo no-repeat; + background-size: 100%; + color: #000; + display: inline-block; + font: normal 11px/11px $font-sans-serif; + height: 32px; + text-align: left; + vertical-align: middle; + width: 90px; - @include breakpt($breakpoint-medium) { - background: $scalable-logo no-repeat center; - height: 46px; - width: 46px; + @include breakpt($breakpoint-medium) { + background: $scalable-logo no-repeat center; + height: 46px; + width: 46px; - span { - display: none; - } + .desc { + display: none; } + } - span { - font-style: italic; - left: 38px; - position: relative; - top: 26px; - } + .desc { + font-style: italic; + left: 38px; + position: relative; + top: 26px; - span a { + a { color: $bar-logo-text-desc; } } } .bar .middle { + // scss-lint:disable SelectorDepth left: 0; pointer-events: none; position: absolute; @@ -178,23 +179,22 @@ color: #fff; } } +} - .btn-mutemicrophone i:before { - content: '\f130'; - } - - .btn-mutemicrophone.active i:before { - content: '\f131'; - } +.btn-mutemicrophone i:before { + content: '\f130'; +} - .btn-mutecamera i:before { - content: '\f06e'; - } +.btn-mutemicrophone.active i:before { + content: '\f131'; +} - .btn-mutecamera.active i:before { - content: '\f070'; - } +.btn-mutecamera i:before { + content: '\f06e'; +} +.btn-mutecamera.active i:before { + content: '\f070'; } @keyframes shakeityeah { diff --git a/src/styles/components/_buddylist.scss b/src/styles/components/_buddylist.scss index 7c12a828..2ea4ced0 100644 --- a/src/styles/components/_buddylist.scss +++ b/src/styles/components/_buddylist.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,7 @@ * */ -#buddylist { +#buddylist { // scss-lint:disable IdSelector bottom: 0; position: absolute; right: 0; @@ -28,7 +28,7 @@ z-index: 50; } -#buddylist:before { +#buddylist:before { // scss-lint:disable IdSelector background: $buddylist-tab-background; border-bottom: 1px solid $bordercolor; border-bottom-left-radius: 6px; @@ -55,12 +55,12 @@ z-index: 1; } -.withBuddylist #buddylist:before { +.withBuddylist #buddylist:before { // scss-lint:disable IdSelector content: '\f101'; padding-right: 0; } -.withBuddylistAutoHide #buddylist:before { +.withBuddylistAutoHide #buddylist:before { // scss-lint:disable IdSelector display: block; } @@ -76,9 +76,7 @@ position: absolute; right: 0; top: 0; -} -.buddylist { &.loading { .buddylistloading { display: block; @@ -139,13 +137,11 @@ position: relative; text-align: left; width: 100%; -} -.buddy:hover { - background: $buddylist-action-background; -} + &:hover { + background: $buddylist-action-background; + } -.buddy { &.withSubline .buddy1, &.contact .buddy1 { top: 15px; @@ -156,8 +152,15 @@ display: block; } - &.hovered .buddyactions { - right: 0; + &.hovered { + + .buddyactions { + right: 0; + } + + .buddysessions { + max-height: 999px; + } } .fa.contact:before { @@ -249,68 +252,65 @@ white-space: nowrap; width: 120px; } -} -.buddy .buddyactions { - background: $buddylist-action-background; - height: 66px; - line-height: 66px; - padding: 0 10px; - position: absolute; - right: -125px; - text-align: right; - top: 0; - transition-duration: .3s; - transition-property: right; - white-space: nowrap; - z-index: 5; - - .btn { - font-size: $buddylist-action-font-size; - height: 40px; - line-height: 40px; - padding: 0; - text-align: center; - vertical-align: middle; - width: 42px; + .buddyactions { + background: $buddylist-action-background; + height: 66px; + line-height: 66px; + padding: 0 10px; + position: absolute; + right: -125px; + text-align: right; + top: 0; + transition-duration: .3s; + transition-property: right; + white-space: nowrap; + z-index: 5; + + .btn { + font-size: $buddylist-action-font-size; + height: 40px; + line-height: 40px; + padding: 0; + text-align: center; + vertical-align: middle; + width: 42px; + } } -} -.buddy .buddysessions { - margin-bottom: 10px; - margin-top: 56px; - max-height: 0; - transition-delay: .1s; - transition-duration: .5s; - transition-property: max-height; - - ul { - border-left: 1px dotted $bordercolor; - border-right: 1px dotted $bordercolor; - margin: 0 14px; - padding-left: 0; - padding-top: 10px; - } + .buddysessions { + margin-bottom: 10px; + margin-top: 56px; + max-height: 0; + transition-delay: .1s; + transition-duration: .5s; + transition-property: max-height; + + ul { + border-left: 1px dotted $bordercolor; + border-right: 1px dotted $bordercolor; + margin: 0 14px; + padding-left: 0; + padding-top: 10px; + } - ul li { - list-style-type: none; - margin-bottom: 2px; - margin-left: 0; + ul li { + // scss-lint:disable NestingDepth + list-style-type: none; + margin-bottom: 2px; + margin-left: 0; - .btn-group { - visibility: hidden; - } + .btn-group { + visibility: hidden; + } - &:hover .btn-group { - visibility: visible; + &:hover .btn-group { + visibility: visible; + } } - } - .currentsession .buddy3 { - font-weight: bold; + .currentsession .buddy3 { + font-weight: bold; + } } } - -.buddy.hovered .buddysessions { - max-height: 999px; -} diff --git a/src/styles/components/_buddypicturecapture.scss b/src/styles/components/_buddypicturecapture.scss index 225e9cc1..9afe92a2 100644 --- a/src/styles/components/_buddypicturecapture.scss +++ b/src/styles/components/_buddypicturecapture.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -61,10 +61,10 @@ top: 0; visibility: hidden; z-index: 5; - } - .videoFlash.flash { - visibility: visible; + &.flash { + visibility: visible; + } } .preview { diff --git a/src/styles/components/_buddypictureupload.scss b/src/styles/components/_buddypictureupload.scss index 4f9edace..9f4eda98 100644 --- a/src/styles/components/_buddypictureupload.scss +++ b/src/styles/components/_buddypictureupload.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -39,6 +39,7 @@ } .showUploadPicture { + // scss-lint:disable NestingDepth background-color: $componentbg; border: 1px solid $bordercolor; height: 200px; @@ -52,10 +53,14 @@ &.imgData { background-color: #000; - } - &.imgData .chooseUploadPicture { - display: none; + .chooseUploadPicture { + display: none; + } + + &:hover .imageUtilites { + visibility: visible; + } } .chooseUploadPicture { @@ -95,10 +100,6 @@ } } - .showUploadPicture.imgData:hover .imageUtilites { - visibility: visible; - } - .moveHorizontal { position: relative; top: -4px; diff --git a/src/styles/components/_chat.scss b/src/styles/components/_chat.scss index b654af85..7fddb035 100644 --- a/src/styles/components/_chat.scss +++ b/src/styles/components/_chat.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,7 @@ * */ -#chat { +#chat { // scss-lint:disable IdSelector bottom: 0; min-width: $chat-width; opacity: 0; @@ -32,6 +32,7 @@ } .withChat { + // scss-lint:disable IdSelector #chat { opacity: 1; } @@ -135,6 +136,7 @@ } .chat { + // scss-lint:disable NestingDepth background: $chat-background; bottom: 0; display: none; @@ -143,20 +145,363 @@ position: absolute; right: 0; top: 0; -} -.chat { - &.active { - &.visible { + &.newmessage { + .chatheadertitle:after { + content: '***'; + position: absolute; + right: 32px; + top: 2px; + } + + .chatheader { + animation: newmessage 1s ease -.3s infinite; + } + } + + &.active.visible { + display: block; + } + + &.with_pictures .message { + &.is_self { + padding-right: 54px; + + .timestamp { + right: 58px; + } + } + + &.is_remote { + padding-left: 58px; + } + } + + .chatbodybottom { + background: $chat-bottom-background; + bottom: 1px; + left: 0; + margin: 0 auto; + position: absolute; + right: 0; + + @include breakpt($breakpoint-chat-small, max-height) { + height: auto; + } + } + + .typinghint { + color: $chat-typing; + font-size: .8em; + height: 14px; + overflow: hidden; + padding: 0 6px; + white-space: nowrap; + + @include breakpt($breakpoint-chat-small, max-height) { + display: none; + } + } + + .inputbox { + position: relative; + + @include breakpt($breakpoint-chat-small, max-height) { + height: auto; + } + + .btn { + display: none; + padding: .5em 1em; + position: absolute; + right: 6px; + top: 1px; + } + + > div { + border-top: 1px solid $bordercolor; + } + } + + .input { + border-color: transparent; + border-radius: 0; + box-shadow: none; + display: block; + height: 54px; + margin: 0; + max-height: 54px; /* FF hack */ + resize: none; + width: 100%; + + @include breakpt($breakpoint-chat-small, max-height) { + max-height: 2.5em; + } + + &:active, + &:focus { + border-color: $chat-input-border-color; + } + } + + .outputbox { + bottom: 75px; + left: 0; + position: absolute; + right: 0; + top: 0; + + @include breakpt($breakpoint-chat-small, max-height) { + bottom: 45px; + } + } + + .output { + height: 100%; + overflow-x: hidden; + overflow-y: auto; + padding: .4em 0; + + > i { + clear: both; + color: $chat-meta; display: block; + font-size: .8em; + padding: 6px 0; + text-align: center; + + &.p2p { + font-weight: bold; + padding: 6px 0; + } + } + } + + .message { + // scss-lint:disable DuplicateProperty + background: $chat-msg-background; + border: 1px solid $chat-msg-border; + border-radius: 6px; + box-shadow: 0 0 2px 0 $chat-msg-shadow; + clear: both; + display: block; + margin: 0 4px 2px 18px; + padding: 8px; + position: relative; + word-wrap: break-word; + + ul { + list-style-type: none; + margin: 0; + padding-left: 0; } - .chatbody { - // nothing + .timestamp { + color: $chat-timestamp; + font-size: .8em; + position: absolute; + right: 8px; + text-align: right; + top: 8px; } - .chatheader { - // nothing + .timestamp-space { + float: right; + height: 10px; + width: 40px; + } + + strong { + display: block; + margin-right: 40px; + overflow: hidden; + padding-bottom: 2px; + text-overflow: ellipsis; + white-space: nowrap; + } + + li { + line-height: 1.1em; + margin: 4px 0; + padding-left: 1.2em; + position: relative; + + &:before { + color: $chat-msg-default-icon-color; + content: '\f075'; + font-family: FontAwesome; + left: 0; + position: absolute; + text-align: center; + width: 12px; + } + + &.unread:before { + color: $chat-msg-unread-icon-color; + content: $chat-msg-unread-icon; + } + + &.sending:before { + color: $chat-msg-sending-icon-color; + content: $chat-msg-sending-icon; + } + + &.sent:before { + color: $chat-msg-sent-icon-color; + content: $chat-msg-sent-icon; + } + + &.delivered:before { + color: $chat-msg-delivered-icon-color; + content: $chat-msg-delivered-icon; + } + + &.received:before { + color: $chat-msg-received-icon-color; + content: $chat-msg-received-icon; + } + + &.read:before { + color: $chat-msg-read-icon-color; + content: $chat-msg-read-icon; + } + } + + .buddyPicture { + background: $actioncolor1; + border-radius: 2px; + height: 46px; + left: 4px; + overflow: hidden; + position: absolute; + text-align: center; + top: 4px; + width: 46px; + z-index: 0; + + .#{$fa-css-prefix} { + color: $actioncolor2; + line-height: 46px; + } + + img { + bottom: 0; + display: block; + left: 0; + max-height: 100%; + max-width: 100%; + position: absolute; + right: 0; + top: 0; + } + } + + &:before, + &:after { + border-style: solid; + content: ''; + display: block; + position: absolute; + width: 0; + } + + &.is_remote { + background: $chat-msg-remote-background; + + &:before { // arrow border + border-color: transparent $chat-arrow-border; + border-width: 7px 11px 7px 0; + bottom: auto; + left: -12px; + top: 4px; + } + + &:after { // arrow background + border-color: transparent $chat-msg-remote-background; + border-width: 6px 10px 6px 0; + bottom: auto; + left: -11px; + top: 5px; + } + } + + &.is_self { + background: $chat-msg-self-background; + margin-left: 4px; + margin-right: 18px; + padding-right: 0; + + &:before { // arrow border + border-color: transparent $chat-arrow-border; + border-width: 7px 0 7px 11px; + bottom: 4px; + bottom: auto; + right: -12px; + } + + &:after { // arrow background + border-color: transparent $chat-msg-self-background; + border-width: 6px 0 6px 10px; + bottom: 5px; + bottom: auto; + right: -11px; + } + + li:before { + color: $chat-msg-default-icon-color; + transform: scale(-1, 1); + } + + .buddyPicture { + left: auto; + right: 4px; + } + } + + &.with_name { + // none + } + + &.with_hoverimage { + + .buddyPicture { + overflow: visible; + z-index: initial; + + &:hover .buddyInfoActions { + height: 40px; + opacity: 1; + } + } + + .buddyInfoActions { + cursor: default; + display: inline-block; + height: 0; + left: 0; + opacity: 0; + overflow: hidden; + position: absolute; + top: 48px; + transition: opacity 0.1s .1s linear, height .4s .1s ease-out; + white-space: nowrap; + z-index: 1; + + .btn-group { + display: block; + margin: 0 auto; + width: 55px; + } + + .btn-primary { + padding: 2px 5px; + } + + .fa { + color: #fff; + line-height: 24px; + } + } } } } @@ -245,331 +590,8 @@ } } -.chat .outputbox { - bottom: 75px; - left: 0; - position: absolute; - right: 0; - top: 0; - - @include breakpt($breakpoint-chat-small, max-height) { - bottom: 45px; - } -} - -.chat .output { - height: 100%; - overflow-x: hidden; - overflow-y: auto; - padding: .4em 0; - - > i { - clear: both; - color: $chat-meta; - display: block; - font-size: .8em; - padding: 6px 0; - text-align: center; - - &.p2p { - font-weight: bold; - padding: 6px 0; - } - } -} - -.chat.with_pictures .message { - &.is_self { - padding-right: 54px; - - .timestamp { - right: 58px; - } - } - - &.is_remote { - padding-left: 58px; - } -} - -.chat .message { - background: $chat-msg-background; - border: 1px solid $chat-msg-border; - border-radius: 6px; - box-shadow: 0 0 2px 0 $chat-msg-shadow; - clear: both; - display: block; - margin: 0 4px 2px 18px; - padding: 8px; - position: relative; - word-wrap: break-word; - - ul { - list-style-type: none; - margin: 0; - padding-left: 0; - } - - li { - line-height: 1.1em; - margin: 4px 0; - padding-left: 1.2em; - position: relative; - - &:before { - color: $chat-msg-default-icon-color; - content: '\f075'; - font-family: FontAwesome; - left: 0; - position: absolute; - text-align: center; - width: 12px; - } - } - - .timestamp { - color: $chat-timestamp; - font-size: .8em; - position: absolute; - right: 8px; - text-align: right; - top: 8px; - } - - .timestamp-space { - float: right; - height: 10px; - width: 40px; - } - - strong { - display: block; - margin-right: 40px; - overflow: hidden; - padding-bottom: 2px; - text-overflow: ellipsis; - white-space: nowrap; - } -} - -.chat .message { - &.is_self li:before { - color: $chat-msg-default-icon-color; - transform: scale(-1, 1); - } - - li { - &.unread:before { - color: $chat-msg-unread-icon-color; - content: $chat-msg-unread-icon; - } - - &.sending:before { - color: $chat-msg-sending-icon-color; - content: $chat-msg-sending-icon; - } - - &.sent:before { - color: $chat-msg-sent-icon-color; - content: $chat-msg-sent-icon; - } - - &.delivered:before { - color: $chat-msg-delivered-icon-color; - content: $chat-msg-delivered-icon; - } - - &.received:before { - color: $chat-msg-received-icon-color; - content: $chat-msg-received-icon; - } - - &.read:before { - color: $chat-msg-read-icon-color; - content: $chat-msg-read-icon; - } - } -} - -.chat .message { - &.is_self .buddyPicture { - left: auto; - right: 4px; - } - - .buddyPicture { - background: $actioncolor1; - border-radius: 2px; - height: 46px; - left: 4px; - overflow: hidden; - position: absolute; - text-align: center; - top: 4px; - width: 46px; - z-index: 0; - - .#{$fa-css-prefix} { - color: $actioncolor2; - line-height: 46px; - } - - img { - bottom: 0; - display: block; - left: 0; - max-height: 100%; - max-width: 100%; - position: absolute; - right: 0; - top: 0; - } - } -} - -.chat .message { - // scss-lint:disable DuplicateProperty - &:before, - &:after { - border-style: solid; - content: ''; - display: block; - position: absolute; - width: 0; - } - - &.is_remote { - background: $chat-msg-remote-background; - - &:before { // arrow border - border-color: transparent $chat-arrow-border; - border-width: 7px 11px 7px 0; - bottom: auto; - left: -12px; - top: 4px; - } - - &:after { // arrow background - border-color: transparent $chat-msg-remote-background; - border-width: 6px 10px 6px 0; - bottom: auto; - left: -11px; - top: 5px; - } - } - - &.is_self { - background: $chat-msg-self-background; - margin-left: 4px; - margin-right: 18px; - padding-right: 0; - - &:before { // arrow border - border-color: transparent $chat-arrow-border; - border-width: 7px 0 7px 11px; - bottom: 4px; - bottom: auto; - right: -12px; - } - - &:after { // arrow background - border-color: transparent $chat-msg-self-background; - border-width: 6px 0 6px 10px; - bottom: 5px; - bottom: auto; - right: -11px; - } - } - - &.with_name { - // none - } -} - -.chat { - .chatbodybottom { - background: $chat-bottom-background; - bottom: 1px; - left: 0; - margin: 0 auto; - position: absolute; - right: 0; - - @include breakpt($breakpoint-chat-small, max-height) { - height: auto; - } - } - - .typinghint { - color: $chat-typing; - font-size: .8em; - height: 14px; - overflow: hidden; - padding: 0 6px; - white-space: nowrap; - - @include breakpt($breakpoint-chat-small, max-height) { - display: none; - } - } - - .inputbox { - position: relative; - - @include breakpt($breakpoint-chat-small, max-height) { - height: auto; - } - - .btn { - display: none; - padding: .5em 1em; - position: absolute; - right: 6px; - top: 1px; - } - - > div { - border-top: 1px solid $bordercolor; - } - } - - .input { - border-color: transparent; - border-radius: 0; - box-shadow: none; - display: block; - height: 54px; - margin: 0; - max-height: 54px; /* FF hack */ - resize: none; - width: 100%; - - @include breakpt($breakpoint-chat-small, max-height) { - max-height: 2.5em; - } - - &:active, - &:focus { - border-color: $chat-input-border-color; - } - } -} - @keyframes newmessage { 0% {background-color: $actioncolor1;} 50% {background-color: $componentbg;} 100% {background-color: $actioncolor1;} } - -.chat.newmessage { - .chatheadertitle:after { - content: '***'; - position: absolute; - right: 32px; - top: 2px; - } - - .chatheader { - animation: newmessage 1s ease -.3s infinite; - } -} diff --git a/src/styles/components/_contactsmanager.scss b/src/styles/components/_contactsmanager.scss index f216ea58..743c1b56 100644 --- a/src/styles/components/_contactsmanager.scss +++ b/src/styles/components/_contactsmanager.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -68,22 +68,22 @@ .table { margin-bottom: 0; + } - tr:first-child td { - border-top: 0; - } + tr:first-child td { + border-top: 0; + } - .name { - text-align: left; - vertical-align: middle; - width: 40%; - } + .name { + text-align: left; + vertical-align: middle; + width: 40%; + } - .action { - padding-right: 15px; - text-align: right; - vertical-align: middle; - } + .action { + padding-right: 15px; + text-align: right; + vertical-align: middle; } } diff --git a/src/styles/components/_fileinfo.scss b/src/styles/components/_fileinfo.scss index 1a145e0f..28ed4180 100644 --- a/src/styles/components/_fileinfo.scss +++ b/src/styles/components/_fileinfo.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -27,9 +27,23 @@ padding: 1em; position: relative; text-align: center; -} -.file-info { + &.downloader { + .anim { + margin-left: -40px; + } + + .file-info-size { + margin-bottom: 10px; + } + } + + &.downloading { + .file-info-size { + border-color: $fileinfo-downloading-size-border; + } + } + > div { position: relative; z-index: 3; @@ -37,17 +51,14 @@ .file-info-bg { bottom: 0; + color: $fileinfo-icon-background-color; + font-size: 20em; left: 41px; overflow: hidden; position: absolute; right: 0; - top: -17px; + top: -82px; z-index: 2; - - .#{$fa-css-prefix} { - color: $fileinfo-icon-background-color; - font-size: 20em; - } } .actions { @@ -57,6 +68,37 @@ text-align: left; top: 14px; } + + .uploader { + // scss-lint:disable NestingDepth + .file-info-speed { + bottom: 6px; + } + + .actions { + margin-left: 30px; + opacity: 0; + } + + .anim { + margin-left: 0; + } + + .hovercontrol { + &:hover .anim { + margin-left: -50px; + } + + &:hover .actions { + margin-left: 0; + opacity: 1; + } + + > div { + transition: all .2s ease-in-out; + } + } + } } .is_remote .file-info { @@ -64,10 +106,8 @@ border: 1px solid $fileinfo-border-remote; .file-info-bg { - .#{$fa-css-prefix} { - color: $fileinfo-icon-background-color-remote; - font-size: 20em; - } + color: $fileinfo-icon-background-color-remote; + font-size: 20em; } } @@ -96,6 +136,7 @@ } > div { + //scss-lint:disable NestingDepth bottom: 0; box-shadow: none !important; left: 0; @@ -106,12 +147,13 @@ &.progress-bar { opacity: .5; - } - &.progress-bar.download { - opacity: 1; - z-index: 1; + &.download { + opacity: 1; + z-index: 1; + } } + } } @@ -123,49 +165,3 @@ right: 0; text-align: center; } - -.file-info.uploader { - .file-info-speed { - bottom: 6px; - } - - .actions { - margin-left: 30px; - opacity: 0; - } - - .anim { - margin-left: 0; - } - - .hovercontrol { - &:hover .anim { - margin-left: -50px; - } - - &:hover .actions { - margin-left: 0; - opacity: 1; - } - - > div { - transition: all .2s ease-in-out; - } - } -} - -.file-info.downloader { - .anim { - margin-left: -40px; - } - - .file-info-size { - margin-bottom: 10px; - } -} - -.file-info.downloading { - .file-info-size { - border-color: $fileinfo-downloading-size-border; - } -} diff --git a/src/styles/components/_presentation.scss b/src/styles/components/_presentation.scss index e1c227c9..835a5c68 100644 --- a/src/styles/components/_presentation.scss +++ b/src/styles/components/_presentation.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,219 +19,197 @@ * */ -.presentation { - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; -} - -.presentationpane .welcome { - padding: 0; -} - -.presentationpane .welcome h1 { - white-space: normal; -} - -.presentationpane .welcome button { - margin-top: 30px; -} - -.presentationpane .welcome .progress span { - text-shadow: none; -} -.presentationpane .welcome .progress .download-info { - color: $text-color; - left: 0; - position: absolute; - text-shadow: 1px 1px 1px #fff; - width: 100%; -} - -.mainPresentation #presentation { +.mainPresentation #presentation { // scss-lint:disable IdSelector display: block; } -.presentationpane { +.presentation { bottom: 0; left: 0; - overflow: auto; position: absolute; right: 0; top: 0; -} - -.presentationpane { - .canvasContainer { - height: 100%; - width: 100%; - } - canvas { - display: block; - margin: 0 auto; - position: relative; - } + .overlaybar { + bottom: 0; + left: 0; + right: 0; + text-align: center; - .odfcanvas { - cursor: default; - user-select: none; + .overlaybar-content { + // scss-lint:disable NestingDepth + max-width: 100%; - body { - background-color: transparent; + .pagecontrol { + height: 30px; + } } - document { - display: block; + .btn-prev { + left: 40px; } - } - .odfcontainer { - display: none; - margin: 0; - padding: 0; - } + .btn-next { + left: auto; + right: 0; + } - .odfcontainer.showonepage { - overflow: hidden; - text-align: center; + .overlaybar-button { + font-size: 20px; + line-height: 28px; + padding: 4px 6px; + position: absolute; + top: 0; + } } -} -.presentation .overlaybar { - bottom: 0; - left: 0; - right: 0; - text-align: center; -} - -.presentation .overlaybar .overlaybar-content { - max-width: 100%; -} + .thumbnail { + color: #333; + display: inline-block; + height: 122px; + margin-left: 20px; + margin-top: 20px; + position: relative; + text-shadow: none; + vertical-align: middle; + width: 160px; -.presentation .overlaybar .overlaybar-button { - font-size: 20px; - line-height: 28px; - padding: 4px 6px; - position: absolute; - top: 0; -} + &:first-child { + margin-left: 0; + } -.overlaybar-content .pagecontrol { - height: 30px; -} + &.presentable { + cursor: pointer; + } -.presentation .overlaybar .btn-prev { - left: 40px; -} + &:hover .presentation-action { + display: block; + } -.presentation .overlaybar .btn-next { - left: auto; - right: 0; -} + &:hover .notavailable { + display: block; + } -.pageinfo input { - display: inline; - width: 70px; -} + .caption { + // scss-lint:disable NestingDepth + overflow: hidden; + padding-bottom: 0; + text-overflow: ellipsis; + + .size { + font-size: 10px; + } + + .progress { + position: relative; + } + + .download-info { + bottom: 0; + color: $text-color; + left: 0; + line-height: 20px; + position: absolute; + right: 0; + text-shadow: 1px 1px 1px #fff; + top: 0; + } + } -.presentation .thumbnail { - color: #333; - display: inline-block; - height: 122px; - margin-left: 20px; - margin-top: 20px; - position: relative; - text-shadow: none; - vertical-align: middle; - width: 160px; -} + .active { + bottom: 0; + color: #84b819; + font-size: 10em; + left: 0; + opacity: .7; + position: absolute; + right: 0; + text-align: center; + top: 0; + } -.presentation .thumbnail.presentable { - cursor: pointer; -} + .notavailable { + bottom: 0; + color: #d2322d; + display: none; + font-size: 10em; + left: 0; + opacity: .25; + position: absolute; + right: 0; + text-align: center; + top: 0; + } -.presentation .thumbnail:first-child { - margin-left: 0; -} + .presentation-action { + display: none; + position: absolute; + top: 1px; + } -.presentation .thumbnail .caption { - overflow: hidden; - padding-bottom: 0; - text-overflow: ellipsis; -} + .download { + left: 1px; + } -.presentation .thumbnail .caption .size { - font-size: 10px; -} + .delete { + right: 1px; + } -.presentation .thumbnail .caption .progress { - position: relative; + .filetype { + font-size: 5em; + } + } } -.presentation .thumbnail .caption .download-info { +.presentationpane { bottom: 0; - color: $text-color; left: 0; - line-height: 20px; + overflow: auto; position: absolute; right: 0; - text-shadow: 1px 1px 1px #fff; top: 0; -} -.presentation .thumbnail .active { - bottom: 0; - color: #84b819; - font-size: 10em; - left: 0; - opacity: .7; - position: absolute; - right: 0; - text-align: center; - top: 0; -} + .welcome { + padding: 0; -.presentation .thumbnail .notavailable { - bottom: 0; - color: #d2322d; - display: none; - font-size: 10em; - left: 0; - opacity: .25; - position: absolute; - right: 0; - text-align: center; - top: 0; -} + h1 { + white-space: normal; + } -.presentation .thumbnail:hover .notavailable { - display: block; -} + .btn { + margin-top: 30px; + } -.presentation .thumbnail .presentation-action { - display: none; - position: absolute; - top: 1px; -} + .progress span { + text-shadow: none; + } -.presentation .thumbnail .download { - left: 1px; -} + .progress .download-info { + color: $text-color; + left: 0; + position: absolute; + text-shadow: 1px 1px 1px #fff; + width: 100%; + } + } -.presentation .thumbnail .delete { - right: 1px; -} + .canvasContainer { + height: 100%; + width: 100%; + overflow: hidden; -.presentation .thumbnail:hover .presentation-action { - display: block; + iframe { + border: 0; + height: 100%; + width: 100%; + } + } } -.presentation .thumbnail .filetype { - font-size: 5em; +.pageinfo input { + display: inline; + width: 70px; } .presentations { diff --git a/src/styles/components/_rightslide.scss b/src/styles/components/_rightslide.scss index 28b24f69..ce242090 100644 --- a/src/styles/components/_rightslide.scss +++ b/src/styles/components/_rightslide.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,12 @@ * */ -#rightslide { + +.withBuddylist #rightslide { // scss-lint:disable IdSelector + right: 0; +} + +#rightslide { // scss-lint:disable IdSelector bottom: 0; left: 0; pointer-events: none; @@ -28,14 +33,10 @@ top: $minbarheight; transition: right 200ms ease-in-out; z-index: 5; -} - -.withBuddylist #rightslide { - right: 0; -} -#rightslide .rightslidepane { - height: 100%; - position: relative; - width: 100%; + .rightslidepane { + height: 100%; + position: relative; + width: 100%; + } } diff --git a/src/styles/components/_roombar.scss b/src/styles/components/_roombar.scss index 3c2ec41b..fae2967c 100644 --- a/src/styles/components/_roombar.scss +++ b/src/styles/components/_roombar.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,19 +19,18 @@ * */ -#roombar { +#roombar { // scss-lint:disable IdSelector left: 0; min-width: 260px; position: absolute; right: 0; top: $minbarheight; z-index: 4; -} -#roombar .roombar { - left: 0; - position: absolute; - right: 0; - top: 0; + .roombar { + left: 0; + position: absolute; + right: 0; + top: 0; + } } - diff --git a/src/styles/components/_screenshare.scss b/src/styles/components/_screenshare.scss index ad19392f..297013b1 100644 --- a/src/styles/components/_screenshare.scss +++ b/src/styles/components/_screenshare.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,16 +19,23 @@ * */ + +.mainScreenshare #screenshare { // scss-lint:disable IdSelector + display: block; +} + .screenshare { bottom: 0; left: 0; position: absolute; right: 0; top: 0; -} -.mainScreenshare #screenshare { - display: block; + .overlaybar { + bottom: 0; + left: 0; + right: 0; + } } .screensharepane { @@ -39,9 +46,6 @@ position: absolute; right: 0; top: 0; -} - -.screensharepane { .remotescreen { position: relative; } @@ -56,9 +60,3 @@ max-height: none; width: auto; } - -.screenshare .overlaybar { - bottom: 0; - left: 0; - right: 0; -} diff --git a/src/styles/components/_settings.scss b/src/styles/components/_settings.scss index ae6f3ed3..7e52935c 100644 --- a/src/styles/components/_settings.scss +++ b/src/styles/components/_settings.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,8 @@ * */ -#settings { +#settings { // scss-lint:disable IdSelector + // scss-lint:disable NestingDepth background: $settings-background; border-left: 1px solid $bordercolor; bottom: 0; @@ -30,27 +31,27 @@ transition: right 200ms ease-in-out; width: 520px; z-index: 50; -} -#settings.show { - right: 0; - - @include breakpt($breakpoint-settings-medium, max-width, only screen) { - background: $settings-background; - left: 0; - width: auto; - } - - .form-actions { + &.show { + right: 0; @include breakpt($breakpoint-settings-medium, max-width, only screen) { - bottom: 0; - height: 60px; + background: $settings-background; left: 0; - margin-bottom: 0; - padding: 6px 0 6px 120px; - position: fixed; - right: 0; + width: auto; + } + + .form-actions { + + @include breakpt($breakpoint-settings-medium, max-width, only screen) { + bottom: 0; + height: 60px; + left: 0; + margin-bottom: 0; + padding: 6px 0 6px 120px; + position: fixed; + right: 0; + } } } } @@ -65,9 +66,7 @@ position: absolute; right: 0; top: 0; -} -.settings { @include breakpt($breakpoint-settings-medium, max-width, only screen) { padding-bottom: 10px; } diff --git a/src/styles/components/_social.scss b/src/styles/components/_social.scss index f43b5545..125d564a 100644 --- a/src/styles/components/_social.scss +++ b/src/styles/components/_social.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/components/_usability.scss b/src/styles/components/_usability.scss index 62c72463..da1dbbaf 100644 --- a/src/styles/components/_usability.scss +++ b/src/styles/components/_usability.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,24 @@ * */ -#help { + +.withChat, +.withBuddylist { + // scss-lint:disable IdSelector + #help { + right: 260px; + } +} + +.withChat.withBuddylist, +.withSettings { + // scss-lint:disable IdSelector + #help { + right: 520px; + } +} + +#help { // scss-lint:disable IdSelector bottom: 10px; color: #aaa; font-size: 1.1em; @@ -34,17 +51,7 @@ width: 350px; } -.withChat #help, -.withBuddylist #help { - right: 260px; -} - -.withChat.withBuddylist #help, -.withSettings #help { - right: 520px; -} - -#help { +.help { @include breakpt($breakpoint-useability-small, max-width, only screen) { display: none; } diff --git a/src/styles/components/_youtubevideo.scss b/src/styles/components/_youtubevideo.scss index e0b99305..bceca951 100644 --- a/src/styles/components/_youtubevideo.scss +++ b/src/styles/components/_youtubevideo.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,12 +19,66 @@ * */ +.mainYoutubevideo #youtubevideo { // scss-lint:disable IdSelector + display: block; +} + .youtubevideo { bottom: 0; left: 0; position: absolute; right: 0; top: 0; + + .click-container { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 5; + } + + .welcome { + max-width: 700px; + + h1 { + margin-top: 10px; + } + + .welcome-container { + max-width: 700px; + } + + .welcome-logo { + background: transparent; + font-size: 10em; + } + } + + .overlaybar { + bottom: 0; + left: 0; + right: 0; + } + + .overlaybar-content { + max-width: 100%; + width: 100%; + + form .overlaybar-buttons { + position: absolute; + right: 23px; + top: 6px; + } + } + + .overlaybar-input { + padding-right: 15px; + position: relative; + width: 100%; + } + } .youtubevideopane { @@ -36,11 +90,15 @@ top: 0; } -#youtubecontainer { +.youtubecontainer { position: relative; + + &.fullscreen { + width: 100%; + } } -#youtubeplayerinfo { +.youtubeplayerinfo { bottom: 10%; left: 0; opacity: 0; @@ -55,67 +113,14 @@ &:hover { opacity: .8; } -} - -#youtubeplayerinfo div { - background-color: #f9f2f4; - border-radius: 10px; - display: inline-block; - font-size: 2em; - padding: 20px 40px; -} - -.youtubevideo .click-container { - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; - z-index: 5; -} - -.youtubevideo .welcome { - max-width: 700px; -} - -.youtubevideo .welcome h1 { - margin-top: 10px; -} - -.youtubevideo .welcome .welcome-container { - max-width: 700px; -} - -.youtubevideo .welcome .welcome-logo { - background: transparent; - font-size: 10em; -} - -.mainYoutubevideo #youtubevideo { - display: block; -} - -.youtubevideo .overlaybar { - bottom: 0; - left: 0; - right: 0; -} - -.youtubevideo .overlaybar-content { - max-width: 100%; - width: 100%; -} - -.youtubevideo .overlaybar-input { - padding-right: 15px; - position: relative; - width: 100%; -} -.youtubevideo .overlaybar-content form .overlaybar-buttons { - position: absolute; - right: 23px; - top: 6px; + div { + background-color: #f9f2f4; + border-radius: 10px; + display: inline-block; + font-size: 2em; + padding: 20px 40px; + } } .volumecontrol { @@ -143,37 +148,37 @@ display: inline-block; padding: 6px 8px; vertical-align: middle; -} - -.volumebar .bar { - -webkit-appearance: none; - background-color: #aaa; - border: 1px solid #aaa; - height: 3px; - outline: 0; - width: 100px; -} - -.volumebar .bar::-webkit-slider-thumb { - -webkit-appearance: none; - background-color: #fff; - height: 20px; - width: 6px; -} - -.volumebar .bar::-moz-range-track { - background: #aaa; - border: 0; -} - -.volumebar .bar::-moz-range-thumb { - background-color: #fff; - border-radius: 0; - height: 20px; - width: 6px; -} -.volumebar .bar::-moz-focusring { - outline: 1px solid #aaa; - outline-offset: -1px; + .bar { + -webkit-appearance: none; + background-color: #aaa; + border: 1px solid #aaa; + height: 3px; + outline: 0; + width: 100px; + + &::-webkit-slider-thumb { + -webkit-appearance: none; + background-color: #fff; + height: 20px; + width: 6px; + } + + &::-moz-range-track { + background: #aaa; + border: 0; + } + + &::-moz-range-thumb { + background-color: #fff; + border-radius: 0; + height: 20px; + width: 6px; + } + + &::-moz-focusring { + outline: 1px solid #aaa; + outline-offset: -1px; + } + } } diff --git a/src/styles/csp.scss b/src/styles/csp.scss index 5fc6c5ce..cd82a8bb 100644 --- a/src/styles/csp.scss +++ b/src/styles/csp.scss @@ -1,6 +1,6 @@ /*! * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/font-awesome.scss b/src/styles/font-awesome.scss index dbb3a5c3..2f03bb05 100644 --- a/src/styles/font-awesome.scss +++ b/src/styles/font-awesome.scss @@ -2,3 +2,8 @@ @import 'compass'; @import 'global/variables'; @import 'libs/font-awesome/scss/font-awesome'; + +/* Hide all which requires FA while still loading web font */ +.wf-loading .fa { + visibility: hidden !important; +} diff --git a/src/styles/global/_animations.scss b/src/styles/global/_animations.scss index 22f3e569..eac29df3 100644 --- a/src/styles/global/_animations.scss +++ b/src/styles/global/_animations.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/global/_base.scss b/src/styles/global/_base.scss index 42d2305e..9a7ef9f5 100644 --- a/src/styles/global/_base.scss +++ b/src/styles/global/_base.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -38,7 +38,7 @@ a { cursor: pointer; } -#background { +#background { // scss-lint:disable IdSelector background: $main-background; bottom: 0; left: 0; @@ -68,4 +68,4 @@ a { :fullscreen { background: #000; -} \ No newline at end of file +} diff --git a/src/styles/global/_loader.scss b/src/styles/global/_loader.scss index 9f2e9ad4..e2d0afd8 100644 --- a/src/styles/global/_loader.scss +++ b/src/styles/global/_loader.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,7 @@ * */ -#loader { +#loader { // scss-lint:disable IdSelector background: $load-logo no-repeat center; background-size: contain; bottom: 15%; diff --git a/src/styles/global/_nicescroll.scss b/src/styles/global/_nicescroll.scss index dc0f864c..c978b90a 100644 --- a/src/styles/global/_nicescroll.scss +++ b/src/styles/global/_nicescroll.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/global/_overlaybar.scss b/src/styles/global/_overlaybar.scss index 12d0a03d..93abbf64 100644 --- a/src/styles/global/_overlaybar.scss +++ b/src/styles/global/_overlaybar.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,6 +20,7 @@ */ .overlaybar { + // scss-lint:disable QualifyingElement background: $overlaybar-background; border-bottom: 1px solid #222; border-top: 1px solid #222; @@ -30,14 +31,30 @@ text-shadow: 0 0 5px #000; user-select: none; vertical-align: middle; -} -.overlaybar { - // scss-lint:disable QualifyingElement &:hover { background: $componentfg2; } + &.notvisible { + background: transparent; + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + pointer-events: none; + + &:hover { + background: transparent; + } + + .overlaybar-content { + display: none; + } + + .overlaybar-overlay { + display: block; + } + } + .btn { text-shadow: none; } @@ -59,27 +76,6 @@ label { padding-top: 6px !important; } -} - -.overlaybar { - &.notvisible { - background: transparent; - border-bottom: 1px solid transparent; - border-top: 1px solid transparent; - pointer-events: none; - - &:hover { - background: transparent; - } - - .overlaybar-content { - display: none; - } - - .overlaybar-overlay { - display: block; - } - } .overlaybar-button { color: $overlaybar-btn; diff --git a/src/styles/global/_pages.scss b/src/styles/global/_pages.scss index f10f2f63..81752e45 100644 --- a/src/styles/global/_pages.scss +++ b/src/styles/global/_pages.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -19,7 +19,7 @@ * */ -#page { +#page { // scss-lint:disable IdSelector bottom: 0; left: 0; position: absolute; diff --git a/src/styles/global/_variables.scss b/src/styles/global/_variables.scss index dcf6f2fa..056a8ab3 100644 --- a/src/styles/global/_variables.scss +++ b/src/styles/global/_variables.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/global/_views.scss b/src/styles/global/_views.scss index e563d1b1..7541cc4e 100644 --- a/src/styles/global/_views.scss +++ b/src/styles/global/_views.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/global/_withs.scss b/src/styles/global/_withs.scss index dae95b83..8f0feb97 100644 --- a/src/styles/global/_withs.scss +++ b/src/styles/global/_withs.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/libs/_dialogs.scss b/src/styles/libs/_dialogs.scss index f08da9ff..d64d6e43 100644 --- a/src/styles/libs/_dialogs.scss +++ b/src/styles/libs/_dialogs.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/libs/angular/angular-csp.scss b/src/styles/libs/angular/angular-csp.scss index e64287ec..656a7705 100644 --- a/src/styles/libs/angular/angular-csp.scss +++ b/src/styles/libs/angular/angular-csp.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/libs/angular/angular.scss b/src/styles/libs/angular/angular.scss index 45c1b924..84498244 100644 --- a/src/styles/libs/angular/angular.scss +++ b/src/styles/libs/angular/angular.scss @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/main.scss b/src/styles/main.scss index bb04b257..e061bb08 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -1,6 +1,6 @@ /*! * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/src/styles/scss.yml b/src/styles/scss.yml index 80c15e53..1af0bd0b 100644 --- a/src/styles/scss.yml +++ b/src/styles/scss.yml @@ -1,7 +1,3 @@ -scss_files: "**/*.scss" - -exclude: "libs/**" - linters: BangFormat: enabled: true diff --git a/static/css/csp.min.css b/static/css/csp.min.css index ccb93c24..2b6e6d3a 100644 --- a/static/css/csp.min.css +++ b/static/css/csp.min.css @@ -1,6 +1,6 @@ /*! * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/css/font-awesome.min.css b/static/css/font-awesome.min.css index 83ccfc59..8b7ec653 100644 --- a/static/css/font-awesome.min.css +++ b/static/css/font-awesome.min.css @@ -1,4 +1,4 @@ /*! * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.1.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff?v=4.1.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.1.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:0.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0);-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before{content:""}.fa-check-circle:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook:before{content:""}.fa-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-square:before,.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""} + */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.1.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff?v=4.1.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.1.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:0.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0);-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before{content:""}.fa-check-circle:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook:before{content:""}.fa-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-square:before,.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.wf-loading .fa{visibility:hidden !important} diff --git a/static/css/main.min.css b/static/css/main.min.css index 7b43d08f..b5436b6e 100644 --- a/static/css/main.min.css +++ b/static/css/main.min.css @@ -1,6 +1,6 @@ /*! * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -17,4 +17,4 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * - *//*! HiDPI v2.0.1 | MIT License | git.io/hidpi */.toast-title{font-weight:bold}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#ffffff}.toast-message a:hover{color:#cccccc;text-decoration:none}.toast-close-button{position:relative;right:-0.3em;top:-0.3em;float:right;font-size:20px;font-weight:bold;color:#ffffff;-webkit-text-shadow:0 1px 0 #ffffff;text-shadow:0 1px 0 #ffffff;opacity:0.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}.toast-close-button:hover,.toast-close-button:focus{color:#000000;text-decoration:none;cursor:pointer;opacity:0.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}button.toast-close-button{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;border-radius:3px 3px 3px 3px;background-position:15px center;background-repeat:no-repeat;-webkit-box-shadow:0 0 12px #999999;box-shadow:0 0 12px #999999;color:#ffffff;opacity:0.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>:hover{-webkit-box-shadow:0 0 12px #000000;box-shadow:0 0 12px #000000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important}#toast-container>.toast-error{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important}#toast-container>.toast-success{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important}#toast-container>.toast-warning{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important}#toast-container.toast-top-center>div,#toast-container.toast-bottom-center>div{width:300px;margin:auto}#toast-container.toast-top-full-width>div,#toast-container.toast-bottom-full-width>div{width:96%;margin:auto}.toast{background-color:#030303}.toast-success{background-color:#51a351}.toast-error{background-color:#bd362f}.toast-info{background-color:#2f96b4}.toast-warning{background-color:#f89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000000;opacity:0.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width: 240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container .toast-close-button{right:-0.2em;top:-0.2em}}@media all and (min-width: 241px) and (max-width: 480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container .toast-close-button{right:-0.2em;top:-0.2em}}@media all and (min-width: 481px) and (max-width: 768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}}.dialog-header-error{background-color:#d2322d}.dialog-header-wait{background-color:#428bca}.dialog-header-notify{background-color:#eee}.dialog-header-confirm{background-color:#eee}.dialog-header-error span,.dialog-header-error h4,.dialog-header-wait span,.dialog-header-wait h4{color:#fff}.modal-content{overflow:hidden}.modal-content .modal-body{min-height:160px}[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none !important}html,body{-webkit-background-clip:padding-box;background-clip:padding-box;background-color:#e5e5e5;height:100%}body{margin:0;max-height:100%;max-width:100%;overflow:hidden;padding:0}a{cursor:pointer}#background{background:url("../img/bg-tiles.jpg");bottom:0;left:0;position:fixed;right:0;top:0;z-index:0}@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 1.3dppx){#background{background-image:url("../img/bg-tiles_x2.jpg");-webkit-background-size:198px 200px;background-size:198px 200px}}.help-block{color:#737373}.dialog-header-notify,.dialog-header-confirm{background-color:#eee}.desktopnotify-icon{background-image:url("../img/logo-48x48.png")}:-webkit-full-screen{background:#000}:-moz-full-screen{background:#000}:-ms-fullscreen{background:#000}:fullscreen{background:#000}#loader{background:url("../img/logo.svg") no-repeat center;-webkit-background-size:contain;background-size:contain;bottom:15%;left:15%;margin:auto;max-height:150px;max-width:200px;opacity:1;pointer-events:none;position:fixed;right:15%;top:15%;-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-property:opacity;transition-property:opacity;z-index:20000}#loader.done{opacity:0}#loader>div{bottom:0;color:#ddd;display:block;font-size:2em;left:0;margin:0 auto;margin-bottom:-40px;position:absolute;right:0;text-align:center;text-shadow:0 0 5px #000}#loader .loader-message{font-size:.5em}.mainview{bottom:0;display:none;left:0;position:absolute;right:0;top:51px}@media (max-width: 700px){.mainview{left:0;left:0}}.videolayoutSmally .mainview{left:150px}.videolayoutClassroom .mainview{left:360px}.withChat .mainview,.withBuddylist .mainview{right:260px}.withBuddylist.withChat .mainview{right:520px}#page{bottom:0;left:0;position:absolute;right:0;top:51px}.welcome{color:#aaa;font-size:1.1em;margin-top:80px;max-width:600px;min-height:160px;padding-left:105px;padding-right:0;position:relative;text-shadow:0 0 5px #000}@media (max-width: 700px){.welcome{margin:0 auto;padding-left:10px;padding-right:20px}}.welcome h1{margin-top:0;white-space:nowrap}@media (max-width: 700px){.welcome h1{white-space:normal}}.welcome .welcome-container{margin:0 auto}.welcome .welcome-logo{background:url("../img/logo.svg") no-repeat left top;-webkit-background-size:contain;background-size:contain;bottom:0;left:0;position:absolute;top:1px;width:90px}@media (max-width: 700px){.welcome .welcome-logo{height:70px;margin-bottom:20px;margin-top:30px;position:relative;width:70px}}.welcome .welcome-input{position:relative}.welcome .welcome-input input{padding-right:105px}.welcome .welcome-input-buttons{position:absolute;right:8px;text-shadow:none;top:6px}.welcome .welcome-input-buttons a{color:#000;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.welcome .room-link{margin-top:-10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.welcome .room-link a{color:#aaa}.welcome .rooms-history{margin-top:3em}.welcome .rooms-history a{display:inline-block;margin-right:.5em}.welcome .rooms-history a:hover{text-decoration:none}.nicescroll::-webkit-scrollbar{background-color:#e5e5e5;border:solid transparent;height:8px;width:8px}.nicescroll::-webkit-scrollbar:hover{background-color:#e5e5e5;border-left:1px solid rgba(0,0,0,0.12);border-right:1px solid rgba(0,0,0,0.12)}.nicescroll::-webkit-scrollbar-thumb{background:rgba(0,0,0,0.2)}.nicescroll::-webkit-scrollbar-thumb:active{background:rgba(0,0,0,0.4)}.fadetogglecontainer>div{position:absolute;width:100%}.animate-show.ng-hide-add{display:block !important;opacity:1;-webkit-transition:all linear .5s;transition:all linear .5s}.animate-show.ng-hide-add.ng-hide-add-active{opacity:0}.animate-show.ng-hide-remove{display:block !important;opacity:0;-webkit-transition:all linear .5s;transition:all linear .5s}.animate-show.ng-hide-remove.ng-hide-remove-active{opacity:1}.overlaybar{background:rgba(0,0,0,0.2);border-bottom:1px solid #222;border-top:1px solid #222;color:#e7e7e7;min-height:36px;padding:3px 8px 0 30px;position:absolute;text-shadow:0 0 5px #000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle}.overlaybar:hover{background:rgba(0,0,0,0.5)}.overlaybar .btn{text-shadow:none}.overlaybar .btn-link{text-shadow:0 0 5px #000}.overlaybar .form-group>*{float:left;padding-top:0}.overlaybar input[type="radio"],.overlaybar input[type="checkbox"]{margin-top:2px}.overlaybar label{padding-top:6px !important}.overlaybar.notvisible{background:transparent;border-bottom:1px solid transparent;border-top:1px solid transparent;pointer-events:none}.overlaybar.notvisible:hover{background:transparent}.overlaybar.notvisible .overlaybar-content{display:none}.overlaybar.notvisible .overlaybar-overlay{display:block}.overlaybar .overlaybar-button{color:#e7e7e7;display:block;font-size:20px;left:3px;opacity:.7;padding:4px 6px;pointer-events:auto;position:absolute;top:0;vertical-align:middle;z-index:15}.overlaybar .overlaybar-content{display:inline-block;margin-bottom:0;margin-left:.5em;max-width:60%}.overlaybar .overlaybar-content>*{padding-right:.5em}.overlaybar .overlaybar-content .input-group{max-width:160px}.overlaybar .overlaybar-overlay{display:none;margin-left:.5em;opacity:.7;padding-top:2px;text-align:left}.visible-with-contacts,.visible-with-contacts-inline{display:none}.with-contacts .visible-with-contacts{display:block}.with-contacts .visible-with-contacts-inline{display:inline-block}.with-contacts .hidden-with-contacts{display:none}#rightslide{bottom:0;left:0;pointer-events:none;position:absolute;right:-260px;top:51px;-webkit-transition:right 200ms ease-in-out;transition:right 200ms ease-in-out;z-index:5}.withBuddylist #rightslide{right:0}#rightslide .rightslidepane{height:100%;position:relative;width:100%}.bar{background:#f8f8f8;color:#262626;font:bold 1em/50px "Helvetica Neue",Helvetica,Arial,sans-serif;text-align:center;touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:60}.bar .left{padding:5px 5px 5px 15px}@media (max-width: 700px){.bar .left{padding:2px 5px 0 11px;padding:2px 5px 0 11px}}.bar .left .logo{background:url("../img/logo-small.png") no-repeat;-webkit-background-size:100%;background-size:100%;color:#000;display:inline-block;font:normal 11px/11px "Helvetica Neue",Helvetica,Arial,sans-serif;height:32px;text-align:left;vertical-align:middle;width:90px}@media (max-width: 700px){.bar .left .logo{background:url("../img/logo.svg") no-repeat center;height:46px;width:46px}.bar .left .logo span{display:none}}.bar .left .logo span{font-style:italic;left:38px;position:relative;top:26px}.bar .left .logo span a{color:#222}.bar .middle{left:0;pointer-events:none;position:absolute;right:60px;text-align:center;top:0}.bar .middle>span{background:#f8f8f8;display:inline-block;min-height:50px;pointer-events:auto}.bar .middle .userpicture{border-radius:2px;display:inline-block;height:46px;margin:-1px .5em 0;width:46px}@media (max-width: 700px){.bar .middle .userpicture{display:none}}@media (max-width: 700px){.bar .middle .status-connected,.bar .middle .status-conference,.bar .middle .status-connecting,.bar .middle .status-closed,.bar .middle .status-reconnecting,.bar .middle .status-error,.bar .middle .status-ringing{left:0;max-width:100%;position:absolute;right:0}}.bar .right{margin-top:-1px;padding-right:4px}.bar .right .badge{background-color:#84b819;border:1px solid #fff;font-size:.4em;position:absolute;right:0;top:2px}.bar .right .btn{background:#e9e9e9;border-color:#e2e2e2;color:#333;font:24px/40px "Helvetica Neue",Helvetica,Arial,sans-serif;height:42px;margin-left:-2px;padding:0;position:relative;text-align:center;width:42px}.bar .right .btn:focus{border:0;-webkit-box-shadow:0;box-shadow:0;outline:none}.bar .right .btn:hover{background-color:transparent;border-color:#e7e7e7;color:#666}.bar .right .btn.active{background-color:transparent;border-color:#e7e7e7;color:#666}.bar .right .btn.active.amutebtn{background-color:#db4f39;border-color:#db4f39;color:#fff}.bar .right .btn.active.aenablebtn{background-color:#84b819;border-color:#84b819;color:#fff}.bar .right .btn-mutemicrophone i:before{content:'\f130'}.bar .right .btn-mutemicrophone.active i:before{content:'\f131'}.bar .right .btn-mutecamera i:before{content:'\f06e'}.bar .right .btn-mutecamera.active i:before{content:'\f070'}@-webkit-keyframes shakeityeah{0%{-webkit-transform:translate(2px, 1px) rotate(0deg);transform:translate(2px, 1px) rotate(0deg)}2%{-webkit-transform:translate(-1px, -2px) rotate(-1deg);transform:translate(-1px, -2px) rotate(-1deg)}4%{-webkit-transform:translate(-3px, 0) rotate(1deg);transform:translate(-3px, 0) rotate(1deg)}8%{-webkit-transform:translate(0, 2px) rotate(0deg);transform:translate(0, 2px) rotate(0deg)}10%{-webkit-transform:translate(1px, -1px) rotate(1deg);transform:translate(1px, -1px) rotate(1deg)}12%{-webkit-transform:translate(-1px, 2px) rotate(-1deg);transform:translate(-1px, 2px) rotate(-1deg)}14%{-webkit-transform:translate(-3px, 1px) rotate(0deg);transform:translate(-3px, 1px) rotate(0deg)}16%{-webkit-transform:translate(2px, 1px) rotate(-1deg);transform:translate(2px, 1px) rotate(-1deg)}18%{-webkit-transform:translate(-1px, -1px) rotate(1deg);transform:translate(-1px, -1px) rotate(1deg)}20%{-webkit-transform:translate(2px, 2px) rotate(0deg);transform:translate(2px, 2px) rotate(0deg)}22%{-webkit-transform:translate(1px, -2px) rotate(-1deg);transform:translate(1px, -2px) rotate(-1deg)}24%{-webkit-transform:translate(0, 0) rotate(0deg);transform:translate(0, 0) rotate(0deg)}}@keyframes shakeityeah{0%{-webkit-transform:translate(2px, 1px) rotate(0deg);transform:translate(2px, 1px) rotate(0deg)}2%{-webkit-transform:translate(-1px, -2px) rotate(-1deg);transform:translate(-1px, -2px) rotate(-1deg)}4%{-webkit-transform:translate(-3px, 0) rotate(1deg);transform:translate(-3px, 0) rotate(1deg)}8%{-webkit-transform:translate(0, 2px) rotate(0deg);transform:translate(0, 2px) rotate(0deg)}10%{-webkit-transform:translate(1px, -1px) rotate(1deg);transform:translate(1px, -1px) rotate(1deg)}12%{-webkit-transform:translate(-1px, 2px) rotate(-1deg);transform:translate(-1px, 2px) rotate(-1deg)}14%{-webkit-transform:translate(-3px, 1px) rotate(0deg);transform:translate(-3px, 1px) rotate(0deg)}16%{-webkit-transform:translate(2px, 1px) rotate(-1deg);transform:translate(2px, 1px) rotate(-1deg)}18%{-webkit-transform:translate(-1px, -1px) rotate(1deg);transform:translate(-1px, -1px) rotate(1deg)}20%{-webkit-transform:translate(2px, 2px) rotate(0deg);transform:translate(2px, 2px) rotate(0deg)}22%{-webkit-transform:translate(1px, -2px) rotate(-1deg);transform:translate(1px, -2px) rotate(-1deg)}24%{-webkit-transform:translate(0, 0) rotate(0deg);transform:translate(0, 0) rotate(0deg)}}.btn-shakeityeah{-webkit-animation-duration:4s;animation-duration:4s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:shakeityeah;animation-name:shakeityeah;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-transform-origin:50% 50%;-ms-transform-origin:50% 50%;transform-origin:50% 50%}#buddylist{bottom:0;position:absolute;right:0;top:0;width:285px;z-index:50}#buddylist:before{background:#f8f8f8;border-bottom:1px solid #e7e7e7;border-bottom-left-radius:6px;border-left:1px solid #e7e7e7;border-top:1px solid #e7e7e7;border-top-left-radius:6px;bottom:0;color:rgba(0,0,0,0.3);content:'\f100';cursor:pointer;display:none;font-family:FontAwesome;font-size:1.8em;height:55px;left:0;line-height:55px;margin:auto;padding-right:4px;pointer-events:auto;position:absolute;text-align:center;top:0;width:26px;z-index:1}.withBuddylist #buddylist:before{content:'\f101';padding-right:0}.withBuddylistAutoHide #buddylist:before{display:block}.buddylist{background:#f8f8f8;border-left:1px solid #e7e7e7;bottom:0;left:25px;overflow-x:hidden;overflow-y:auto;pointer-events:auto;position:absolute;right:0;top:0}.buddylist.loading .buddylistloading{display:block}.buddylist.empty .buddylistempty{display:block}.buddylist .buddycontainer{pointer-events:auto;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.buddylist .buddylistempty{bottom:0;color:#b3b3b3;display:none;font-size:1.4em;height:2em;left:0;margin:auto;padding:.4em;position:absolute;right:0;text-align:center;top:0}.buddylist .buddylistloading{bottom:0;color:#b3b3b3;display:none;font-size:1.4em;height:2em;margin:auto;padding:.4em;position:absolute;right:0;text-align:center}.buddy{-webkit-tap-highlight-color:transparent;background:#fff;border-bottom:1px solid #e7e7e7;cursor:pointer;display:block;font-size:13px;min-height:66px;overflow:hidden;position:relative;text-align:left;width:100%}.buddy:hover{background:rgba(255,255,255,0.5)}.buddy.withSubline .buddy1,.buddy.contact .buddy1{top:15px}.buddy.withSubline .buddy2,.buddy.contact .buddy2{display:block}.buddy.hovered .buddyactions{right:0}.buddy .fa.contact:before{content:'\f006'}.buddy.contact .fa.contact:before{content:'\f005'}.buddy.isself .fa.contact:before{content:'\f192'}.buddy .buddyPicture{background:#84b819;border-radius:2px;float:left;height:46px;margin:10px;overflow:hidden;position:relative;text-align:center;width:46px}.buddy .buddyPicture .fa{color:#009534;font-size:3em;line-height:46px}.buddy .buddyPicture img{bottom:0;display:block;left:0;max-height:100%;max-width:100%;position:absolute;right:0;top:0}.buddy .buddyPictureSmall{height:30px;margin:0;margin-left:0;margin-right:0;width:30px}.buddy .buddyPictureSmall .fa{font-size:2em;line-height:30px}.buddy .buddy1{color:#262626;font-size:14px;font-weight:bold;height:28px;left:65px;overflow:hidden;position:absolute;right:4px;text-overflow:ellipsis;top:24px;white-space:nowrap}.buddy .buddy2{color:rgba(0,0,0,0.5);display:none;left:65px;overflow:hidden;position:absolute;right:0;top:33px;white-space:nowrap}.buddy .buddy3{display:inline-block;overflow:hidden;padding:0 6px;text-align:left;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap;width:120px}.buddy .buddyactions{background:rgba(255,255,255,0.5);height:66px;line-height:66px;padding:0 10px;position:absolute;right:-125px;text-align:right;top:0;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transition-property:right;transition-property:right;white-space:nowrap;z-index:5}.buddy .buddyactions .btn{font-size:1.6em;height:40px;line-height:40px;padding:0;text-align:center;vertical-align:middle;width:42px}.buddy .buddysessions{margin-bottom:10px;margin-top:56px;max-height:0;-webkit-transition-delay:.1s;transition-delay:.1s;-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-property:max-height;transition-property:max-height}.buddy .buddysessions ul{border-left:1px dotted #e7e7e7;border-right:1px dotted #e7e7e7;margin:0 14px;padding-left:0;padding-top:10px}.buddy .buddysessions ul li{list-style-type:none;margin-bottom:2px;margin-left:0}.buddy .buddysessions ul li .btn-group{visibility:hidden}.buddy .buddysessions ul li:hover .btn-group{visibility:visible}.buddy .buddysessions .currentsession .buddy3{font-weight:bold}.buddy.hovered .buddysessions{max-height:999px}.buddyPictureCapture .picture{display:block;margin-bottom:5px}.buddyPictureCapture .videoPicture{margin-bottom:4px}.buddyPictureCapture .videoPicture .videoPictureVideo{background-color:#000;overflow:hidden;position:relative}.buddyPictureCapture .videoPicture video{object-fit:cover}.buddyPictureCapture .videoPictureVideo{height:200px;width:200px}.buddyPictureCapture .videoPictureVideo .videoPrev,.buddyPictureCapture .videoPictureVideo video,.buddyPictureCapture .videoPictureVideo .preview{height:100%;width:100%}.buddyPictureCapture .videoFlash{background-color:#fff;border:1px dotted #e7e7e7;bottom:0;left:0;position:absolute;right:0;top:0;visibility:hidden;z-index:5}.buddyPictureCapture .videoFlash.flash{visibility:visible}.buddyPictureCapture .preview{left:0;position:absolute;top:0}.buddyPictureCapture .preview.previewPicture{position:relative}.buddyPictureCapture .btn-takePicture,.buddyPictureCapture .btn-retakePicture{left:0;margin:0 auto;max-width:40%;position:absolute;right:0;top:50%}.buddyPictureCapture .btn-retakePicture{visibility:hidden}.buddyPictureCapture .videoPictureVideo:hover .btn-retakePicture{visibility:visible}.buddyPictureCapture .countdownPicture{color:#f8f8f8;font-size:45px;left:0;margin:0 auto;opacity:.8;position:absolute;right:0;text-align:center;text-shadow:0 0 5px #000;top:75px}.buddyPictureUpload{position:relative}.buddyPictureUpload .loader{left:90px;position:absolute;z-index:1}.buddyPictureUpload .loader .fa-spin{color:#737373}.buddyPictureUpload>p{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.buddyPictureUpload .showUploadPicture{background-color:#f8f8f8;border:1px solid #e7e7e7;height:200px;line-height:200px;margin-bottom:10px;overflow:hidden;position:relative;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:200px}.buddyPictureUpload .showUploadPicture.imgData{background-color:#000}.buddyPictureUpload .showUploadPicture.imgData .chooseUploadPicture{display:none}.buddyPictureUpload .showUploadPicture .chooseUploadPicture{color:#737373;left:0;margin:0 auto;position:absolute;right:0;z-index:1}.buddyPictureUpload .showUploadPicture .fa{color:#f8f8f8;opacity:.8;text-shadow:0 0 5px #000}.buddyPictureUpload .preview{left:0;position:relative;top:0}.buddyPictureUpload .imageUtilites{line-height:30px;position:absolute;visibility:hidden;width:200px;z-index:1}.buddyPictureUpload .imageUtilites .fa{cursor:pointer;font-size:40px;height:50px;width:50px}.buddyPictureUpload .showUploadPicture.imgData:hover .imageUtilites{visibility:visible}.buddyPictureUpload .moveHorizontal{position:relative;top:-4px}.buddyPictureUpload .moveVertical{left:158px;position:absolute}.buddyPictureUpload .resize{position:relative;top:108px}#settings{background:#fff;border-left:1px solid #e7e7e7;bottom:0;padding-right:20px;position:fixed;right:-520px;top:51px;-webkit-transition:right 200ms ease-in-out;transition:right 200ms ease-in-out;width:520px;z-index:50}#settings.show{right:0}@media only screen and (max-width: 630px){#settings.show{background:#fff;left:0;width:auto}}@media only screen and (max-width: 630px){#settings.show .form-actions{bottom:0;height:60px;left:0;margin-bottom:0;padding:6px 0 6px 120px;position:fixed;right:0}}.settings{background:#fff;bottom:0;left:0;overflow-x:hidden;overflow-y:auto;padding:10px;position:absolute;right:0;top:0}@media only screen and (max-width: 630px){.settings{padding-bottom:10px}}.settings .version{color:#ccc;font-size:10px;position:absolute;right:10px;top:10px}@media only screen and (max-width: 630px){.settings .form-horizontal .controls{margin-left:110px}}@media only screen and (max-width: 630px){.settings .form-horizontal .control-label{width:100px;word-wrap:break-word}}settings-advanced{display:block;padding-top:15px}#chat{bottom:0;min-width:260px;opacity:0;pointer-events:none;position:absolute;right:260px;top:0;width:260px;z-index:45}.withChat #chat{opacity:1}.withChat.withChatMaximized #chat{left:0;width:auto}.withChat .chat{pointer-events:auto}.chatcontainer{background:#f8f8f8;bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.showchatlist .chatpane{right:100%}.showchatlist .chatlist{left:0}.chatlist{background:#f8f8f8;bottom:0;left:100%;position:absolute;top:0;width:100%}.chatlist .list-group{margin-bottom:-1px;margin-top:-1px;max-height:100%;overflow-x:hidden;overflow-y:auto}.chatlist .list-group-item{border-left:0;border-radius:0;border-right:0;line-height:26px;min-height:51px;padding-right:70px;position:relative}.chatlist .list-group-item.newmessage{-webkit-animation:newmessage 1s ease -.3s infinite;animation:newmessage 1s ease -.3s infinite}.chatlist .list-group-item.disabled{color:#aaa}.chatlist .list-group-item:hover button{display:inline}.chatlist .list-group-item .fa-lg{display:inline-block;text-align:center;width:18px}.chatlist .list-group-item .badge{background:#84b819;border:1px solid #fff;position:absolute;right:50px;top:14px}.chatlist .list-group-item button{display:none;position:absolute;right:10px}.chatpane{-webkit-backface-visibility:hidden;backface-visibility:hidden;bottom:0;position:absolute;right:0;top:0;width:100%}.chat{background:#f8f8f8;bottom:0;display:none;left:0;overflow:hidden;position:absolute;right:0;top:0}.chat.active.visible{display:block}.chatmenu{height:36px;left:0;padding:2px 4px;position:absolute;right:0;top:36px}@media (max-height: 210px){.chatmenu{display:none}}.chatbody{border-left:1px solid #e7e7e7;bottom:-1px;left:0;position:absolute;right:0;top:74px}@media (max-height: 210px){.chatbody{border-top:1px solid #e7e7e7;top:0;top:0}}.chatheader{background:rgba(255,255,255,0.9);border-bottom:1px solid #e7e7e7;border-left:1px solid #e7e7e7;height:36px;left:0;line-height:34px;padding:0 4px 0 8px;position:absolute;right:0;top:0}@media (max-height: 210px){.chatheader{display:none}}.chatheader .chatstatusicon{cursor:pointer;display:block;font-size:1.4em;height:36px;left:0;position:absolute;text-align:center;top:0;width:36px}.chatheader .chatheadertitle{display:inline;padding-left:28px}.chatheader .ctrl{color:rgba(0,0,0,0.3);position:absolute;right:1px;top:0}.chatheader .ctrl .fa{cursor:pointer;padding:6px}.chatheader span{display:inline-block;max-width:60%;overflow:hidden;pointer-events:none;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap}.chat .outputbox{bottom:75px;left:0;position:absolute;right:0;top:0}@media (max-height: 210px){.chat .outputbox{bottom:45px}}.chat .output{height:100%;overflow-x:hidden;overflow-y:auto;padding:.4em 0}.chat .output>i{clear:both;color:#aaa;display:block;font-size:.8em;padding:6px 0;text-align:center}.chat .output>i.p2p{font-weight:bold;padding:6px 0}.chat.with_pictures .message.is_self{padding-right:54px}.chat.with_pictures .message.is_self .timestamp{right:58px}.chat.with_pictures .message.is_remote{padding-left:58px}.chat .message{background:#fff;border:1px solid transparent;border-radius:6px;-webkit-box-shadow:0 0 2px 0 rgba(0,0,0,0.03);box-shadow:0 0 2px 0 rgba(0,0,0,0.03);clear:both;display:block;margin:0 4px 2px 18px;padding:8px;position:relative;word-wrap:break-word}.chat .message ul{list-style-type:none;margin:0;padding-left:0}.chat .message li{line-height:1.1em;margin:4px 0;padding-left:1.2em;position:relative}.chat .message li:before{color:#ccc;content:'\f075';font-family:FontAwesome;left:0;position:absolute;text-align:center;width:12px}.chat .message .timestamp{color:#aaa;font-size:.8em;position:absolute;right:8px;text-align:right;top:8px}.chat .message .timestamp-space{float:right;height:10px;width:40px}.chat .message strong{display:block;margin-right:40px;overflow:hidden;padding-bottom:2px;text-overflow:ellipsis;white-space:nowrap}.chat .message.is_self li:before{color:#ccc;-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.chat .message li.unread:before{color:#fe9a2e;content:""}.chat .message li.sending:before{color:#ccc;content:""}.chat .message li.sent:before{color:#5882fa;content:""}.chat .message li.delivered:before{color:#5882fa;content:""}.chat .message li.received:before{color:#84b819;content:""}.chat .message li.read:before{color:#ccc;content:""}.chat .message.is_self .buddyPicture{left:auto;right:4px}.chat .message .buddyPicture{background:#84b819;border-radius:2px;height:46px;left:4px;overflow:hidden;position:absolute;text-align:center;top:4px;width:46px;z-index:0}.chat .message .buddyPicture .fa{color:#009534;line-height:46px}.chat .message .buddyPicture img{bottom:0;display:block;left:0;max-height:100%;max-width:100%;position:absolute;right:0;top:0}.chat .message:before,.chat .message:after{border-style:solid;content:'';display:block;position:absolute;width:0}.chat .message.is_remote{background:#fff}.chat .message.is_remote:before{border-color:transparent #eee;border-width:7px 11px 7px 0;bottom:auto;left:-12px;top:4px}.chat .message.is_remote:after{border-color:transparent #fff;border-width:6px 10px 6px 0;bottom:auto;left:-11px;top:5px}.chat .message.is_self{background:#fff;margin-left:4px;margin-right:18px;padding-right:0}.chat .message.is_self:before{border-color:transparent #eee;border-width:7px 0 7px 11px;bottom:4px;bottom:auto;right:-12px}.chat .message.is_self:after{border-color:transparent #fff;border-width:6px 0 6px 10px;bottom:5px;bottom:auto;right:-11px}.chat .chatbodybottom{background:#f8f8f8;bottom:1px;left:0;margin:0 auto;position:absolute;right:0}@media (max-height: 210px){.chat .chatbodybottom{height:auto}}.chat .typinghint{color:#aaa;font-size:.8em;height:14px;overflow:hidden;padding:0 6px;white-space:nowrap}@media (max-height: 210px){.chat .typinghint{display:none}}.chat .inputbox{position:relative}@media (max-height: 210px){.chat .inputbox{height:auto}}.chat .inputbox .btn{display:none;padding:.5em 1em;position:absolute;right:6px;top:1px}.chat .inputbox>div{border-top:1px solid #e7e7e7}.chat .input{border-color:transparent;border-radius:0;-webkit-box-shadow:none;box-shadow:none;display:block;height:54px;margin:0;max-height:54px;resize:none;width:100%}@media (max-height: 210px){.chat .input{max-height:2.5em}}.chat .input:active,.chat .input:focus{border-color:#66afe9}@-webkit-keyframes newmessage{0%{background-color:#84b819}50%{background-color:#f8f8f8}100%{background-color:#84b819}}@keyframes newmessage{0%{background-color:#84b819}50%{background-color:#f8f8f8}100%{background-color:#84b819}}.chat.newmessage .chatheadertitle:after{content:'***';position:absolute;right:32px;top:2px}.chat.newmessage .chatheader{-webkit-animation:newmessage 1s ease -.3s infinite;animation:newmessage 1s ease -.3s infinite}#help{bottom:10px;color:#aaa;font-size:1.1em;left:0;margin:0 auto;position:absolute;right:0;text-shadow:0 0 5px #000;top:80px;-webkit-transition:right 200ms ease-in-out;transition:right 200ms ease-in-out;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:350px}.withChat #help,.withBuddylist #help{right:260px}.withChat.withBuddylist #help,.withSettings #help{right:520px}@media only screen and (max-width: 400px){#help{display:none}}@media only screen and (min-width: 400px) and (max-width: 1020px){#help{font-size:1em;width:250px}}#help>div{margin:0 10px}#help .help-subline{color:#888;padding:20px 0}#help .btn{text-shadow:none}#audiolevel{left:0;margin:0 auto;position:fixed;right:0;top:43px;width:400px;z-index:60}#audiolevel .audio-level{background:#84b819;background:gradient(linear, left top, left bottom, color-stop(0%, #84b819), color-stop(50%, #a1d54f), color-stop(51%, #80c217), color-stop(100%, #7cbc0a));background:-webkit-gradient(linear, left top, left bottom, from(#84b819), color-stop(50%, #a1d54f), color-stop(51%, #80c217), to(#7cbc0a));background:-webkit-linear-gradient(top, #84b819 0%, #a1d54f 50%, #80c217 51%, #7cbc0a 100%);background:linear-gradient(to bottom, #84b819 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%);border-radius:0 0 2px 2px;height:4px;left:0;margin:0 auto;position:absolute;right:0;-webkit-transition:width .05s ease-in-out;transition:width .05s ease-in-out;width:0}.file-info{background:#fff;border:1px solid #ddd;border-radius:4px;max-width:170px;padding:1em;position:relative;text-align:center}.file-info>div{position:relative;z-index:3}.file-info .file-info-bg{bottom:0;left:41px;overflow:hidden;position:absolute;right:0;top:-17px;z-index:2}.file-info .file-info-bg .fa{color:#eee;font-size:20em}.file-info .actions{left:50%;margin-left:10px;position:absolute;text-align:left;top:14px}.is_remote .file-info{background:#fff;border:1px solid #ddd}.is_remote .file-info .file-info-bg .fa{color:#eee;font-size:20em}.file-info-name{font-size:1.1em;margin:.2em 0;min-width:140px;padding:0 .2em}.file-info-size{font-size:.8em;height:20px;position:relative}.file-info-size>span{display:block;left:0;margin:0 auto;padding:3px;position:absolute;right:0;text-shadow:1px 1px 1px #fff;top:0;z-index:5}.file-info-size>div{bottom:0;-webkit-box-shadow:none !important;box-shadow:none !important;left:0;position:absolute;top:0;width:0;z-index:0}.file-info-size>div.progress-bar{opacity:.5}.file-info-size>div.progress-bar.download{opacity:1;z-index:1}.file-info-speed{bottom:8px;font-size:.8em;left:0;position:absolute;right:0;text-align:center}.file-info.uploader .file-info-speed{bottom:6px}.file-info.uploader .actions{margin-left:30px;opacity:0}.file-info.uploader .anim{margin-left:0}.file-info.uploader .hovercontrol:hover .anim{margin-left:-50px}.file-info.uploader .hovercontrol:hover .actions{margin-left:0;opacity:1}.file-info.uploader .hovercontrol>div{-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.file-info.downloader .anim{margin-left:-40px}.file-info.downloader .file-info-size{margin-bottom:10px}.file-info.downloading .file-info-size{border-color:#ddd}#audiovideo{bottom:0;left:0;position:absolute;right:0;top:51px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media only screen and (max-width: 590px){#audiovideo{right:0}}#audiovideo.fullscreen{bottom:0 !important;left:0 !important;right:0 !important;top:0 !important}#audiovideo.fullscreen .remoteVideo .peerActions{display:none}@media only screen and (max-width: 630px){.mainScreenshare #audiovideo,.mainPresentation #audiovideo{display:none}}.withChat #audiovideo,.withBuddylist #audiovideo{right:260px}.withBuddylist.withChat #audiovideo{right:520px}.audiovideo{bottom:0;left:0;position:absolute;right:0;top:0}.audiovideo.active{-webkit-perspective:1000;perspective:1000}.audiovideo.active:hover .overlayActions{opacity:.3}.audiovideo.active .overlayActions:hover{opacity:.6}.audiovideo.active .audiovideoBase{-webkit-transform:rotateY(180deg);-ms-transform:rotateY(180deg);transform:rotateY(180deg)}.audiovideo .audiovideoBase{height:100%;position:relative;-webkit-transform:rotateY(0deg);-ms-transform:rotateY(0deg);transform:rotateY(0deg);-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:-webkit-transform;transition-property:transform;width:100%;z-index:2}.audiovideo .localContainer{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1);z-index:2;overflow:hidden}.audiovideo video{object-fit:cover}.audiovideo .remoteContainer{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;-webkit-transform:rotateY(180deg);-ms-transform:rotateY(180deg);transform:rotateY(180deg);z-index:2}.audiovideo .miniContainer{background:#000;bottom:2px;height:100%;max-height:18%;opacity:0;position:absolute;right:2px;-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1);-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-property:opacity;transition-property:opacity;z-index:25;overflow:hidden}.audiovideo .miniContainer.visible{opacity:1}.audiovideo .miniVideo{display:block;height:100%;max-height:100%;max-width:100%;width:100%}.audiovideo .localVideo{background:rgba(0,0,0,0.4);display:block;max-height:100%;opacity:0;-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:opacity;transition-property:opacity;width:100%}.audiovideo .remoteVideos{bottom:0;left:0;opacity:0;position:absolute;right:0;top:0;-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:opacity;transition-property:opacity}.audiovideo .remoteVideos video{display:block;height:100%;width:100%}.audiovideo .remoteVideo{background:rgba(0,0,0,0.4);display:inline-block;max-height:100%;max-width:100%;overflow:hidden;position:relative;vertical-align:bottom;width:100%}.audiovideo .remoteVideo.onlyaudio{background:#666}.audiovideo .remoteVideo .onlyaudio{color:rgba(255,255,255,0.3);display:none;font-size:80px;left:0;margin-top:-40px;pointer-events:auto;position:absolute;right:0;text-align:center;top:45%}.audiovideo .remoteVideo.onlyaudio video,.audiovideo .remoteVideo.dummy video{visibility:hidden}.audiovideo .remoteVideo.onlyaudio .onlyaudio{display:block}.audiovideo .remoteVideo.dummy .onlyaudio{display:block}.audiovideo .remoteVideo .peerActions{bottom:5%;left:40px;opacity:0;pointer-events:auto;position:absolute;right:40px;text-align:center;-webkit-transition-duration:.2s;transition-duration:.2s;-webkit-transition-property:opacity;transition-property:opacity;z-index:10}.audiovideo .remoteVideo .peerActions:hover{opacity:.5}.audiovideo .remoteVideo .peerActions i{font-size:3vw}.audiovideo .remoteVideo .peerLabel{bottom:4%;color:#fff;font-size:2.5vw;left:4%;max-width:30%;opacity:.7;overflow:hidden;padding:4px;position:absolute;text-overflow:ellipsis;text-shadow:0 0 4px #000;white-space:nowrap;z-index:8}.audiovideo .overlayActions{background:rgba(0,0,0,0.9);bottom:0;height:140px;left:0;margin:auto 0;opacity:0;padding:3px 0;position:absolute;top:0;width:40px;z-index:5}.audiovideo .overlayActions .btn{color:#ccc;cursor:pointer;display:block;outline:0;text-shadow:0 0 5px #000;width:40px}.remoteVideo.talking .peerLabel{color:#84b819}.remoteVideo .peerLabel{-webkit-transition:color 500ms ease-out;transition:color 500ms ease-out}.remoteVideo .overlayLogo{background:url("../img/logo-overlay.png") no-repeat center;-webkit-background-size:100%;background-size:100%;height:20%;max-height:40px;max-width:111px;opacity:.5;pointer-events:none;position:absolute;right:2.5%;top:4%;width:20%;z-index:2}.miniContainer.talking:after{bottom:2px;-webkit-box-shadow:0 0 20px #84b819 inset;box-shadow:0 0 20px #84b819 inset;content:'';left:2px;position:absolute;right:2px;top:2px}.renderer-auditorium{position:relative}.renderer-auditorium span:before{content:'\f183';left:50%;margin-left:-.8em;margin-top:-.5em;position:absolute;top:50%}.renderer-auditorium span:after{content:'\f183';margin-right:-.9em;margin-top:-.5em;position:absolute;right:50%;top:50%}.renderer-smally{background:#000;border-right:0;border-top:0;width:150px}.renderer-smally .remoteVideos{padding-bottom:85px}.renderer-smally .remoteVideo .peerLabel{font-size:.9em;font-weight:bold}.renderer-smally .remoteVideo .peerActions i{font-size:1em}.renderer-smally .miniContainer{bottom:0;height:85px;left:0;max-height:none;right:0}.renderer-democrazy .remoteVideos .miniContainer{bottom:auto;display:inline-block;max-height:100%;max-width:100%;position:relative;right:auto;vertical-align:bottom}.renderer-democrazy .active .miniContainer{opacity:1}.renderer-conferencekiosk .remoteVideos{background:rgba(0,0,0,0.4);bottom:2px;min-height:108px;pointer-events:auto;text-align:center;top:auto;white-space:nowrap}.renderer-conferencekiosk .remoteVideos>div{cursor:pointer;height:108px;width:192px}.renderer-conferencekiosk .remoteVideos .overlayLogo{display:none}.renderer-conferencekiosk .remoteVideos .peerLabel,.renderer-conferencekiosk .remoteVideos .peerActions i{font-size:1.1em}.renderer-conferencekiosk .remoteVideos .peerLabel{background:rgba(0,0,0,0.9)}.renderer-conferencekiosk .miniContainer{height:108px;max-height:none;width:192px}.renderer-conferencekiosk .bigVideo{bottom:112px;left:0;margin:auto;opacity:0;position:absolute;right:0;top:2px;-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:opacity;transition-property:opacity}.renderer-conferencekiosk .bigVideo video{height:100%;width:100%}.renderer-auditorium .remoteContainer{border-left:40px solid #000}.renderer-auditorium .remoteVideos{background:rgba(0,0,0,0.4);pointer-events:auto;top:180px;width:320px}.renderer-auditorium .remoteVideos .overlayLogo{display:none}.renderer-auditorium .remoteVideos video{height:100%;margin-top:-9px;object-fit:cover;width:100%}.renderer-auditorium .remoteVideos>div{cursor:pointer;display:inline-block;height:60px;width:80px}.renderer-auditorium .remoteVideos .overlayLogo{display:none}.renderer-auditorium .remoteVideos .peerLabel{background:rgba(0,0,0,0.9);bottom:0;font-size:.6em;left:0;line-height:9px;max-width:100%;padding:0 4px;right:0}.renderer-auditorium .remoteVideos .peerActions{display:none}.renderer-auditorium .remoteVideos .miniContainer{max-height:auto;right:auto}.renderer-auditorium .bigVideo{height:180px;width:320px}.renderer-auditorium .bigVideo video{height:100%;width:100%}.renderer-auditorium .bigVideo .peerLabel{bottom:8%;font-size:1vw;max-width:40%}.screenshare{bottom:0;left:0;position:absolute;right:0;top:0}.mainScreenshare #screenshare{display:block}.screensharepane{background:#000;bottom:0;left:0;overflow:auto;position:absolute;right:0;top:0}.screensharepane .remotescreen{position:relative}.screensharepane video{max-height:99%;width:100%}.remotesize .screensharepane video{max-height:none;width:auto}.screenshare .overlaybar{bottom:0;left:0;right:0}#roombar{left:0;min-width:260px;position:absolute;right:0;top:51px;z-index:4}#roombar .roombar{left:0;position:absolute;right:0;top:0}.fa.link{color:#aaa}.fa.email{color:#aaa}.fa.facebook{color:#45619d}.fa.google{color:#dd4b39}.fa.twitter{color:#00aced}.fa.xing{color:#fff}.contactsmanager .desc{font-size:20px;font-weight:normal;text-align:baseline}.contactsmanager .addbtn{font-size:14px}.contactsmanager .addbtn .fa-users{font-size:22px}.contactsmanager .addbtn .fa-plus{font-size:15px}.contactsmanager .editpicture{float:left;margin-right:20px;vertical-align:middle}.contactsmanager .uploadbtn{margin-top:7px}.contactsmanager .editlist{max-height:250px;overflow-y:auto}.contactsmanager .picture{border-bottom:0;cursor:auto;display:table-cell;min-height:46px;position:static;width:auto}.contactsmanager .picture .buddyPicture{margin:0 0 0 10px}.contactsmanager .table{margin-bottom:0}.contactsmanager .table tr:first-child td{border-top:0}.contactsmanager .table .name{text-align:left;vertical-align:middle;width:40%}.contactsmanager .table .action{padding-right:15px;text-align:right;vertical-align:middle}.contactsmanageredit .buddy .buddyPicture{margin:0}.search:before{content:'\f002';font-family:'fontAwesome';font-size:14px;left:22px;opacity:.4;position:absolute;top:6px}.search ~ input{padding-left:25px}.presentation{bottom:0;left:0;position:absolute;right:0;top:0}.presentationpane .welcome{padding:0}.presentationpane .welcome h1{white-space:normal}.presentationpane .welcome button{margin-top:30px}.presentationpane .welcome .progress span{text-shadow:none}.presentationpane .welcome .progress .download-info{color:#333;left:0;position:absolute;text-shadow:1px 1px 1px #fff;width:100%}.mainPresentation #presentation{display:block}.presentationpane{bottom:0;left:0;overflow:auto;position:absolute;right:0;top:0}.presentationpane .canvasContainer{height:100%;width:100%}.presentationpane canvas{display:block;margin:0 auto;position:relative}.presentationpane .odfcanvas{cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.presentationpane .odfcanvas body{background-color:transparent}.presentationpane .odfcanvas document{display:block}.presentationpane .odfcontainer{display:none;margin:0;padding:0}.presentationpane .odfcontainer.showonepage{overflow:hidden;text-align:center}.presentation .overlaybar{bottom:0;left:0;right:0;text-align:center}.presentation .overlaybar .overlaybar-content{max-width:100%}.presentation .overlaybar .overlaybar-button{font-size:20px;line-height:28px;padding:4px 6px;position:absolute;top:0}.overlaybar-content .pagecontrol{height:30px}.presentation .overlaybar .btn-prev{left:40px}.presentation .overlaybar .btn-next{left:auto;right:0}.pageinfo input{display:inline;width:70px}.presentation .thumbnail{color:#333;display:inline-block;height:122px;margin-left:20px;margin-top:20px;position:relative;text-shadow:none;vertical-align:middle;width:160px}.presentation .thumbnail.presentable{cursor:pointer}.presentation .thumbnail:first-child{margin-left:0}.presentation .thumbnail .caption{overflow:hidden;padding-bottom:0;text-overflow:ellipsis}.presentation .thumbnail .caption .size{font-size:10px}.presentation .thumbnail .caption .progress{position:relative}.presentation .thumbnail .caption .download-info{bottom:0;color:#333;left:0;line-height:20px;position:absolute;right:0;text-shadow:1px 1px 1px #fff;top:0}.presentation .thumbnail .active{bottom:0;color:#84b819;font-size:10em;left:0;opacity:.7;position:absolute;right:0;text-align:center;top:0}.presentation .thumbnail .notavailable{bottom:0;color:#d2322d;display:none;font-size:10em;left:0;opacity:.25;position:absolute;right:0;text-align:center;top:0}.presentation .thumbnail:hover .notavailable{display:block}.presentation .thumbnail .presentation-action{display:none;position:absolute;top:1px}.presentation .thumbnail .download{left:1px}.presentation .thumbnail .delete{right:1px}.presentation .thumbnail:hover .presentation-action{display:block}.presentation .thumbnail .filetype{font-size:5em}.presentations{height:156px;margin-left:-25px;margin-right:10px;overflow-x:auto;overflow-y:hidden;white-space:nowrap}.youtubevideo{bottom:0;left:0;position:absolute;right:0;top:0}.youtubevideopane{bottom:0;left:0;overflow:auto;position:absolute;right:0;top:0}#youtubecontainer{position:relative}#youtubeplayerinfo{bottom:10%;left:0;opacity:0;pointer-events:auto;position:absolute;right:0;text-align:center;-webkit-transition-duration:.2s;transition-duration:.2s;-webkit-transition-property:opacity;transition-property:opacity;z-index:10}#youtubeplayerinfo:hover{opacity:.8}#youtubeplayerinfo div{background-color:#f9f2f4;border-radius:10px;display:inline-block;font-size:2em;padding:20px 40px}.youtubevideo .click-container{bottom:0;left:0;position:absolute;right:0;top:0;z-index:5}.youtubevideo .welcome{max-width:700px}.youtubevideo .welcome h1{margin-top:10px}.youtubevideo .welcome .welcome-container{max-width:700px}.youtubevideo .welcome .welcome-logo{background:transparent;font-size:10em}.mainYoutubevideo #youtubevideo{display:block}.youtubevideo .overlaybar{bottom:0;left:0;right:0}.youtubevideo .overlaybar-content{max-width:100%;width:100%}.youtubevideo .overlaybar-input{padding-right:15px;position:relative;width:100%}.youtubevideo .overlaybar-content form .overlaybar-buttons{position:absolute;right:23px;top:6px}.volumecontrol{background:rgba(0,0,0,0.6);bottom:0;left:0;opacity:0;padding:4px;pointer-events:auto;position:absolute;right:0;z-index:10}.volumecontrol:hover{opacity:1}.volume-button{display:inline;min-width:38px}.volumebar{display:inline-block;padding:6px 8px;vertical-align:middle}.volumebar .bar{-webkit-appearance:none;background-color:#aaa;border:1px solid #aaa;height:3px;outline:0;width:100px}.volumebar .bar::-webkit-slider-thumb{-webkit-appearance:none;background-color:#fff;height:20px;width:6px}.volumebar .bar::-moz-range-track{background:#aaa;border:0}.volumebar .bar::-moz-range-thumb{background-color:#fff;border-radius:0;height:20px;width:6px}.volumebar .bar::-moz-focusring{outline:1px solid #aaa;outline-offset:-1px}.modal{overflow-y:auto}#toast-container>.toast{background-image:none !important}#toast-container>.toast:before{color:#fff;float:left;font-family:FontAwesome;font-size:20px;line-height:20px;margin:auto .5em auto -1.5em;padding-right:.5em;position:fixed}#toast-container>.toast-warning:before{content:'\f05a'}#toast-container>.toast-error:before{content:'\f05a'}#toast-container>.toast-info:before{content:'\f05a'}#toast-container>.toast-success:before{content:'\f05a'}#toast-container>:hover,#toast-container>div{-webkit-box-shadow:none !important;box-shadow:none !important}.toast-info{background-color:#5bc0de}.toast-close-button{font-size:1em;top:-.6em}#toast-container>div{filter:alpha(opacity=100);opacity:1} + *//*! HiDPI v2.0.1 | MIT License | git.io/hidpi */.toast-title{font-weight:bold}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#ffffff}.toast-message a:hover{color:#cccccc;text-decoration:none}.toast-close-button{position:relative;right:-0.3em;top:-0.3em;float:right;font-size:20px;font-weight:bold;color:#ffffff;-webkit-text-shadow:0 1px 0 #ffffff;text-shadow:0 1px 0 #ffffff;opacity:0.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}.toast-close-button:hover,.toast-close-button:focus{color:#000000;text-decoration:none;cursor:pointer;opacity:0.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}button.toast-close-button{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;border-radius:3px 3px 3px 3px;background-position:15px center;background-repeat:no-repeat;-webkit-box-shadow:0 0 12px #999999;box-shadow:0 0 12px #999999;color:#ffffff;opacity:0.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>:hover{-webkit-box-shadow:0 0 12px #000000;box-shadow:0 0 12px #000000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important}#toast-container>.toast-error{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important}#toast-container>.toast-success{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important}#toast-container>.toast-warning{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important}#toast-container.toast-top-center>div,#toast-container.toast-bottom-center>div{width:300px;margin:auto}#toast-container.toast-top-full-width>div,#toast-container.toast-bottom-full-width>div{width:96%;margin:auto}.toast{background-color:#030303}.toast-success{background-color:#51a351}.toast-error{background-color:#bd362f}.toast-info{background-color:#2f96b4}.toast-warning{background-color:#f89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000000;opacity:0.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width: 240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container .toast-close-button{right:-0.2em;top:-0.2em}}@media all and (min-width: 241px) and (max-width: 480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container .toast-close-button{right:-0.2em;top:-0.2em}}@media all and (min-width: 481px) and (max-width: 768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}}.dialog-header-error{background-color:#d2322d}.dialog-header-wait{background-color:#428bca}.dialog-header-notify{background-color:#eee}.dialog-header-confirm{background-color:#eee}.dialog-header-error span,.dialog-header-error h4,.dialog-header-wait span,.dialog-header-wait h4{color:#fff}.modal-content{overflow:hidden}.modal-content .modal-body{min-height:160px}[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none !important}html,body{-webkit-background-clip:padding-box;background-clip:padding-box;background-color:#e5e5e5;height:100%}body{margin:0;max-height:100%;max-width:100%;overflow:hidden;padding:0}a{cursor:pointer}#background{background:url("../img/bg-tiles.jpg");bottom:0;left:0;position:fixed;right:0;top:0;z-index:0}@media (-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 1.3dppx){#background{background-image:url("../img/bg-tiles_x2.jpg");-webkit-background-size:198px 200px;background-size:198px 200px}}.help-block{color:#737373}.dialog-header-notify,.dialog-header-confirm{background-color:#eee}.desktopnotify-icon{background-image:url("../img/logo-48x48.png")}:-webkit-full-screen{background:#000}:-moz-full-screen{background:#000}:-ms-fullscreen{background:#000}:fullscreen{background:#000}#loader{background:url("../img/logo.svg") no-repeat center;-webkit-background-size:contain;background-size:contain;bottom:15%;left:15%;margin:auto;max-height:150px;max-width:200px;opacity:1;pointer-events:none;position:fixed;right:15%;top:15%;-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-property:opacity;transition-property:opacity;z-index:20000}#loader.done{opacity:0}#loader>div{bottom:0;color:#ddd;display:block;font-size:2em;left:0;margin:0 auto;margin-bottom:-40px;position:absolute;right:0;text-align:center;text-shadow:0 0 5px #000}#loader .loader-message{font-size:.5em}.mainview{bottom:0;display:none;left:0;position:absolute;right:0;top:51px}@media (max-width: 700px){.mainview{left:0;left:0}}.videolayoutSmally .mainview{left:150px}.videolayoutClassroom .mainview{left:360px}.withChat .mainview,.withBuddylist .mainview{right:260px}.withBuddylist.withChat .mainview{right:520px}#page{bottom:0;left:0;position:absolute;right:0;top:51px}.welcome{color:#aaa;font-size:1.1em;margin-top:80px;max-width:600px;min-height:160px;padding-left:105px;padding-right:0;position:relative;text-shadow:0 0 5px #000}@media (max-width: 700px){.welcome{margin:0 auto;padding-left:10px;padding-right:20px}}.welcome h1{margin-top:0;white-space:nowrap}@media (max-width: 700px){.welcome h1{white-space:normal}}.welcome .welcome-container{margin:0 auto}.welcome .welcome-logo{background:url("../img/logo.svg") no-repeat left top;-webkit-background-size:contain;background-size:contain;bottom:0;left:0;position:absolute;top:1px;width:90px}@media (max-width: 700px){.welcome .welcome-logo{height:70px;margin-bottom:20px;margin-top:30px;position:relative;width:70px}}.welcome .welcome-input{position:relative}.welcome .welcome-input input{padding-right:105px}.welcome .welcome-input-buttons{position:absolute;right:8px;text-shadow:none;top:6px}.welcome .welcome-input-buttons a{color:#000;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.welcome .room-link{margin-top:-10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.welcome .room-link a{color:#aaa}.welcome .rooms-history{margin-top:3em}.welcome .rooms-history a{display:inline-block;margin-right:.5em}.welcome .rooms-history a:hover{text-decoration:none}.nicescroll::-webkit-scrollbar{background-color:#e5e5e5;border:solid transparent;height:8px;width:8px}.nicescroll::-webkit-scrollbar:hover{background-color:#e5e5e5;border-left:1px solid rgba(0,0,0,0.12);border-right:1px solid rgba(0,0,0,0.12)}.nicescroll::-webkit-scrollbar-thumb{background:rgba(0,0,0,0.2)}.nicescroll::-webkit-scrollbar-thumb:active{background:rgba(0,0,0,0.4)}.fadetogglecontainer>div{position:absolute;width:100%}.animate-show.ng-hide-add{display:block !important;opacity:1;-webkit-transition:all linear .5s;transition:all linear .5s}.animate-show.ng-hide-add.ng-hide-add-active{opacity:0}.animate-show.ng-hide-remove{display:block !important;opacity:0;-webkit-transition:all linear .5s;transition:all linear .5s}.animate-show.ng-hide-remove.ng-hide-remove-active{opacity:1}.overlaybar{background:rgba(0,0,0,0.2);border-bottom:1px solid #222;border-top:1px solid #222;color:#e7e7e7;min-height:36px;padding:3px 8px 0 30px;position:absolute;text-shadow:0 0 5px #000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle}.overlaybar:hover{background:rgba(0,0,0,0.5)}.overlaybar.notvisible{background:transparent;border-bottom:1px solid transparent;border-top:1px solid transparent;pointer-events:none}.overlaybar.notvisible:hover{background:transparent}.overlaybar.notvisible .overlaybar-content{display:none}.overlaybar.notvisible .overlaybar-overlay{display:block}.overlaybar .btn{text-shadow:none}.overlaybar .btn-link{text-shadow:0 0 5px #000}.overlaybar .form-group>*{float:left;padding-top:0}.overlaybar input[type="radio"],.overlaybar input[type="checkbox"]{margin-top:2px}.overlaybar label{padding-top:6px !important}.overlaybar .overlaybar-button{color:#e7e7e7;display:block;font-size:20px;left:3px;opacity:.7;padding:4px 6px;pointer-events:auto;position:absolute;top:0;vertical-align:middle;z-index:15}.overlaybar .overlaybar-content{display:inline-block;margin-bottom:0;margin-left:.5em;max-width:60%}.overlaybar .overlaybar-content>*{padding-right:.5em}.overlaybar .overlaybar-content .input-group{max-width:160px}.overlaybar .overlaybar-overlay{display:none;margin-left:.5em;opacity:.7;padding-top:2px;text-align:left}.visible-with-contacts,.visible-with-contacts-inline{display:none}.with-contacts .visible-with-contacts{display:block}.with-contacts .visible-with-contacts-inline{display:inline-block}.with-contacts .hidden-with-contacts{display:none}.withBuddylist #rightslide{right:0}#rightslide{bottom:0;left:0;pointer-events:none;position:absolute;right:-260px;top:51px;-webkit-transition:right 200ms ease-in-out;transition:right 200ms ease-in-out;z-index:5}#rightslide .rightslidepane{height:100%;position:relative;width:100%}.bar{background:#f8f8f8;color:#262626;font:bold 1em/50px "Helvetica Neue",Helvetica,Arial,sans-serif;text-align:center;touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:60}.bar .left{padding:5px 5px 5px 15px}@media (max-width: 700px){.bar .left{padding:2px 5px 0 11px;padding:2px 5px 0 11px}}.logo{background:url("../img/logo-small.png") no-repeat;-webkit-background-size:100%;background-size:100%;color:#000;display:inline-block;font:normal 11px/11px "Helvetica Neue",Helvetica,Arial,sans-serif;height:32px;text-align:left;vertical-align:middle;width:90px}@media (max-width: 700px){.logo{background:url("../img/logo.svg") no-repeat center;height:46px;width:46px}.logo .desc{display:none}}.logo .desc{font-style:italic;left:38px;position:relative;top:26px}.logo .desc a{color:#222}.bar .middle{left:0;pointer-events:none;position:absolute;right:60px;text-align:center;top:0}.bar .middle>span{background:#f8f8f8;display:inline-block;min-height:50px;pointer-events:auto}.bar .middle .userpicture{border-radius:2px;display:inline-block;height:46px;margin:-1px .5em 0;width:46px}@media (max-width: 700px){.bar .middle .userpicture{display:none}}@media (max-width: 700px){.bar .middle .status-connected,.bar .middle .status-conference,.bar .middle .status-connecting,.bar .middle .status-closed,.bar .middle .status-reconnecting,.bar .middle .status-error,.bar .middle .status-ringing{left:0;max-width:100%;position:absolute;right:0}}.bar .right{margin-top:-1px;padding-right:4px}.bar .right .badge{background-color:#84b819;border:1px solid #fff;font-size:.4em;position:absolute;right:0;top:2px}.bar .right .btn{background:#e9e9e9;border-color:#e2e2e2;color:#333;font:24px/40px "Helvetica Neue",Helvetica,Arial,sans-serif;height:42px;margin-left:-2px;padding:0;position:relative;text-align:center;width:42px}.bar .right .btn:focus{border:0;-webkit-box-shadow:0;box-shadow:0;outline:none}.bar .right .btn:hover{background-color:transparent;border-color:#e7e7e7;color:#666}.bar .right .btn.active{background-color:transparent;border-color:#e7e7e7;color:#666}.bar .right .btn.active.amutebtn{background-color:#db4f39;border-color:#db4f39;color:#fff}.bar .right .btn.active.aenablebtn{background-color:#84b819;border-color:#84b819;color:#fff}.btn-mutemicrophone i:before{content:'\f130'}.btn-mutemicrophone.active i:before{content:'\f131'}.btn-mutecamera i:before{content:'\f06e'}.btn-mutecamera.active i:before{content:'\f070'}@-webkit-keyframes shakeityeah{0%{-webkit-transform:translate(2px, 1px) rotate(0deg);transform:translate(2px, 1px) rotate(0deg)}2%{-webkit-transform:translate(-1px, -2px) rotate(-1deg);transform:translate(-1px, -2px) rotate(-1deg)}4%{-webkit-transform:translate(-3px, 0) rotate(1deg);transform:translate(-3px, 0) rotate(1deg)}8%{-webkit-transform:translate(0, 2px) rotate(0deg);transform:translate(0, 2px) rotate(0deg)}10%{-webkit-transform:translate(1px, -1px) rotate(1deg);transform:translate(1px, -1px) rotate(1deg)}12%{-webkit-transform:translate(-1px, 2px) rotate(-1deg);transform:translate(-1px, 2px) rotate(-1deg)}14%{-webkit-transform:translate(-3px, 1px) rotate(0deg);transform:translate(-3px, 1px) rotate(0deg)}16%{-webkit-transform:translate(2px, 1px) rotate(-1deg);transform:translate(2px, 1px) rotate(-1deg)}18%{-webkit-transform:translate(-1px, -1px) rotate(1deg);transform:translate(-1px, -1px) rotate(1deg)}20%{-webkit-transform:translate(2px, 2px) rotate(0deg);transform:translate(2px, 2px) rotate(0deg)}22%{-webkit-transform:translate(1px, -2px) rotate(-1deg);transform:translate(1px, -2px) rotate(-1deg)}24%{-webkit-transform:translate(0, 0) rotate(0deg);transform:translate(0, 0) rotate(0deg)}}@keyframes shakeityeah{0%{-webkit-transform:translate(2px, 1px) rotate(0deg);transform:translate(2px, 1px) rotate(0deg)}2%{-webkit-transform:translate(-1px, -2px) rotate(-1deg);transform:translate(-1px, -2px) rotate(-1deg)}4%{-webkit-transform:translate(-3px, 0) rotate(1deg);transform:translate(-3px, 0) rotate(1deg)}8%{-webkit-transform:translate(0, 2px) rotate(0deg);transform:translate(0, 2px) rotate(0deg)}10%{-webkit-transform:translate(1px, -1px) rotate(1deg);transform:translate(1px, -1px) rotate(1deg)}12%{-webkit-transform:translate(-1px, 2px) rotate(-1deg);transform:translate(-1px, 2px) rotate(-1deg)}14%{-webkit-transform:translate(-3px, 1px) rotate(0deg);transform:translate(-3px, 1px) rotate(0deg)}16%{-webkit-transform:translate(2px, 1px) rotate(-1deg);transform:translate(2px, 1px) rotate(-1deg)}18%{-webkit-transform:translate(-1px, -1px) rotate(1deg);transform:translate(-1px, -1px) rotate(1deg)}20%{-webkit-transform:translate(2px, 2px) rotate(0deg);transform:translate(2px, 2px) rotate(0deg)}22%{-webkit-transform:translate(1px, -2px) rotate(-1deg);transform:translate(1px, -2px) rotate(-1deg)}24%{-webkit-transform:translate(0, 0) rotate(0deg);transform:translate(0, 0) rotate(0deg)}}.btn-shakeityeah{-webkit-animation-duration:4s;animation-duration:4s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:shakeityeah;animation-name:shakeityeah;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-transform-origin:50% 50%;-ms-transform-origin:50% 50%;transform-origin:50% 50%}#buddylist{bottom:0;position:absolute;right:0;top:0;width:285px;z-index:50}#buddylist:before{background:#f8f8f8;border-bottom:1px solid #e7e7e7;border-bottom-left-radius:6px;border-left:1px solid #e7e7e7;border-top:1px solid #e7e7e7;border-top-left-radius:6px;bottom:0;color:rgba(0,0,0,0.3);content:'\f100';cursor:pointer;display:none;font-family:FontAwesome;font-size:1.8em;height:55px;left:0;line-height:55px;margin:auto;padding-right:4px;pointer-events:auto;position:absolute;text-align:center;top:0;width:26px;z-index:1}.withBuddylist #buddylist:before{content:'\f101';padding-right:0}.withBuddylistAutoHide #buddylist:before{display:block}.buddylist{background:#f8f8f8;border-left:1px solid #e7e7e7;bottom:0;left:25px;overflow-x:hidden;overflow-y:auto;pointer-events:auto;position:absolute;right:0;top:0}.buddylist.loading .buddylistloading{display:block}.buddylist.empty .buddylistempty{display:block}.buddylist .buddycontainer{pointer-events:auto;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.buddylist .buddylistempty{bottom:0;color:#b3b3b3;display:none;font-size:1.4em;height:2em;left:0;margin:auto;padding:.4em;position:absolute;right:0;text-align:center;top:0}.buddylist .buddylistloading{bottom:0;color:#b3b3b3;display:none;font-size:1.4em;height:2em;margin:auto;padding:.4em;position:absolute;right:0;text-align:center}.buddy{-webkit-tap-highlight-color:transparent;background:#fff;border-bottom:1px solid #e7e7e7;cursor:pointer;display:block;font-size:13px;min-height:66px;overflow:hidden;position:relative;text-align:left;width:100%}.buddy:hover{background:rgba(255,255,255,0.5)}.buddy.withSubline .buddy1,.buddy.contact .buddy1{top:15px}.buddy.withSubline .buddy2,.buddy.contact .buddy2{display:block}.buddy.hovered .buddyactions{right:0}.buddy.hovered .buddysessions{max-height:999px}.buddy .fa.contact:before{content:'\f006'}.buddy.contact .fa.contact:before{content:'\f005'}.buddy.isself .fa.contact:before{content:'\f192'}.buddy .buddyPicture{background:#84b819;border-radius:2px;float:left;height:46px;margin:10px;overflow:hidden;position:relative;text-align:center;width:46px}.buddy .buddyPicture .fa{color:#009534;font-size:3em;line-height:46px}.buddy .buddyPicture img{bottom:0;display:block;left:0;max-height:100%;max-width:100%;position:absolute;right:0;top:0}.buddy .buddyPictureSmall{height:30px;margin:0;margin-left:0;margin-right:0;width:30px}.buddy .buddyPictureSmall .fa{font-size:2em;line-height:30px}.buddy .buddy1{color:#262626;font-size:14px;font-weight:bold;height:28px;left:65px;overflow:hidden;position:absolute;right:4px;text-overflow:ellipsis;top:24px;white-space:nowrap}.buddy .buddy2{color:rgba(0,0,0,0.5);display:none;left:65px;overflow:hidden;position:absolute;right:0;top:33px;white-space:nowrap}.buddy .buddy3{display:inline-block;overflow:hidden;padding:0 6px;text-align:left;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap;width:120px}.buddy .buddyactions{background:rgba(255,255,255,0.5);height:66px;line-height:66px;padding:0 10px;position:absolute;right:-125px;text-align:right;top:0;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transition-property:right;transition-property:right;white-space:nowrap;z-index:5}.buddy .buddyactions .btn{font-size:1.6em;height:40px;line-height:40px;padding:0;text-align:center;vertical-align:middle;width:42px}.buddy .buddysessions{margin-bottom:10px;margin-top:56px;max-height:0;-webkit-transition-delay:.1s;transition-delay:.1s;-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-property:max-height;transition-property:max-height}.buddy .buddysessions ul{border-left:1px dotted #e7e7e7;border-right:1px dotted #e7e7e7;margin:0 14px;padding-left:0;padding-top:10px}.buddy .buddysessions ul li{list-style-type:none;margin-bottom:2px;margin-left:0}.buddy .buddysessions ul li .btn-group{visibility:hidden}.buddy .buddysessions ul li:hover .btn-group{visibility:visible}.buddy .buddysessions .currentsession .buddy3{font-weight:bold}.buddyPictureCapture .picture{display:block;margin-bottom:5px}.buddyPictureCapture .videoPicture{margin-bottom:4px}.buddyPictureCapture .videoPicture .videoPictureVideo{background-color:#000;overflow:hidden;position:relative}.buddyPictureCapture .videoPicture video{object-fit:cover}.buddyPictureCapture .videoPictureVideo{height:200px;width:200px}.buddyPictureCapture .videoPictureVideo .videoPrev,.buddyPictureCapture .videoPictureVideo video,.buddyPictureCapture .videoPictureVideo .preview{height:100%;width:100%}.buddyPictureCapture .videoFlash{background-color:#fff;border:1px dotted #e7e7e7;bottom:0;left:0;position:absolute;right:0;top:0;visibility:hidden;z-index:5}.buddyPictureCapture .videoFlash.flash{visibility:visible}.buddyPictureCapture .preview{left:0;position:absolute;top:0}.buddyPictureCapture .preview.previewPicture{position:relative}.buddyPictureCapture .btn-takePicture,.buddyPictureCapture .btn-retakePicture{left:0;margin:0 auto;max-width:40%;position:absolute;right:0;top:50%}.buddyPictureCapture .btn-retakePicture{visibility:hidden}.buddyPictureCapture .videoPictureVideo:hover .btn-retakePicture{visibility:visible}.buddyPictureCapture .countdownPicture{color:#f8f8f8;font-size:45px;left:0;margin:0 auto;opacity:.8;position:absolute;right:0;text-align:center;text-shadow:0 0 5px #000;top:75px}.buddyPictureUpload{position:relative}.buddyPictureUpload .loader{left:90px;position:absolute;z-index:1}.buddyPictureUpload .loader .fa-spin{color:#737373}.buddyPictureUpload>p{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.buddyPictureUpload .showUploadPicture{background-color:#f8f8f8;border:1px solid #e7e7e7;height:200px;line-height:200px;margin-bottom:10px;overflow:hidden;position:relative;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:200px}.buddyPictureUpload .showUploadPicture.imgData{background-color:#000}.buddyPictureUpload .showUploadPicture.imgData .chooseUploadPicture{display:none}.buddyPictureUpload .showUploadPicture.imgData:hover .imageUtilites{visibility:visible}.buddyPictureUpload .showUploadPicture .chooseUploadPicture{color:#737373;left:0;margin:0 auto;position:absolute;right:0;z-index:1}.buddyPictureUpload .showUploadPicture .fa{color:#f8f8f8;opacity:.8;text-shadow:0 0 5px #000}.buddyPictureUpload .preview{left:0;position:relative;top:0}.buddyPictureUpload .imageUtilites{line-height:30px;position:absolute;visibility:hidden;width:200px;z-index:1}.buddyPictureUpload .imageUtilites .fa{cursor:pointer;font-size:40px;height:50px;width:50px}.buddyPictureUpload .moveHorizontal{position:relative;top:-4px}.buddyPictureUpload .moveVertical{left:158px;position:absolute}.buddyPictureUpload .resize{position:relative;top:108px}#settings{background:#fff;border-left:1px solid #e7e7e7;bottom:0;padding-right:20px;position:fixed;right:-520px;top:51px;-webkit-transition:right 200ms ease-in-out;transition:right 200ms ease-in-out;width:520px;z-index:50}#settings.show{right:0}@media only screen and (max-width: 630px){#settings.show{background:#fff;left:0;width:auto}}@media only screen and (max-width: 630px){#settings.show .form-actions{bottom:0;height:60px;left:0;margin-bottom:0;padding:6px 0 6px 120px;position:fixed;right:0}}.settings{background:#fff;bottom:0;left:0;overflow-x:hidden;overflow-y:auto;padding:10px;position:absolute;right:0;top:0}@media only screen and (max-width: 630px){.settings{padding-bottom:10px}}.settings .version{color:#ccc;font-size:10px;position:absolute;right:10px;top:10px}@media only screen and (max-width: 630px){.settings .form-horizontal .controls{margin-left:110px}}@media only screen and (max-width: 630px){.settings .form-horizontal .control-label{width:100px;word-wrap:break-word}}settings-advanced{display:block;padding-top:15px}#chat{bottom:0;min-width:260px;opacity:0;pointer-events:none;position:absolute;right:260px;top:0;width:260px;z-index:45}.withChat #chat{opacity:1}.withChat.withChatMaximized #chat{left:0;width:auto}.withChat .chat{pointer-events:auto}.chatcontainer{background:#f8f8f8;bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.showchatlist .chatpane{right:100%}.showchatlist .chatlist{left:0}.chatlist{background:#f8f8f8;bottom:0;left:100%;position:absolute;top:0;width:100%}.chatlist .list-group{margin-bottom:-1px;margin-top:-1px;max-height:100%;overflow-x:hidden;overflow-y:auto}.chatlist .list-group-item{border-left:0;border-radius:0;border-right:0;line-height:26px;min-height:51px;padding-right:70px;position:relative}.chatlist .list-group-item.newmessage{-webkit-animation:newmessage 1s ease -.3s infinite;animation:newmessage 1s ease -.3s infinite}.chatlist .list-group-item.disabled{color:#aaa}.chatlist .list-group-item:hover button{display:inline}.chatlist .list-group-item .fa-lg{display:inline-block;text-align:center;width:18px}.chatlist .list-group-item .badge{background:#84b819;border:1px solid #fff;position:absolute;right:50px;top:14px}.chatlist .list-group-item button{display:none;position:absolute;right:10px}.chatpane{-webkit-backface-visibility:hidden;backface-visibility:hidden;bottom:0;position:absolute;right:0;top:0;width:100%}.chat{background:#f8f8f8;bottom:0;display:none;left:0;overflow:hidden;position:absolute;right:0;top:0}.chat.newmessage .chatheadertitle:after{content:'***';position:absolute;right:32px;top:2px}.chat.newmessage .chatheader{-webkit-animation:newmessage 1s ease -.3s infinite;animation:newmessage 1s ease -.3s infinite}.chat.active.visible{display:block}.chat.with_pictures .message.is_self{padding-right:54px}.chat.with_pictures .message.is_self .timestamp{right:58px}.chat.with_pictures .message.is_remote{padding-left:58px}.chat .chatbodybottom{background:#f8f8f8;bottom:1px;left:0;margin:0 auto;position:absolute;right:0}@media (max-height: 210px){.chat .chatbodybottom{height:auto}}.chat .typinghint{color:#aaa;font-size:.8em;height:14px;overflow:hidden;padding:0 6px;white-space:nowrap}@media (max-height: 210px){.chat .typinghint{display:none}}.chat .inputbox{position:relative}@media (max-height: 210px){.chat .inputbox{height:auto}}.chat .inputbox .btn{display:none;padding:.5em 1em;position:absolute;right:6px;top:1px}.chat .inputbox>div{border-top:1px solid #e7e7e7}.chat .input{border-color:transparent;border-radius:0;-webkit-box-shadow:none;box-shadow:none;display:block;height:54px;margin:0;max-height:54px;resize:none;width:100%}@media (max-height: 210px){.chat .input{max-height:2.5em}}.chat .input:active,.chat .input:focus{border-color:#66afe9}.chat .outputbox{bottom:75px;left:0;position:absolute;right:0;top:0}@media (max-height: 210px){.chat .outputbox{bottom:45px}}.chat .output{height:100%;overflow-x:hidden;overflow-y:auto;padding:.4em 0}.chat .output>i{clear:both;color:#aaa;display:block;font-size:.8em;padding:6px 0;text-align:center}.chat .output>i.p2p{font-weight:bold;padding:6px 0}.chat .message{background:#fff;border:1px solid transparent;border-radius:6px;-webkit-box-shadow:0 0 2px 0 rgba(0,0,0,0.03);box-shadow:0 0 2px 0 rgba(0,0,0,0.03);clear:both;display:block;margin:0 4px 2px 18px;padding:8px;position:relative;word-wrap:break-word}.chat .message ul{list-style-type:none;margin:0;padding-left:0}.chat .message .timestamp{color:#aaa;font-size:.8em;position:absolute;right:8px;text-align:right;top:8px}.chat .message .timestamp-space{float:right;height:10px;width:40px}.chat .message strong{display:block;margin-right:40px;overflow:hidden;padding-bottom:2px;text-overflow:ellipsis;white-space:nowrap}.chat .message li{line-height:1.1em;margin:4px 0;padding-left:1.2em;position:relative}.chat .message li:before{color:#ccc;content:'\f075';font-family:FontAwesome;left:0;position:absolute;text-align:center;width:12px}.chat .message li.unread:before{color:#fe9a2e;content:""}.chat .message li.sending:before{color:#ccc;content:""}.chat .message li.sent:before{color:#5882fa;content:""}.chat .message li.delivered:before{color:#5882fa;content:""}.chat .message li.received:before{color:#84b819;content:""}.chat .message li.read:before{color:#ccc;content:""}.chat .message .buddyPicture{background:#84b819;border-radius:2px;height:46px;left:4px;overflow:hidden;position:absolute;text-align:center;top:4px;width:46px;z-index:0}.chat .message .buddyPicture .fa{color:#009534;line-height:46px}.chat .message .buddyPicture img{bottom:0;display:block;left:0;max-height:100%;max-width:100%;position:absolute;right:0;top:0}.chat .message:before,.chat .message:after{border-style:solid;content:'';display:block;position:absolute;width:0}.chat .message.is_remote{background:#fff}.chat .message.is_remote:before{border-color:transparent #eee;border-width:7px 11px 7px 0;bottom:auto;left:-12px;top:4px}.chat .message.is_remote:after{border-color:transparent #fff;border-width:6px 10px 6px 0;bottom:auto;left:-11px;top:5px}.chat .message.is_self{background:#fff;margin-left:4px;margin-right:18px;padding-right:0}.chat .message.is_self:before{border-color:transparent #eee;border-width:7px 0 7px 11px;bottom:4px;bottom:auto;right:-12px}.chat .message.is_self:after{border-color:transparent #fff;border-width:6px 0 6px 10px;bottom:5px;bottom:auto;right:-11px}.chat .message.is_self li:before{color:#ccc;-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.chat .message.is_self .buddyPicture{left:auto;right:4px}.chat .message.with_hoverimage .buddyPicture{overflow:visible;z-index:initial}.chat .message.with_hoverimage .buddyPicture:hover .buddyInfoActions{height:40px;opacity:1}.chat .message.with_hoverimage .buddyInfoActions{cursor:default;display:inline-block;height:0;left:0;opacity:0;overflow:hidden;position:absolute;top:48px;-webkit-transition:opacity 0.1s .1s linear, height .4s .1s ease-out;transition:opacity 0.1s .1s linear, height .4s .1s ease-out;white-space:nowrap;z-index:1}.chat .message.with_hoverimage .buddyInfoActions .btn-group{display:block;margin:0 auto;width:55px}.chat .message.with_hoverimage .buddyInfoActions .btn-primary{padding:2px 5px}.chat .message.with_hoverimage .buddyInfoActions .fa{color:#fff;line-height:24px}.chatmenu{height:36px;left:0;padding:2px 4px;position:absolute;right:0;top:36px}@media (max-height: 210px){.chatmenu{display:none}}.chatbody{border-left:1px solid #e7e7e7;bottom:-1px;left:0;position:absolute;right:0;top:74px}@media (max-height: 210px){.chatbody{border-top:1px solid #e7e7e7;top:0;top:0}}.chatheader{background:rgba(255,255,255,0.9);border-bottom:1px solid #e7e7e7;border-left:1px solid #e7e7e7;height:36px;left:0;line-height:34px;padding:0 4px 0 8px;position:absolute;right:0;top:0}@media (max-height: 210px){.chatheader{display:none}}.chatheader .chatstatusicon{cursor:pointer;display:block;font-size:1.4em;height:36px;left:0;position:absolute;text-align:center;top:0;width:36px}.chatheader .chatheadertitle{display:inline;padding-left:28px}.chatheader .ctrl{color:rgba(0,0,0,0.3);position:absolute;right:1px;top:0}.chatheader .ctrl .fa{cursor:pointer;padding:6px}.chatheader span{display:inline-block;max-width:60%;overflow:hidden;pointer-events:none;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap}@-webkit-keyframes newmessage{0%{background-color:#84b819}50%{background-color:#f8f8f8}100%{background-color:#84b819}}@keyframes newmessage{0%{background-color:#84b819}50%{background-color:#f8f8f8}100%{background-color:#84b819}}.withChat #help,.withBuddylist #help{right:260px}.withChat.withBuddylist #help,.withSettings #help{right:520px}#help{bottom:10px;color:#aaa;font-size:1.1em;left:0;margin:0 auto;position:absolute;right:0;text-shadow:0 0 5px #000;top:80px;-webkit-transition:right 200ms ease-in-out;transition:right 200ms ease-in-out;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:350px}@media only screen and (max-width: 400px){.help{display:none}}@media only screen and (min-width: 400px) and (max-width: 1020px){.help{font-size:1em;width:250px}}.help>div{margin:0 10px}.help .help-subline{color:#888;padding:20px 0}.help .btn{text-shadow:none}#audiolevel{left:0;margin:0 auto;position:fixed;right:0;top:43px;width:400px;z-index:60}#audiolevel .audio-level{background:#84b819;background:gradient(linear, left top, left bottom, color-stop(0%, #84b819), color-stop(50%, #a1d54f), color-stop(51%, #80c217), color-stop(100%, #7cbc0a));background:-webkit-gradient(linear, left top, left bottom, from(#84b819), color-stop(50%, #a1d54f), color-stop(51%, #80c217), to(#7cbc0a));background:-webkit-linear-gradient(top, #84b819 0%, #a1d54f 50%, #80c217 51%, #7cbc0a 100%);background:linear-gradient(to bottom, #84b819 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%);border-radius:0 0 2px 2px;height:4px;left:0;margin:0 auto;position:absolute;right:0;-webkit-transition:width .05s ease-in-out;transition:width .05s ease-in-out;width:0}.file-info{background:#fff;border:1px solid #ddd;border-radius:4px;max-width:170px;padding:1em;position:relative;text-align:center}.file-info.downloader .anim{margin-left:-40px}.file-info.downloader .file-info-size{margin-bottom:10px}.file-info.downloading .file-info-size{border-color:#ddd}.file-info>div{position:relative;z-index:3}.file-info .file-info-bg{bottom:0;color:#eee;font-size:20em;left:41px;overflow:hidden;position:absolute;right:0;top:-82px;z-index:2}.file-info .actions{left:50%;margin-left:10px;position:absolute;text-align:left;top:14px}.file-info .uploader .file-info-speed{bottom:6px}.file-info .uploader .actions{margin-left:30px;opacity:0}.file-info .uploader .anim{margin-left:0}.file-info .uploader .hovercontrol:hover .anim{margin-left:-50px}.file-info .uploader .hovercontrol:hover .actions{margin-left:0;opacity:1}.file-info .uploader .hovercontrol>div{-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.is_remote .file-info{background:#fff;border:1px solid #ddd}.is_remote .file-info .file-info-bg{color:#eee;font-size:20em}.file-info-name{font-size:1.1em;margin:.2em 0;min-width:140px;padding:0 .2em}.file-info-size{font-size:.8em;height:20px;position:relative}.file-info-size>span{display:block;left:0;margin:0 auto;padding:3px;position:absolute;right:0;text-shadow:1px 1px 1px #fff;top:0;z-index:5}.file-info-size>div{bottom:0;-webkit-box-shadow:none !important;box-shadow:none !important;left:0;position:absolute;top:0;width:0;z-index:0}.file-info-size>div.progress-bar{opacity:.5}.file-info-size>div.progress-bar.download{opacity:1;z-index:1}.file-info-speed{bottom:8px;font-size:.8em;left:0;position:absolute;right:0;text-align:center}@media only screen and (max-width: 630px){.mainScreenshare #audiovideo,.mainPresentation #audiovideo{display:none}}.withChat #audiovideo,.withBuddylist #audiovideo{right:260px}.withBuddylist.withChat #audiovideo{right:520px}#audiovideo{bottom:0;left:0;position:absolute;right:0;top:51px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media only screen and (max-width: 590px){#audiovideo{right:0}}#audiovideo.fullscreen{bottom:0 !important;left:0 !important;right:0 !important;top:0 !important}#audiovideo.fullscreen .remoteVideo .peerActions{display:none}.audiovideo{bottom:0;left:0;position:absolute;right:0;top:0}.audiovideo.active{-webkit-perspective:1000;perspective:1000}.audiovideo.active:hover .overlayActions{opacity:.3}.audiovideo.active .overlayActions:hover{opacity:.6}.audiovideo.active .audiovideoBase{-webkit-transform:rotateY(180deg);-ms-transform:rotateY(180deg);transform:rotateY(180deg)}.audiovideo .audiovideoBase{height:100%;position:relative;-webkit-transform:rotateY(0deg);-ms-transform:rotateY(0deg);transform:rotateY(0deg);-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:-webkit-transform;transition-property:transform;width:100%;z-index:2}.audiovideo .localContainer{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1);z-index:2;overflow:hidden}.audiovideo video{object-fit:cover}.audiovideo .onlyaudio{bottom:0;color:rgba(255,255,255,0.3);display:none;font-size:1em;left:0;pointer-events:auto;position:absolute;right:0;text-align:center;top:0}.audiovideo .onlyaudio:before{content:'';display:inline-block;height:100%;vertical-align:middle}.audiovideo .onlyaudio>*{font-size:6em;vertical-align:middle}.audiovideo .remoteContainer{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;-webkit-transform:rotateY(180deg);-ms-transform:rotateY(180deg);transform:rotateY(180deg);z-index:2}.audiovideo .miniContainer{background:#000;bottom:2px;height:100%;max-height:18%;opacity:0;position:absolute;right:2px;-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1);-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-property:opacity;transition-property:opacity;z-index:25;overflow:hidden}.audiovideo .miniContainer.visible{opacity:1}.audiovideo.cameraMute .miniContainer,.audiovideo.cameraMute .localVideos{background:#666}.audiovideo.cameraMute .miniContainer .onlyaudio,.audiovideo.cameraMute .localVideos .onlyaudio{display:block}.audiovideo.cameraMute .miniContainer video,.audiovideo.cameraMute .localVideos video{visibility:hidden}.audiovideo .miniVideo{display:block;height:100%;max-height:100%;max-width:100%;width:100%}.audiovideo .localVideo{background:rgba(0,0,0,0.4);display:block;max-height:100%;opacity:0;-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:opacity;transition-property:opacity;width:100%}.audiovideo .localVideos{bottom:0;left:0;position:absolute;right:0;top:0;-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:opacity;transition-property:opacity}.audiovideo .remoteVideos{bottom:0;left:0;opacity:0;position:absolute;right:0;top:0;-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:opacity;transition-property:opacity}.audiovideo .remoteVideos video{display:block;height:100%;width:100%}.audiovideo .overlayActions{background:rgba(0,0,0,0.9);bottom:0;height:140px;left:0;margin:auto 0;opacity:0;padding:3px 0;position:absolute;top:0;width:40px;z-index:5}.audiovideo .overlayActions .btn{color:#ccc;cursor:pointer;display:block;outline:0;text-shadow:0 0 5px #000;width:40px}.audiovideo .remoteVideo{background:rgba(0,0,0,0.4);display:inline-block;max-height:100%;max-width:100%;overflow:hidden;position:relative;vertical-align:bottom;width:100%}.audiovideo .remoteVideo.onlyaudioVideo{background:#666}.audiovideo .remoteVideo.onlyaudioVideo .onlyaudio{display:block}.audiovideo .remoteVideo.onlyaudioVideo video,.audiovideo .remoteVideo.dummy video{visibility:hidden}.audiovideo .remoteVideo.dummy .onlyaudio{display:block}.audiovideo .remoteVideo .peerActions{bottom:5%;left:40px;opacity:0;pointer-events:auto;position:absolute;right:40px;text-align:center;-webkit-transition-duration:.2s;transition-duration:.2s;-webkit-transition-property:opacity;transition-property:opacity;z-index:10}.audiovideo .remoteVideo .peerActions:hover{opacity:.5}.audiovideo .remoteVideo .peerActions i{font-size:3vw}.audiovideo .remoteVideo .peerLabel{bottom:4%;color:#fff;font-size:2.5vw;left:4%;max-width:30%;opacity:.7;overflow:hidden;padding:4px;position:absolute;text-overflow:ellipsis;text-shadow:0 0 4px #000;white-space:nowrap;z-index:8}.remoteVideo.talking .peerLabel{color:#84b819}.remoteVideo .peerLabel{-webkit-transition:color 500ms ease-out;transition:color 500ms ease-out}.remoteVideo .overlayLogo{background:url("../img/logo-overlay.png") no-repeat center;-webkit-background-size:100%;background-size:100%;height:20%;max-height:40px;max-width:111px;opacity:.5;pointer-events:none;position:absolute;right:2.5%;top:4%;width:20%;z-index:2}.miniContainer.talking:after{bottom:2px;-webkit-box-shadow:0 0 20px #84b819 inset;box-shadow:0 0 20px #84b819 inset;content:'';left:2px;position:absolute;right:2px;top:2px}.renderer-smally{background:#000;border-right:0;border-top:0;width:150px}.renderer-smally .remoteVideos{padding-bottom:85px}.renderer-smally .remoteVideo .peerLabel{font-size:.9em;font-weight:bold}.renderer-smally .remoteVideo .peerActions i{font-size:1em}.renderer-smally .miniContainer{bottom:0;height:85px;left:0;max-height:none;right:0}.renderer-onepeople .miniContainer .onlyaudio{font-size:.4em}.renderer-democrazy .remoteVideos .miniContainer{bottom:auto;display:inline-block;max-height:100%;max-width:100%;position:relative;right:auto;vertical-align:bottom}.renderer-democrazy .active .miniContainer{opacity:1}.renderer-conferencekiosk .remoteVideos{background:rgba(0,0,0,0.4);bottom:2px;min-height:108px;pointer-events:auto;text-align:center;top:auto;white-space:nowrap}.renderer-conferencekiosk .remoteVideos>div{cursor:pointer;height:108px;width:192px}.renderer-conferencekiosk .remoteVideos .overlayLogo{display:none}.renderer-conferencekiosk .remoteVideos .peerLabel,.renderer-conferencekiosk .remoteVideos .peerActions i{font-size:1.1em}.renderer-conferencekiosk .remoteVideos .peerLabel{background:rgba(0,0,0,0.9)}.renderer-conferencekiosk .miniContainer{height:108px;max-height:none;width:192px}.renderer-conferencekiosk .bigVideo{bottom:112px;left:0;margin:auto;opacity:0;position:absolute;right:0;top:2px;-webkit-transition-duration:2s;transition-duration:2s;-webkit-transition-property:opacity;transition-property:opacity}.renderer-conferencekiosk .bigVideo video{height:100%;width:100%}.renderer-auditorium{position:relative}.renderer-auditorium span:before{content:'\f183';left:50%;margin-left:-.8em;margin-top:-.5em;position:absolute;top:50%}.renderer-auditorium span:after{content:'\f183';margin-right:-.9em;margin-top:-.5em;position:absolute;right:50%;top:50%}.renderer-auditorium .remoteContainer{border-left:40px solid #000}.renderer-auditorium .remoteVideos{background:rgba(0,0,0,0.4);pointer-events:auto;top:180px;width:320px}.renderer-auditorium .remoteVideos video{height:100%;margin-top:-9px;object-fit:cover;width:100%}.renderer-auditorium .remoteVideos>div{cursor:pointer;display:inline-block;height:60px;width:80px}.renderer-auditorium .remoteVideos .overlayLogo{display:none}.renderer-auditorium .remoteVideos .peerLabel{background:rgba(0,0,0,0.9);bottom:0;font-size:.6em;left:0;line-height:9px;max-width:100%;padding:0 4px;right:0}.renderer-auditorium .remoteVideos .peerActions{display:none}.renderer-auditorium .remoteVideos .miniContainer{max-height:auto;right:auto}.renderer-auditorium .bigVideo{height:180px;width:320px}.renderer-auditorium .bigVideo .remoteVideo,.renderer-auditorium .bigVideo .video{height:100%;width:100%}.renderer-auditorium .bigVideo .peerLabel{bottom:8%;font-size:1vw;max-width:40%}.mainScreenshare #screenshare{display:block}.screenshare{bottom:0;left:0;position:absolute;right:0;top:0}.screenshare .overlaybar{bottom:0;left:0;right:0}.screensharepane{background:#000;bottom:0;left:0;overflow:auto;position:absolute;right:0;top:0}.screensharepane .remotescreen{position:relative}.screensharepane video{max-height:99%;width:100%}.remotesize .screensharepane video{max-height:none;width:auto}#roombar{left:0;min-width:260px;position:absolute;right:0;top:51px;z-index:4}#roombar .roombar{left:0;position:absolute;right:0;top:0}.fa.link{color:#aaa}.fa.email{color:#aaa}.fa.facebook{color:#45619d}.fa.google{color:#dd4b39}.fa.twitter{color:#00aced}.fa.xing{color:#fff}.contactsmanager .desc{font-size:20px;font-weight:normal;text-align:baseline}.contactsmanager .addbtn{font-size:14px}.contactsmanager .addbtn .fa-users{font-size:22px}.contactsmanager .addbtn .fa-plus{font-size:15px}.contactsmanager .editpicture{float:left;margin-right:20px;vertical-align:middle}.contactsmanager .uploadbtn{margin-top:7px}.contactsmanager .editlist{max-height:250px;overflow-y:auto}.contactsmanager .picture{border-bottom:0;cursor:auto;display:table-cell;min-height:46px;position:static;width:auto}.contactsmanager .picture .buddyPicture{margin:0 0 0 10px}.contactsmanager .table{margin-bottom:0}.contactsmanager tr:first-child td{border-top:0}.contactsmanager .name{text-align:left;vertical-align:middle;width:40%}.contactsmanager .action{padding-right:15px;text-align:right;vertical-align:middle}.contactsmanageredit .buddy .buddyPicture{margin:0}.search:before{content:'\f002';font-family:'fontAwesome';font-size:14px;left:22px;opacity:.4;position:absolute;top:6px}.search ~ input{padding-left:25px}.mainPresentation #presentation{display:block}.presentation{bottom:0;left:0;position:absolute;right:0;top:0}.presentation .overlaybar{bottom:0;left:0;right:0;text-align:center}.presentation .overlaybar .overlaybar-content{max-width:100%}.presentation .overlaybar .overlaybar-content .pagecontrol{height:30px}.presentation .overlaybar .btn-prev{left:40px}.presentation .overlaybar .btn-next{left:auto;right:0}.presentation .overlaybar .overlaybar-button{font-size:20px;line-height:28px;padding:4px 6px;position:absolute;top:0}.presentation .thumbnail{color:#333;display:inline-block;height:122px;margin-left:20px;margin-top:20px;position:relative;text-shadow:none;vertical-align:middle;width:160px}.presentation .thumbnail:first-child{margin-left:0}.presentation .thumbnail.presentable{cursor:pointer}.presentation .thumbnail:hover .presentation-action{display:block}.presentation .thumbnail:hover .notavailable{display:block}.presentation .thumbnail .caption{overflow:hidden;padding-bottom:0;text-overflow:ellipsis}.presentation .thumbnail .caption .size{font-size:10px}.presentation .thumbnail .caption .progress{position:relative}.presentation .thumbnail .caption .download-info{bottom:0;color:#333;left:0;line-height:20px;position:absolute;right:0;text-shadow:1px 1px 1px #fff;top:0}.presentation .thumbnail .active{bottom:0;color:#84b819;font-size:10em;left:0;opacity:.7;position:absolute;right:0;text-align:center;top:0}.presentation .thumbnail .notavailable{bottom:0;color:#d2322d;display:none;font-size:10em;left:0;opacity:.25;position:absolute;right:0;text-align:center;top:0}.presentation .thumbnail .presentation-action{display:none;position:absolute;top:1px}.presentation .thumbnail .download{left:1px}.presentation .thumbnail .delete{right:1px}.presentation .thumbnail .filetype{font-size:5em}.presentationpane{bottom:0;left:0;overflow:auto;position:absolute;right:0;top:0}.presentationpane .welcome{padding:0}.presentationpane .welcome h1{white-space:normal}.presentationpane .welcome .btn{margin-top:30px}.presentationpane .welcome .progress span{text-shadow:none}.presentationpane .welcome .progress .download-info{color:#333;left:0;position:absolute;text-shadow:1px 1px 1px #fff;width:100%}.presentationpane .canvasContainer{height:100%;width:100%;overflow:hidden}.presentationpane .canvasContainer iframe{border:0;height:100%;width:100%}.pageinfo input{display:inline;width:70px}.presentations{height:156px;margin-left:-25px;margin-right:10px;overflow-x:auto;overflow-y:hidden;white-space:nowrap}.mainYoutubevideo #youtubevideo{display:block}.youtubevideo{bottom:0;left:0;position:absolute;right:0;top:0}.youtubevideo .click-container{bottom:0;left:0;position:absolute;right:0;top:0;z-index:5}.youtubevideo .welcome{max-width:700px}.youtubevideo .welcome h1{margin-top:10px}.youtubevideo .welcome .welcome-container{max-width:700px}.youtubevideo .welcome .welcome-logo{background:transparent;font-size:10em}.youtubevideo .overlaybar{bottom:0;left:0;right:0}.youtubevideo .overlaybar-content{max-width:100%;width:100%}.youtubevideo .overlaybar-content form .overlaybar-buttons{position:absolute;right:23px;top:6px}.youtubevideo .overlaybar-input{padding-right:15px;position:relative;width:100%}.youtubevideopane{bottom:0;left:0;overflow:auto;position:absolute;right:0;top:0}.youtubecontainer{position:relative}.youtubecontainer.fullscreen{width:100%}.youtubeplayerinfo{bottom:10%;left:0;opacity:0;pointer-events:auto;position:absolute;right:0;text-align:center;-webkit-transition-duration:.2s;transition-duration:.2s;-webkit-transition-property:opacity;transition-property:opacity;z-index:10}.youtubeplayerinfo:hover{opacity:.8}.youtubeplayerinfo div{background-color:#f9f2f4;border-radius:10px;display:inline-block;font-size:2em;padding:20px 40px}.volumecontrol{background:rgba(0,0,0,0.6);bottom:0;left:0;opacity:0;padding:4px;pointer-events:auto;position:absolute;right:0;z-index:10}.volumecontrol:hover{opacity:1}.volume-button{display:inline;min-width:38px}.volumebar{display:inline-block;padding:6px 8px;vertical-align:middle}.volumebar .bar{-webkit-appearance:none;background-color:#aaa;border:1px solid #aaa;height:3px;outline:0;width:100px}.volumebar .bar::-webkit-slider-thumb{-webkit-appearance:none;background-color:#fff;height:20px;width:6px}.volumebar .bar::-moz-range-track{background:#aaa;border:0}.volumebar .bar::-moz-range-thumb{background-color:#fff;border-radius:0;height:20px;width:6px}.volumebar .bar::-moz-focusring{outline:1px solid #aaa;outline-offset:-1px}.modal{overflow-y:auto}#toast-container>.toast{background-image:none !important}#toast-container>.toast:before{color:#fff;float:left;font-family:FontAwesome;font-size:20px;line-height:20px;margin:auto .5em auto -1.5em;padding-right:.5em;position:fixed}#toast-container>.toast-warning:before{content:'\f05a'}#toast-container>.toast-error:before{content:'\f05a'}#toast-container>.toast-info:before{content:'\f05a'}#toast-container>.toast-success:before{content:'\f05a'}#toast-container>:hover,#toast-container>div{-webkit-box-shadow:none !important;box-shadow:none !important}.toast-info{background-color:#5bc0de}.toast-close-button{font-size:1em;top:-.6em}#toast-container>div{filter:alpha(opacity=100);opacity:1} diff --git a/static/js/app.js b/static/js/app.js index 488e1ce6..c6cc7736 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -87,6 +87,22 @@ define([ var src; if (data && data.locale_data) { src = data.locale_data[domain]; + // Support older po files built for older jed (see https://github.com/SlexAxton/Jed/issues/36). + var count = 0; + var v; + for (var k in src) { + if (src.hasOwnProperty(k)) { + v = src[k]; + if (v.constructor === Array && v[0] === null) { + v.shift(); + } else { + count++; + } + if (count > 1) { + break; + } + } + } } var dst = this.data.locale_data[domain]; if (!dst) { @@ -138,7 +154,7 @@ define([ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|filesystem|blob):/); $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|filesystem|blob):|data:image\//); // Setup routing - $routeProvider.when("/:room", {}); + $routeProvider.when("/:room*", {}); // Use HTML5 routing. $locationProvider.html5Mode(true); }]); @@ -174,7 +190,17 @@ define([ app.directive("spreedWebrtc", [function() { return { restrict: "A", - controller: "MediastreamController" + scope: false, + controller: "AppController" + } + }]); + + app.directive("uiLogo", ["globalContext", function(globalContext) { + return { + restrict: "A", + link: function($scope, $element, $attrs) { + $attrs.$set("title", globalContext.Cfg.Title || ""); + } } }]); @@ -182,7 +208,7 @@ define([ }; - // Our API version as float. This value is incremented on + // Our client side API version as float. This value is incremented on // breaking changes to plugins can check on it. var apiversion = 1.1; diff --git a/static/js/base.js b/static/js/base.js index b24dd8ca..115e77f6 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -32,4 +32,6 @@ define([ // Helper module to put non dependency base libraries together. 'rAF', 'humanize', 'sha', - 'sjcl'], function() {}); + 'sjcl', + 'text', + 'webfont'], function() {}); diff --git a/static/js/controllers/appcontroller.js b/static/js/controllers/appcontroller.js new file mode 100644 index 00000000..2d3d6dfd --- /dev/null +++ b/static/js/controllers/appcontroller.js @@ -0,0 +1,122 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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", "angular", "underscore"], function($, angular, _) { + + // AppController + return ["$scope", "$window", "appData", "userSettingsData", "$timeout", function($scope, $window, appData, userSettingsData, $timeout) { + + // Disable drag and drop. + $($window).on("dragover dragenter drop", function(event) { + event.preventDefault(); + }); + + appData.set($scope); + + // User related scope data. + $scope.authorizing = false; + $scope.roomsHistory = []; + $scope.defaults = { + displayName: null, + buddyPicture: null, + message: null, + settings: { + videoQuality: "high", + sendStereo: false, + maxFrameRate: 20, + defaultRoom: "", + language: "", + audioRenderToAssociatedSkin: true, + videoCpuOveruseDetection: true, + experimental: { + enabled: false, + audioEchoCancellation2: true, + audioAutoGainControl2: true, + audioNoiseSuppression2: true, + audioTypingNoiseDetection: true, + videoLeakyBucket: true, + videoNoiseReduction: false + } + } + }; + $scope.master = angular.copy($scope.defaults); + + $scope.update = function(user) { + $scope.master = angular.copy(user); + if (appData.flags.connected) { + $scope.updateStatus(); + } + $scope.refreshWebrtcSettings(); + }; + + $scope.reset = function() { + $scope.user = angular.copy($scope.master); + }; + + $scope.loadUserSettings = function() { + $scope.master = angular.copy($scope.defaults); + var storedUser = userSettingsData.load(); + if (storedUser) { + $scope.user = $.extend(true, {}, $scope.master, storedUser); + $scope.user.settings = $.extend(true, {}, $scope.user.settings, $scope.master.settings, $scope.user.settings); + $scope.update($scope.user); + $scope.loadedUser = storedUser.displayName && true; + } else { + $scope.loadedUser = false; + } + $scope.roomsHistory = []; + appData.e.triggerHandler("userSettingsLoaded", [$scope.loadedUser, $scope.user]); + $scope.reset(); + }; + + $scope.manualReloadApp = function(url) { + appData.flags.manualUnload = true; + if (url) { + $window.location.href = url; + $timeout(function() { + appData.flags.manualUnload = false; + }, 0); + } else { + $window.location.reload(true); + } + }; + + $scope.$on("room.joined", function(event, roomName) { + if (roomName) { + _.pull($scope.roomsHistory, roomName); + $scope.roomsHistory.unshift(roomName); + if ($scope.roomsHistory.length > 15) { + // Limit the history. + $scope.roomsHistory = $scope.roomsHistory.splice(0, 15); + } + } + }); + + appData.e.on("authorizing", function(event, authorizing) { + $scope.authorizing = !!authorizing; + }); + + $scope.reset(); // Call once for bootstrap. + + }]; + +}); \ No newline at end of file diff --git a/static/js/controllers/chatroomcontroller.js b/static/js/controllers/chatroomcontroller.js index dea0d14b..d6611ef8 100644 --- a/static/js/controllers/chatroomcontroller.js +++ b/static/js/controllers/chatroomcontroller.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,10 +20,10 @@ */ "use strict"; -define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html', 'text!partials/geolocation.html'], function($, _, moment, templateFileInfo, templateContactRequest, templateGeolocation) { +define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html', 'text!partials/geolocation.html', 'text!partials/picturehover.html'], function($, _, moment, templateFileInfo, templateContactRequest, templateGeolocation, templatePictureHover) { // ChatroomController - return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) { + return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", "mediaStream", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation, mediaStream) { $scope.outputElement = $element.find(".output"); $scope.inputElement = $element.find(".input"); @@ -50,6 +50,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p var fileInfo = $compile(templateFileInfo); var contactRequest = $compile(templateContactRequest); var geoLocation = $compile(templateGeolocation); + var pictureHover = $compile(templatePictureHover); var knowMessage = { r: {}, @@ -101,6 +102,42 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p } }; + var addPictureHover = function(from, msg, is_self) { + if (msg.picture && !is_self) { + var subscope = $scope.$new(); + subscope.startChat = function() { + $scope.$emit("startchat", from, { + autofocus: true, + restore: true + }); + }; + subscope.doCall = function() { + mediaStream.webrtc.doCall(from); + }; + pictureHover(subscope, function(clonedElement, scope) { + msg.picture.append(clonedElement); + }); + } else { + return; + } + msg.extra_css += "with_hoverimage "; + }; + + var showTitleAndPicture = function(from, msg, is_self) { + if ($scope.isgroupchat) { + msg.title = $(""); + msg.title.html(displayName(from, true)); + msg.extra_css += "with_name "; + var imgSrc = buddyImageSrc(from); + msg.picture = $('
'); + if (imgSrc) { + msg.picture.find("img").attr("src", imgSrc); + } + addPictureHover(from, msg, is_self); + } + }; + + // Make sure that chat links are openend in a new window. $element.on("click", function(event) { var elem = $(event.target); @@ -302,27 +339,16 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p var is_new_message = lastSender !== from; var is_self = from === sessonid; - var extra_css = ""; - var title = null; - var picture = null; - - var showTitleAndPicture = function() { - if ($scope.isgroupchat) { - title = $(""); - title.html(displayName(from, true)); - extra_css += "with_name "; - var imgSrc = buddyImageSrc(from); - picture = $('
'); - if (imgSrc) { - picture.find("img").attr("src", imgSrc); - } - } + var msg = { + extra_css: "", + title: null, + picture: null }; if (is_new_message) { lastSender = from; $scope.showdate(timestamp); - showTitleAndPicture() + showTitleAndPicture(from, msg, is_self); } var strMessage = s.join(" "); @@ -336,9 +362,9 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p } if (is_self) { - extra_css += "is_self"; + msg.extra_css += "is_self"; } else { - extra_css += "is_remote"; + msg.extra_css += "is_remote"; } if (timestamp) { var ts = $('
'); @@ -349,7 +375,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p nodes = ts; } } - return $scope.display(strMessage, nodes, extra_css, title, picture); + return $scope.display(strMessage, nodes, msg.extra_css, msg.title, msg.picture); }; diff --git a/static/js/controllers/contactsmanagercontroller.js b/static/js/controllers/contactsmanagercontroller.js index c5bae123..7fb00028 100644 --- a/static/js/controllers/contactsmanagercontroller.js +++ b/static/js/controllers/contactsmanagercontroller.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/controllers/contactsmanagereditcontroller.js b/static/js/controllers/contactsmanagereditcontroller.js index 214dad69..d0c74c2d 100644 --- a/static/js/controllers/contactsmanagereditcontroller.js +++ b/static/js/controllers/contactsmanagereditcontroller.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/controllers/controllers.js b/static/js/controllers/controllers.js index 10d51949..102124a6 100644 --- a/static/js/controllers/controllers.js +++ b/static/js/controllers/controllers.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -23,20 +23,22 @@ define([ 'underscore', - 'controllers/mediastreamcontroller', + 'controllers/uicontroller', 'controllers/statusmessagecontroller', 'controllers/chatroomcontroller', 'controllers/usersettingscontroller', 'controllers/contactsmanagercontroller', - 'controllers/contactsmanagereditcontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, UsersettingsController, ContactsmanagerController, ContactsmanagereditController) { + 'controllers/contactsmanagereditcontroller', + 'controllers/appcontroller'], function(_, UiController, StatusmessageController, ChatroomController, UsersettingsController, ContactsmanagerController, ContactsmanagereditController, AppController) { var controllers = { - MediastreamController: MediastreamController, + UiController: UiController, StatusmessageController: StatusmessageController, ChatroomController: ChatroomController, UsersettingsController: UsersettingsController, ContactsmanagerController: ContactsmanagerController, - ContactsmanagereditController: ContactsmanagereditController + ContactsmanagereditController: ContactsmanagereditController, + AppController: AppController }; var initialize = function(angModule) { diff --git a/static/js/controllers/statusmessagecontroller.js b/static/js/controllers/statusmessagecontroller.js index b606189e..5313fd0a 100644 --- a/static/js/controllers/statusmessagecontroller.js +++ b/static/js/controllers/statusmessagecontroller.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/controllers/mediastreamcontroller.js b/static/js/controllers/uicontroller.js similarity index 80% rename from static/js/controllers/mediastreamcontroller.js rename to static/js/controllers/uicontroller.js index e652cf62..2d927f4e 100644 --- a/static/js/controllers/mediastreamcontroller.js +++ b/static/js/controllers/uicontroller.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,21 +20,13 @@ */ "use strict"; -define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function($, _, angular, 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", "userSettingsData", "localStatus", "dialogs", "rooms", "constraints", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, userSettingsData, localStatus, dialogs, rooms, constraints) { - - /*console.log("route", $route, $routeParams, $location);*/ - - // Disable drag and drop. - $($window).on("dragover dragenter drop", function(event) { - event.preventDefault(); - }); + 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) { // Avoid accidential reloads or exits when in a call. - var manualUnload = false; $($window).on("beforeunload", function(event) { - if (manualUnload || !$scope.peer) { + if (appData.flags.manualUnload || !$scope.peer) { return; } return translation._("Close this window and disconnect?"); @@ -93,16 +85,13 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder "prompt": "question1" }); - appData.set($scope); - var displayName = safeDisplayName; - // Init STUN and TURN servers. - $scope.stun = mediaStream.config.StunURIs || []; - if (!$scope.stun.length) { - $scope.stun.push("stun:stun.l.google.com:19302") - } - $scope.turn = {}; // TURN servers are set on received.self. + // Init STUN from server config. + (function() { + var stun = mediaStream.config.StunURIs || []; + constraints.stun(stun); + })(); // Add browser details for easy access. $scope.isChrome = $window.webrtcDetectedBrowser === "chrome"; @@ -112,8 +101,8 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder // Add support status. $scope.supported = { screensharing: screensharing.supported, - renderToAssociatedSink: $window.navigator.platform.indexOf("Win") === 0 - } + constraints: constraints.supported + }; // Default scope data. $scope.status = "initializing"; @@ -133,50 +122,7 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder $scope.chatMessagesUnseen = 0; $scope.autoAccept = null; $scope.isCollapsed = true; - $scope.roomsHistory = []; - $scope.defaults = { - displayName: null, - buddyPicture: null, - message: null, - settings: { - videoQuality: "high", - sendStereo: false, - maxFrameRate: 20, - defaultRoom: "", - language: "", - audioRenderToAssociatedSkin: true, - videoCpuOveruseDetection: true, - experimental: { - enabled: false, - audioEchoCancellation2: true, - audioAutoGainControl2: true, - audioNoiseSuppression2: true, - audioTypingNoiseDetection: true, - videoLeakyBucket: true, - videoNoiseReduction: false - } - } - }; - $scope.master = angular.copy($scope.defaults); - - // Data voids. - var resurrect = null; - var reconnecting = false; - var connected = false; - var autoreconnect = true; - - $scope.update = function(user) { - $scope.master = angular.copy(user); - if (connected) { - $scope.updateStatus(); - } - $scope.refreshWebrtcSettings(); - }; - - $scope.reset = function() { - $scope.user = angular.copy($scope.master); - }; - $scope.reset(); // Call once for bootstrap. + $scope.usermedia = null; $scope.setStatus = function(status) { // This is the connection status to signaling server. @@ -201,34 +147,14 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder }; $scope.refreshWebrtcSettings = function() { - - if (!$window.webrtcDetectedBrowser) { - console.warn("This is not a WebRTC capable browser."); - return; - } - - var settings = $scope.master.settings; - - // Create iceServers from scope. - var iceServers = []; - var iceServer; - if ($scope.stun.length) { - iceServer = $window.createIceServers($scope.stun); - if (iceServer.length) { - iceServers.push.apply(iceServers, iceServer); - } - } - if ($scope.turn.urls && $scope.turn.urls.length) { - iceServer = $window.createIceServers($scope.turn.urls, $scope.turn.username, $scope.turn.password); - if (iceServer.length) { - iceServers.push.apply(iceServers, iceServer); - } - } - mediaStream.webrtc.settings.pcConfig.iceServers = iceServers; - // Refresh constraints. - constraints.refresh($scope.master.settings); - + constraints.refresh($scope.master.settings).then(function() { + var um = $scope.usermedia; + if (um && um.renegotiation && um.started) { + // Trigger renegotiation if supported and started. + um.doGetUserMediaWithConstraints(mediaStream.webrtc.settings.mediaConstraints); + } + }); }; $scope.refreshWebrtcSettings(); // Call once for bootstrap. @@ -257,34 +183,6 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder }; - $scope.manualReloadApp = function(url) { - manualUnload = true; - if (url) { - $window.location.href = url; - $timeout(function() { - manualUnload = false; - }, 0); - } else { - $window.location.reload(true); - } - }; - - $scope.loadUserSettings = function() { - $scope.master = angular.copy($scope.defaults); - var storedUser = userSettingsData.load(); - if (storedUser) { - $scope.user = $.extend(true, {}, $scope.master, storedUser); - $scope.user.settings = $.extend(true, {}, $scope.user.settings, $scope.master.settings, $scope.user.settings); - $scope.update($scope.user); - $scope.loadedUser = storedUser.displayName && true; - } else { - $scope.loadedUser = false; - } - $scope.roomsHistory = []; - appData.e.triggerHandler("userSettingsLoaded", [$scope.loadedUser, $scope.user]); - $scope.reset(); - }; - $scope.toggleBuddylist = (function() { var oldState = null; return function(status, force) { @@ -341,10 +239,13 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder scope.id = scope.myid = data.Id; scope.userid = scope.myuserid = data.Userid ? data.Userid : null; scope.suserid = data.Suserid ? data.Suserid : null; - scope.turn = data.Turn; - scope.stun = data.Stun; - scope.refreshWebrtcSettings(); }); + + // Set TURN and STUN data and refresh webrtc settings. + constraints.turn(data.Turn); + constraints.stun(data.Stun); + $scope.refreshWebrtcSettings(); + if (data.Version !== mediaStream.version) { console.info("Server was upgraded. Reload required."); if (!reloadDialog) { @@ -388,9 +289,9 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder } // Support resurrection shrine. - if (resurrect) { - var resurrection = resurrect; - resurrect = null; + if (appData.flags.resurrect) { + var resurrection = appData.flags.resurrect; + appData.flags.resurrect = null; $timeout(function() { if (resurrection.id === $scope.id) { console.log("Using resurrection shrine", resurrection); @@ -513,25 +414,38 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder alertify.dialog.alert(translation._("Oops") + "
" + message); }); + mediaStream.webrtc.e.on("usermedia", function(event, usermedia) { + safeApply($scope, function(scope) { + scope.usermedia = usermedia; + }); + }); + + appData.flags.autoreconnect = true; + appData.flags.autoreconnectDelay = 0; + var reconnect = function() { - if (connected && autoreconnect) { - if (resurrect === null) { + if (appData.flags.connected && appData.flags.autoreconnect) { + if (appData.flags.resurrect === null) { // Storage data at the resurrection shrine. - resurrect = { + appData.flags.resurrect = { status: $scope.getStatus(), id: $scope.id } - console.log("Stored data at the resurrection shrine", resurrect); + console.log("Stored data at the resurrection shrine", appData.flags.resurrect); } - if (!reconnecting) { - reconnecting = true; + if (!appData.flags.reconnecting) { + var delay = appData.flags.autoreconnectDelay; + if (delay < 10000) { + appData.flags.autoreconnectDelay += 500; + } + appData.flags.reconnecting = true; _.delay(function() { - if (autoreconnect) { + if (appData.flags.autoreconnect) { console.log("Requesting to reconnect ..."); mediaStream.reconnect(); } - reconnecting = false; - }, 500); + appData.flags.reconnecting = false; + }, delay); $scope.setStatus("reconnecting"); } else { console.warn("Already reconnecting ..."); @@ -550,12 +464,13 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder $scope.userid = $scope.suserid = null; switch (event.type) { case "open": - connected = true; + appData.flags.connected = true; + appData.flags.autoreconnectDelay = 0; $scope.updateStatus(true); $scope.setStatus("waiting"); break; case "error": - if (connected) { + if (appData.flags.connected) { reconnect(); } else { $scope.setStatus(event.type); @@ -792,17 +707,6 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder $scope.chatMessagesUnseen = $scope.chatMessagesUnseen - count; }); - $scope.$on("room.joined", function(event, roomName) { - if (roomName) { - _.pull($scope.roomsHistory, roomName); - $scope.roomsHistory.unshift(roomName); - if ($scope.roomsHistory.length > 15) { - // Limit the history. - $scope.roomsHistory = $scope.roomsHistory.splice(0, 15); - } - } - }); - _.defer(function() { if (!Modernizr.websockets) { alertify.dialog.alert(translation._("Your browser is not supported. Please upgrade to a current version.")); @@ -813,6 +717,15 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder alertify.dialog.alert(translation._("Your browser does not support WebRTC. No calls possible.")); return; } + if (mediaStream.config.Renegotiation && $window.webrtcDetectedBrowser === "firefox" && $window.webrtcDetectedVersion < 38) { + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1017888 + // and https://bugzilla.mozilla.org/show_bug.cgi?id=840728 + // and https://bugzilla.mozilla.org/show_bug.cgi?id=842455 + // XXX(longsleep): It seems that firefox has implemented new API which + // supports addTrack, removeTrack see http://w3c.github.io/mediacapture-main/#dom-mediastream-removetrack + console.warn("Renegotiation enabled -> currently not compatible with Firefox."); + return; + } }); }]; diff --git a/static/js/controllers/usersettingscontroller.js b/static/js/controllers/usersettingscontroller.js index 41756b20..5ac12ca2 100644 --- a/static/js/controllers/usersettingscontroller.js +++ b/static/js/controllers/usersettingscontroller.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/audiolevel.js b/static/js/directives/audiolevel.js index bfcf22cd..7d6a19c2 100644 --- a/static/js/directives/audiolevel.js +++ b/static/js/directives/audiolevel.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -22,9 +22,7 @@ "use strict"; define(['jquery', 'underscore'], function($, _) { - return ["$window", "mediaStream", "safeApply", "animationFrame", function($window, mediaStream, safeApply, animationFrame) { - - var webrtc = mediaStream.webrtc; + return ["$window", "webrtc", "safeApply", "animationFrame", function($window, webrtc, safeApply, animationFrame) { // Consider anyting lower than this % as no audio. var threshhold = 5; @@ -37,6 +35,13 @@ define(['jquery', 'underscore'], function($, _) { // Talking status history map. var talkingStatus = {}; + // Usermedia reference. + var usermedia = null; + webrtc.e.on("usermedia", function(event, um) { + console.log("Audio level user media changed", um); + usermedia = um; + }); + var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { $scope.talking = false; @@ -47,8 +52,8 @@ define(['jquery', 'underscore'], function($, _) { var width = 0; this.update = _.bind(function() { if (this.active || width > 0) { - if (webrtc.usermedia.audioLevel) { - width = Math.round(100 * webrtc.usermedia.audioLevel); + if (usermedia && usermedia.audioLevel) { + width = Math.round(100 * usermedia.audioLevel); // Hide low volumes. if (width < threshhold) { width = 0; @@ -68,8 +73,8 @@ define(['jquery', 'underscore'], function($, _) { this.meter = _.bind(function() { var talking; - if (this.active) { - var level = Math.round(100 * webrtc.usermedia.audioLevel); + if (this.active && usermedia) { + var level = Math.round(100 * usermedia.audioLevel); if (level < threshhold) { level = 0; } else { diff --git a/static/js/directives/audiovideo.js b/static/js/directives/audiovideo.js index f931acc4..f5b67c93 100644 --- a/static/js/directives/audiovideo.js +++ b/static/js/directives/audiovideo.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -22,7 +22,7 @@ "use strict"; define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) { - return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", "animationFrame", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout, animationFrame) { + return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", "animationFrame", "$timeout", "dummyStream", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout, animationFrame, $timeout, DummyStream) { var peerTemplate = $compile(templatePeer); @@ -33,19 +33,15 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ var getStreamId = function(stream, currentcall) { var id = currentcall.id + "-" + stream.id; - console.log("Created stream ID", id); + //console.log("Created stream ID", id); return id; }; - // Dummy stream. - var dummy = { - id: "defaultDummyStream" - }; - $scope.container = $element[0]; $scope.layoutparent = $element.parent(); $scope.remoteVideos = $element.find(".remoteVideos")[0]; + $scope.localVideos = $element.find(".localVideos")[0]; $scope.localVideo = $element.find(".localVideo")[0]; $scope.miniVideo = $element.find(".miniVideo")[0]; $scope.mini = $element.find(".miniContainer")[0]; @@ -63,33 +59,50 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ $scope.addRemoteStream = function(stream, currentcall) { var id = getStreamId(stream, currentcall); + console.log("New stream", id); if (streams.hasOwnProperty(id)) { - console.warn("Cowardly refusing to add stream id twice", id, currentcall); + console.warn("Cowardly refusing to add stream id twice", id); return; } + var callscope; var subscope; - - // Dummy replacement support. if (calls.hasOwnProperty(currentcall.id)) { - subscope = calls[currentcall.id]; - if (stream === dummy) { + //console.log("xxx has call", id, currentcall.id); + if (DummyStream.is(stream)) { return; } - if (subscope.dummy) { - subscope.$apply(function() { - subscope.attachStream(stream); - }); + callscope = calls[currentcall.id]; + if (callscope.dummy) { + // Current call is marked as dummy. Use it directly. + var dummyId = getStreamId(callscope.dummy, currentcall); + subscope = streams[dummyId]; + if (subscope) { + subscope.dummy = null; + delete streams[dummyId]; + streams[id] = subscope; + safeApply(subscope, function(scope) { + console.log("Replacing dummy with stream", id); + scope.attachStream(stream); + }); + } else { + console.warn("Scope marked as dummy but target stream not found", dummyId); + } return; } } else { + //console.log("xxx create call scope", currentcall.id, id); // Create scope. - subscope = $scope.$new(); - calls[currentcall.id] = subscope; + callscope = $scope.$new(); + calls[currentcall.id] = callscope; + callscope.streams = 0; + console.log("Created call scope", id); } - //console.log("Add remote stream to scope", stream.id, stream, currentcall); + // Create scope for this stream. + subscope = callscope.$new(); + callscope.streams++; var peerid = subscope.peerid = currentcall.id; buddyData.push(peerid); subscope.unattached = true; @@ -100,57 +113,76 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ console.log("Stream scope is now active", id, peerid); }); subscope.$on("$destroy", function() { + if (subscope.destroyed) { + return; + } console.log("Destroyed scope for stream", id, peerid); subscope.destroyed = true; + callscope.streams--; + if (callscope.streams < 1) { + callscope.$destroy(); + delete calls[peerid]; + console.log("Destroyed scope for call", peerid, id); + } }); - console.log("Created stream scope", id, peerid); + console.log("Created stream scope", id); - // Add created scope. - if (stream === dummy) { - subscope.dummy = true; + // If stream is a dummy, mark us in callscope. + if (DummyStream.is(stream)) { + callscope.dummy = stream; } + + // Add created scope. streams[id] = subscope; // Render template. peerTemplate(subscope, function(clonedElement, scope) { - $($scope.remoteVideos).append(clonedElement); clonedElement.data("peerid", scope.peerid); scope.element = clonedElement; scope.attachStream = function(stream) { - if (stream === dummy) { + if (DummyStream.is(stream)) { + scope.withvideo = false; + scope.onlyaudio = true; + $timeout(function() { + scope.$emit("active", currentcall); + $scope.redraw(); + }); return; + } else { + var video = clonedElement.find("video")[0]; + $window.attachMediaStream(video, stream); + // Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly. + videoWaiter.wait(video, stream, function(withvideo) { + if (scope.destroyed) { + console.log("Abort wait for video on destroyed scope."); + return; + } + if (withvideo) { + scope.$apply(function($scope) { + $scope.withvideo = true; + $scope.onlyaudio = false; + }); + } else { + console.info("Incoming stream has no video tracks."); + scope.$apply(function($scope) { + $scope.withvideo = false; + $scope.onlyaudio = true; + }); + } + scope.$emit("active", currentcall); + $scope.redraw(); + }, function() { + if (scope.destroyed) { + console.log("No longer wait for video on destroyed scope."); + return; + } + console.warn("We did not receive video data for remote stream", currentcall, stream, video); + scope.$emit("active", currentcall); + $scope.redraw(); + }); + scope.dummy = null; } - var video = clonedElement.find("video")[0]; - $window.attachMediaStream(video, stream); - // Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly. - videoWaiter.wait(video, stream, function(withvideo) { - if (scope.destroyed) { - console.log("Abort wait for video on destroyed scope."); - return; - } - if (withvideo) { - scope.$apply(function($scope) { - $scope.withvideo = true; - }); - } else { - console.info("Incoming stream has no video tracks."); - scope.$apply(function($scope) { - $scope.onlyaudio = true; - }); - } - scope.$emit("active", currentcall); - $scope.redraw(); - }, function() { - if (scope.destroyed) { - console.log("No longer wait for video on destroyed scope."); - return; - } - console.warn("We did not receive video data for remote stream", currentcall, stream, video); - scope.$emit("active", currentcall); - $scope.redraw(); - }); scope.unattached = false; - scope.dummy = false; }; scope.doChat = function() { $scope.$emit("startchat", currentcall.id, { @@ -159,27 +191,23 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ }); }; scope.attachStream(stream); + $($scope.remoteVideos).append(clonedElement); }); }; $scope.removeRemoteStream = function(stream, currentcall) { - //console.log("remove stream", stream, stream.id, currentcall); var id = getStreamId(stream, currentcall); + console.log("Stream removed", id); var subscope = streams[id]; if (subscope) { buddyData.pop(currentcall.id); delete streams[id]; - //console.log("remove scope", subscope); if (subscope.element) { subscope.element.remove(); } - var callscope = calls[currentcall.id]; - if (subscope === callscope) { - delete calls[currentcall.id]; - } subscope.$destroy(); $scope.redraw(); } @@ -202,6 +230,7 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ $element.addClass("active"); //console.log("active 3"); _.delay(function() { + $scope.localVideos.style.opacity = 0; $scope.localVideo.style.opacity = 0; $scope.localVideo.src = ""; }, 500); @@ -228,6 +257,10 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ mediaStream.webrtc.e.on("usermedia", function(event, usermedia) { + if (!usermedia || !usermedia.started) { + return; + } + //console.log("XXXX XXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia); if ($scope.haveStreams) { @@ -244,6 +277,7 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ return; } if ($scope.localVideo.videoWidth > 0) { + console.log("Local video size: ", $scope.localVideo.videoWidth, $scope.localVideo.videoHeight); $scope.localVideo.style.opacity = 1; $scope.redraw(); } else { @@ -279,6 +313,7 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ $($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; $element.removeClass('active'); @@ -300,6 +335,10 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ $window.reattachMediaStream($scope.miniVideo, $scope.localVideo); $scope.haveStreams = true; } + if (stream === null) { + // Inject dummy stream. + stream = new DummyStream(); + } $scope.addRemoteStream(stream, currentcall); }); @@ -323,7 +362,7 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ case "connected": case "completed": case "failed": - $scope.addRemoteStream(dummy, currentcall); + $scope.addRemoteStream(new DummyStream(), currentcall); break; } @@ -378,7 +417,14 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ width: scope.layoutparent.width(), height: scope.layoutparent.height() } - var again = videoLayout.update(getRendererName(), size, scope, controller); + var name; + if (size.width < 1 || size.height < 1) { + // Use invisible renderer when no size available. + name = "invisible"; + } else { + name = getRendererName(); + } + var again = videoLayout.update(name, size, scope, controller); if (again) { // Layout needs a redraw. needsRedraw = true; diff --git a/static/js/directives/bfi.js b/static/js/directives/bfi.js index af445851..e141fbc2 100644 --- a/static/js/directives/bfi.js +++ b/static/js/directives/bfi.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/buddylist.js b/static/js/directives/buddylist.js index 69c428dd..38efcc17 100644 --- a/static/js/directives/buddylist.js +++ b/static/js/directives/buddylist.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/buddypicturecapture.js b/static/js/directives/buddypicturecapture.js index 7d09ccce..32e3625f 100644 --- a/static/js/directives/buddypicturecapture.js +++ b/static/js/directives/buddypicturecapture.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -25,7 +25,7 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct // buddyPictureCapture return ["$compile", "$window", function($compile, $window) { - var controller = ['$scope', 'safeApply', '$timeout', '$q', function($scope, safeApply, $timeout, $q) { + var controller = ['$scope', 'safeApply', '$timeout', '$q', "mediaDevices", function($scope, safeApply, $timeout, $q, mediaDevices) { // Buddy picutre capture size. $scope.captureSize = { @@ -126,16 +126,16 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct }] }; } - $window.getUserMedia({ + mediaDevices.getUserMedia({ video: videoConstraints - }, function(stream) { + }).then(function(stream) { $scope.showTakePicture = true; localStream = stream; $scope.waitingForPermission = false; $window.attachMediaStream($scope.video, stream); safeApply($scope); videoAllowed.resolve(true); - }, function(error) { + }).catch(function(error) { console.error('Failed to get access to local media. Error code was ' + error.code); $scope.waitingForPermission = false; safeApply($scope); diff --git a/static/js/directives/buddypictureupload.js b/static/js/directives/buddypictureupload.js index 744df141..65533ace 100644 --- a/static/js/directives/buddypictureupload.js +++ b/static/js/directives/buddypictureupload.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/chat.js b/static/js/directives/chat.js index 1980d409..2c9bf7b4 100644 --- a/static/js/directives/chat.js +++ b/static/js/directives/chat.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -246,7 +246,6 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro subscope.sendChat = function(to, message, status, mid, noloop) { //console.log("send chat", to, scope.peer); if (message && message.length > maxMessageSize) { - console.log("XXXXXXX", message.length); return mid; } var peercall = mediaStream.webrtc.findTargetCall(to); diff --git a/static/js/directives/contactrequest.js b/static/js/directives/contactrequest.js index d59ea8c4..ce991451 100644 --- a/static/js/directives/contactrequest.js +++ b/static/js/directives/contactrequest.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/defaultdialog.js b/static/js/directives/defaultdialog.js index 14edda42..e15b0a89 100644 --- a/static/js/directives/defaultdialog.js +++ b/static/js/directives/defaultdialog.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/directives.js b/static/js/directives/directives.js index b0a110ec..5c60f9ec 100644 --- a/static/js/directives/directives.js +++ b/static/js/directives/directives.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -48,7 +48,8 @@ define([ 'directives/bfi', 'directives/title', 'directives/welcome', - 'directives/menu'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi, title, welcome, menu) { + 'directives/menu', + 'directives/ui'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi, title, welcome, menu, ui) { var directives = { onEnter: onEnter, @@ -76,7 +77,8 @@ define([ bfi: bfi, title: title, welcome: welcome, - menu: menu + menu: menu, + ui: ui }; var initialize = function(angModule) { diff --git a/static/js/directives/fileinfo.js b/static/js/directives/fileinfo.js index 43107a2a..cd8b4e99 100644 --- a/static/js/directives/fileinfo.js +++ b/static/js/directives/fileinfo.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/odfcanvas.js b/static/js/directives/odfcanvas.js index 7aa980fb..ba96d944 100644 --- a/static/js/directives/odfcanvas.js +++ b/static/js/directives/odfcanvas.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,109 +20,101 @@ */ "use strict"; -define(['require', 'underscore', 'jquery'], function(require, _, $) { +define(['require', 'underscore', 'jquery', 'text!partials/odfcanvas_sandbox.html'], function(require, _, $, sandboxTemplate) { - return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) { - - var webodf = null; + return ["$window", "$compile", "$http", "translation", "safeApply", "restURL", "sandbox", function($window, $compile, $http, translation, safeApply, restURL, sandbox) { var DOCUMENT_TYPE_PRESENTATION = "presentation"; var DOCUMENT_TYPE_SPREADSHEET = "spreadsheet"; var DOCUMENT_TYPE_TEXT = "text"; - var nsResolver = function(prefix) { - var ns = { - 'draw': "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", - 'presentation': "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0", - 'text': "urn:oasis:names:tc:opendocument:xmlns:text:1.0", - 'office': "urn:oasis:names:tc:opendocument:xmlns:office:1.0" - }; - return ns[prefix] || console.log('prefix [' + prefix + '] unknown.'); - } - - var ODFCanvas_readFile = function(path, encoding, callback) { - if (typeof path === "string") { - webodf.runtime.orig_readFile.call(webodf.runtime, path, encoding, callback); - return; - } - - var fp = path.file || path; - if (typeof URL !== "undefined" && URL.createObjectURL) { - var url = URL.createObjectURL(fp); - webodf.runtime.orig_readFile.call(webodf.runtime, url, encoding, function() { - URL.revokeObjectURL(url); - callback.apply(callback, arguments); - }); - return; - } - - console.error("TODO(fancycode): implement readFile for", path); - }; - - var ODFCanvas_loadXML = function(path, callback) { - if (typeof path === "string") { - webodf.runtime.orig_loadXML.call(webodf.runtime, path, callback); - return; - } - - var fp = path.file || path; - if (typeof URL !== "undefined" && URL.createObjectURL) { - var url = URL.createObjectURL(fp); - webodf.runtime.orig_loadXML.call(webodf.runtime, url, function() { - URL.revokeObjectURL(url); - callback.apply(callback, arguments); - }); - return; - } + var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { - console.error("TODO(fancycode): implement loadXML for", path); - }; + var container = $($element); - var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { + var odfCanvas; + + var template = sandboxTemplate; + template = template.replace(/__PARENT_ORIGIN__/g, $window.location.protocol + "//" + $window.location.host); + template = template.replace(/__WEBODF_SANDBOX_JS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('sandboxes/webodf') + ".js")); + template = template.replace(/__WEBODF_URL__/g, restURL.createAbsoluteUrl(require.toUrl('webodf') + ".js")); + var sandboxApi = sandbox.createSandbox($("iframe", container)[0], template); + + sandboxApi.e.on("message", function(event, message) { + var msg = message.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "ready": + break; + case "webodf.loading": + $scope.$apply(function(scope) { + scope.$emit("presentationLoading", data.source); + container.hide(); + }); + break; + case "webodf.loaded": + odfCanvas._odfLoaded(data.url, data.type, data.numPages); + break; + case "webodf.keyUp": + $scope.$apply(function(scope) { + scope.$emit("keyUp", data.key); + }); + break; + default: + console.log("Unknown message received", message); + break; + } + }); - var ODFCanvas = function(scope, container, canvasDom) { + var ODFCanvas = function(scope, container) { this.scope = scope; this.container = container; - this.canvasDom = canvasDom; - this.canvas = null; + this.doc = null; this.maxPageNumber = -1; this.currentPageNumber = -1; this.pendingPageNumber = null; }; - ODFCanvas.prototype._close = function() { - if (this.canvas) { - this.canvas.destroy(function() { - // ignore callback - }); - this.canvas = null; - } + ODFCanvas.prototype.close = function() { + sandboxApi.postMessage("closeFile", {"close": true}); this.maxPageNumber = -1; this.currentPageNumber = -1; this.pendingPageNumber = null; - }; - - ODFCanvas.prototype.close = function() { - this._close(); + this.doc = null; }; ODFCanvas.prototype.open = function(presentation) { this.scope.$emit("presentationOpening", presentation); presentation.open(_.bind(function(source) { console.log("Loading ODF from", source); - this._openFile(source); + this.close(); + if (typeof source === "string") { + // got a url + this._openFile(source); + return; + } + + var fp = source.file || source; + if (typeof URL !== "undefined" && URL.createObjectURL) { + this.url = URL.createObjectURL(fp); + this._openFile(this.url); + } else { + var fileReader = new FileReader(); + fileReader.onload = _.bind(function(evt) { + var buffer = evt.target.result; + var uint8Array = new Uint8Array(buffer); + this._openFile(uint8Array); + }, this); + fileReader.readAsArrayBuffer(fp); + } }, this)); }; - ODFCanvas.prototype._odfLoaded = function() { + ODFCanvas.prototype._odfLoaded = function(url, document_type, numPages) { this.scope.$apply(_.bind(function(scope) { - var odfcontainer = this.canvas.odfContainer(); - this.document_type = odfcontainer.getDocumentType(); - // pages only supported for presentations - var pages = []; - switch (this.document_type) { + this.document_type = document_type; + switch (document_type) { case DOCUMENT_TYPE_PRESENTATION: - pages = odfcontainer.rootElement.getElementsByTagNameNS(nsResolver('draw'), 'page'); this.container.addClass("showonepage"); break; @@ -131,13 +123,12 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { break; } - this.maxPageNumber = Math.max(1, pages.length); + this.maxPageNumber = numPages; this.currentPageNumber = -1; - console.log("ODF loaded", odfcontainer); - var odfDoc = { + this.doc = { numPages: this.maxPageNumber }; - scope.$emit("presentationLoaded", odfcontainer.getUrl(), odfDoc); + scope.$emit("presentationLoaded", url, this.doc); if (this.pendingPageNumber !== null) { this._showPage(this.pendingPageNumber); this.pendingPageNumber = null; @@ -145,41 +136,19 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { }, this)); }; - ODFCanvas.prototype._doOpenFile = function(source) { - this.scope.$emit("presentationLoading", source); - this.container.hide(); - if (!this.canvas) { - this.canvas = new webodf.odf.OdfCanvas(this.canvasDom[0]); - this.canvas.addListener("statereadychange", _.bind(function() { - this._odfLoaded(); - }, this)); - } - - this.canvas.setZoomLevel(1); - this.canvas.load(source); - }; - ODFCanvas.prototype._openFile = function(source) { - if (webodf === null) { - // load webodf.js lazily - require(['webodf'], _.bind(function(webodf_) { - console.log("Using webodf.js " + webodf_.webodf.Version); - - webodf = webodf_; - - // monkey-patch IO functions - webodf.runtime.orig_readFile = webodf.runtime.readFile; - webodf.runtime.readFile = ODFCanvas_readFile; - webodf.runtime.orig_loadXML = webodf.runtime.loadXML; - webodf.runtime.loadXML = ODFCanvas_loadXML; - - this.scope.$apply(_.bind(function(scope) { - this._doOpenFile(source); - }, this)); + if (typeof(source) === "string") { + // we can't load urls from inside the sandbox, do so here and transmit the contents + $http.get(source, { + responseType: "arraybuffer" + }).then(_.bind(function(response) { + this._openFile(response.data); }, this)); - } else { - this._doOpenFile(source); + return; } + + console.log("Opening file", source); + sandboxApi.postMessage("openFile", {"source": source}); }; ODFCanvas.prototype._showPage = function(page) { @@ -200,7 +169,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { this.redrawPage(); } this.currentPageNumber = page; - this.canvas.showPage(page); + sandboxApi.postMessage("showPage", {"page": page}); this.scope.$emit("presentationPageRendering", page); this.scope.$emit("presentationPageRendered", page, this.maxPageNumber); }, this)); @@ -208,22 +177,12 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { }; ODFCanvas.prototype.redrawPage = function() { - if (this.canvas) { - switch (this.document_type) { - case DOCUMENT_TYPE_PRESENTATION: - this.canvas.fitToContainingElement(this.container.width(), this.container.height()); - break; - - default: - this.canvas.fitToWidth(this.container.width()); - break; - } - } + sandboxApi.postMessage("redrawPage", {"redraw": true}); }; ODFCanvas.prototype.showPage = function(page) { if (page >= 1 && page <= this.maxPageNumber) { - if (!this.canvas) { + if (!this.doc) { this.pendingPageNumber = page; } else { this._showPage(page); @@ -231,9 +190,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { } }; - var container = $($element); - var canvas = $($element).find(".odfcanvas"); - var odfCanvas = new ODFCanvas($scope, container, canvas); + odfCanvas = new ODFCanvas($scope, container); $scope.$watch("currentPresentation", function(presentation, previousPresentation) { if (presentation) { @@ -251,6 +208,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { $scope.$on("$destroy", function() { odfCanvas.close(); odfCanvas = null; + sandboxApi.destroy(); }); $scope.$watch("currentPageNumber", function(page, oldValue) { @@ -273,7 +231,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { return { restrict: 'E', replace: true, - template: '
', + template: '
', controller: controller }; diff --git a/static/js/directives/onenter.js b/static/js/directives/onenter.js index 29272ce9..c86a10bb 100644 --- a/static/js/directives/onenter.js +++ b/static/js/directives/onenter.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/onescape.js b/static/js/directives/onescape.js index 3396a11f..ce19035e 100644 --- a/static/js/directives/onescape.js +++ b/static/js/directives/onescape.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/page.js b/static/js/directives/page.js index fd279d70..a03dd982 100644 --- a/static/js/directives/page.js +++ b/static/js/directives/page.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/pdfcanvas.js b/static/js/directives/pdfcanvas.js index 3ce31134..e4327664 100644 --- a/static/js/directives/pdfcanvas.js +++ b/static/js/directives/pdfcanvas.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,9 +20,9 @@ */ "use strict"; -define(['require', 'underscore', 'jquery'], function(require, _, $) { +define(['require', 'underscore', 'jquery', 'text!partials/pdfcanvas_sandbox.html'], function(require, _, $, sandboxTemplate) { - return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) { + return ["$window", "$compile", "$http", "translation", "safeApply", 'restURL', 'sandbox', function($window, $compile, $http, translation, safeApply, restURL, sandbox) { var pdfjs = null; @@ -30,28 +30,71 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { var container = $($element); - var PDFCanvas = function(scope, canvases) { + var pdfCanvas; + + var template = sandboxTemplate; + template = template.replace(/__PARENT_ORIGIN__/g, $window.location.protocol + "//" + $window.location.host); + template = template.replace(/__PDFJS_SANDBOX_JS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('sandboxes/pdf') + ".js")); + template = template.replace(/__PDFJS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('pdf') + ".js")); + template = template.replace(/__PDFJS_WORKER_URL__/g, restURL.createAbsoluteUrl(require.toUrl('pdf.worker') + ".js")); + template = template.replace(/__PDFJS_COMPATIBILITY_URL__/g, restURL.createAbsoluteUrl(require.toUrl('libs/pdf/compatibility') + ".js")); + var sandboxApi = sandbox.createSandbox($("iframe", container)[0], template); + + sandboxApi.e.on("message", function(event, message) { + var msg = message.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "ready": + break; + case "pdfjs.loading": + $scope.$apply(function(scope) { + scope.$emit("presentationLoading", data.source); + }); + break; + case "pdfjs.loaded": + pdfCanvas._pdfLoaded(data.source, data.doc); + break; + case "pdfjs.loadError": + pdfCanvas._pdfLoadError(data.source, data.error); + break; + case "pdfjs.pageLoaded": + pdfCanvas._pageLoaded(data.page); + break; + case "pdfjs.pageLoadError": + pdfCanvas._pageLoadError(data.page, data.error); + break; + case "pdfjs.renderingPage": + $scope.$apply(function(scope) { + scope.$emit("presentationPageRendering", data.page); + }); + break; + case "pdfjs.pageRendered": + pdfCanvas._pageRendered(data.page); + break; + case "pdfjs.pageRenderError": + pdfCanvas._pageRenderError(data.page, data.error); + break; + case "pdfjs.keyUp": + $scope.$apply(function(scope) { + scope.$emit("keyUp", data.key); + }); + break; + default: + console.log("Unknown message received", message); + break; + } + }); + + var PDFCanvas = function(scope) { this.scope = scope; - this.canvases = canvases; this.doc = null; - this.currentPage = null; this.currentPageNumber = null; this.pendingPageNumber = null; - this.renderTask = null; this.url = null; }; - PDFCanvas.prototype._close = function() { - this._stopRendering(); - if (this.currentPage) { - this.currentPage.destroy(); - this.currentPage = null; - } - if (this.doc) { - this.doc.cleanup(); - this.doc.destroy(); - this.doc = null; - } + PDFCanvas.prototype.close = function() { + sandboxApi.postMessage("closeFile", {"close": true}); if (this.url) { URL.revokeObjectURL(this.url); this.url = null; @@ -59,20 +102,13 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { this.pendingPageNumber = null; this.currentPageNumber = -1; this.maxPageNumber = -1; - // clear visible canvas so it's empty when we show the next document - var canvas = this.canvases[this.scope.canvasIndex]; - canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); - }; - - PDFCanvas.prototype.close = function() { - this._close(); }; PDFCanvas.prototype.open = function(presentation) { this.scope.$emit("presentationOpening", presentation); presentation.open(_.bind(function(source) { console.log("Loading PDF from", source); - this._close(); + this.close(); if (typeof source === "string") { // got a url this._openFile(source); @@ -100,7 +136,6 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { this.doc = doc; this.maxPageNumber = doc.numPages; this.currentPageNumber = -1; - console.log("PDF loaded", doc); scope.$emit("presentationLoaded", source, doc); }, this)); }; @@ -127,43 +162,30 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { }, this)); }; - PDFCanvas.prototype._doOpenFile = function(source) { - this.scope.$emit("presentationLoading", source); - pdfjs.getDocument(source).then(_.bind(function(doc) { - this._pdfLoaded(source, doc); - }, this), _.bind(function(error, exception) { - this._pdfLoadError(source, error, exception); - }, this)); - }; - PDFCanvas.prototype._openFile = function(source) { - if (pdfjs === null) { - // load pdf.js lazily - require(['pdf'], _.bind(function(pdf) { - pdf.workerSrc = require.toUrl('pdf.worker') + ".js"; - - console.log("Using pdf.js " + pdf.version + " (build " + pdf.build + ")"); - - pdfjs = pdf; - - this._doOpenFile(source); + if (typeof(source) === "string") { + // we can't load urls from inside the sandbox, do so here and transmit the contents + $http.get(source, { + responseType: "arraybuffer" + }).then(_.bind(function(response) { + this._openFile(response.data); + }, this), _.bind(function(error) { + this._pdfLoadError(source, error); }, this)); - } else { - this._doOpenFile(source); + return; } + + console.log("Opening file", source); + sandboxApi.postMessage("openFile", {"source": source}); }; - PDFCanvas.prototype._pageLoaded = function(page, pageObject) { + PDFCanvas.prototype._pageLoaded = function(page) { this.scope.$apply(_.bind(function(scope) { - console.log("Got page", pageObject); - scope.$emit("presentationPageLoaded", page, pageObject); - this.currentPage = pageObject; - this.drawPage(pageObject); + scope.$emit("presentationPageLoaded", page); }, this)); }; - PDFCanvas.prototype._pageLoadError = function(page, error, exception) { - console.error("Could not load page", page, error, exception); + PDFCanvas.prototype._pageLoadError = function(page, error) { var loadErrorMessage; if (error) { loadErrorMessage = translation._("An error occurred while loading the PDF page (%s).", error); @@ -181,36 +203,19 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { } console.log("Showing page", page, "/", this.maxPageNumber); - if (this.currentPage) { - this.currentPage.destroy(); - this.currentPage = null; - } this.currentPageNumber = page; this.scope.$emit("presentationPageLoading", page); - this.doc.getPage(page).then(_.bind(function(pageObject) { - this._pageLoaded(page, pageObject); - }, this), _.bind(function(error, exception) { - this._pageLoadError(page, error, exception); - }, this)); + sandboxApi.postMessage("loadPage", {"page": page}); }; - PDFCanvas.prototype._pageRendered = function(pageObject) { - this.renderTask = null; + PDFCanvas.prototype._pageRendered = function(page) { this.scope.$apply(_.bind(function(scope) { - console.log("Rendered page", pageObject.pageNumber); - this.scope.$emit("presentationPageRendered", pageObject.pageNumber, this.maxPageNumber); - // ...and flip the buffers... - scope.canvasIndex = 1 - scope.canvasIndex; + this.scope.$emit("presentationPageRendered", page, this.maxPageNumber); this.showQueuedPage(); }, this)); }; - PDFCanvas.prototype._pageRenderError = function(pageObject, error, exception) { - if (error === "cancelled") { - return; - } - console.error("Could not render page", pageObject, error, exception); - this.renderTask = null; + PDFCanvas.prototype._pageRenderError = function(page, error) { var loadErrorMessage; if (error) { loadErrorMessage = translation._("An error occurred while rendering the PDF page (%s).", error); @@ -218,59 +223,12 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { loadErrorMessage = translation._("An unknown error occurred while rendering the PDF page."); } this.scope.$apply(_.bind(function(scope) { - scope.$emit("presentationPageRenderError", pageObject.pageNumber, this.maxPageNumber, loadErrorMessage); - }, this)); - }; - - PDFCanvas.prototype._stopRendering = function() { - if (this.renderTask) { - if (this.renderTask.internalRenderTask && this.renderTask.internalRenderTask.cancel) { - this.renderTask.internalRenderTask.cancel(); - } - this.renderTask = null; - } - } - - PDFCanvas.prototype.drawPage = function(pageObject) { - var pdfView = pageObject.view; - var pdfWidth = pdfView[2] - pdfView[0]; - var pdfHeight = pdfView[3] - pdfView[1]; - var w = container.width(); - var h = container.height(); - var scale = w / pdfWidth; - if (pdfHeight * scale > h) { - scale = container.height() / pdfHeight; - } - - // use double-buffering to avoid flickering while - // the new page is rendered... - var canvas = this.canvases[1 - this.scope.canvasIndex]; - var viewport = pageObject.getViewport(scale); - canvas.width = Math.round(viewport.width); - canvas.height = Math.round(viewport.height); - var renderContext = { - canvasContext: canvas.getContext("2d"), - viewport: viewport - }; - - console.log("Rendering page", pageObject); - this.scope.$emit("presentationPageRendering", pageObject.pageNumber); - - // TODO(fancycode): also render images in different resolutions for subscribed peers and send to them when ready - this._stopRendering(); - var renderTask = pageObject.render(renderContext); - this.renderTask = renderTask; - renderTask.promise.then(_.bind(function() { - this._pageRendered(pageObject); - }, this), _.bind(function(error, exception) { - this._pageRenderError(pageObject, error, exception); + scope.$emit("presentationPageRenderError", page, this.maxPageNumber, loadErrorMessage); }, this)); }; PDFCanvas.prototype.redrawPage = function() { - if (this.currentPage !== null) { - this.drawPage(this.currentPage); - } + sandboxApi.postMessage("redrawPage", {"redraw": true}); }; PDFCanvas.prototype.showPage = function(page) { @@ -290,10 +248,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { } }; - $scope.canvasIndex = 0; - - var canvases = container.find("canvas"); - var pdfCanvas = new PDFCanvas($scope, canvases); + pdfCanvas = new PDFCanvas($scope); $scope.$watch("currentPresentation", function(presentation, previousPresentation) { if (presentation) { @@ -311,6 +266,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { $scope.$on("$destroy", function() { pdfCanvas.close(); pdfCanvas = null; + sandboxApi.destroy(); }); $scope.$watch("currentPageNumber", function(page, oldValue) { @@ -333,7 +289,7 @@ define(['require', 'underscore', 'jquery'], function(require, _, $) { return { restrict: 'E', replace: true, - template: '
', + template: '
', controller: controller }; diff --git a/static/js/directives/presentation.js b/static/js/directives/presentation.js index 4737202a..bc01604d 100644 --- a/static/js/directives/presentation.js +++ b/static/js/directives/presentation.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -715,6 +715,21 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], }); }); + $scope.$on("keyUp", function(event, keyCode) { + switch (keyCode) { + case 37: + // left arrow + $scope.prevPage(); + break; + case 39: + // right arrow + case 32: + // space + $scope.nextPage(); + break; + } + }) + $(document).on("keyup", function(event) { if (!$scope.layout.presentation) { return; @@ -722,22 +737,10 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], if ($(event.target).is("input,textarea,select")) { return; } - $scope.$apply(function() { - switch (event.keyCode) { - case 37: - // left arrow - $scope.prevPage(); - event.preventDefault(); - break; - case 39: - // right arrow - case 32: - // space - $scope.nextPage(); - event.preventDefault(); - break; - } + $scope.$apply(function(scope) { + scope.$emit("keyUp", event.keyCode); }); + event.preventDefault(); }); $scope.$watch("layout.presentation", function(newval, oldval) { diff --git a/static/js/directives/roombar.js b/static/js/directives/roombar.js index 0573e23e..c6b8817b 100644 --- a/static/js/directives/roombar.js +++ b/static/js/directives/roombar.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -32,15 +32,17 @@ define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angu $scope.newRoomName = ""; }; - //console.log("roomBar directive link", arguments); - //$scope.layout.roombar = true; - $scope.save = function() { if ($scope.roombarform.$invalid) { return; } var roomName = rooms.joinByName($scope.newRoomName); if (roomName !== $scope.currentRoomName) { + // Room name accepted. + $scope.roombarform.$setPristine(); + } else { + // Room name did not apply. Reset new name and form. + $scope.newRoomName = roomName; $scope.roombarform.$setPristine(); } }; diff --git a/static/js/directives/screenshare.js b/static/js/directives/screenshare.js index 4b6a598e..0e6c0726 100644 --- a/static/js/directives/screenshare.js +++ b/static/js/directives/screenshare.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/settings.js b/static/js/directives/settings.js index 26d2cb0e..f145b6de 100644 --- a/static/js/directives/settings.js +++ b/static/js/directives/settings.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -171,29 +171,29 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t $timeout($scope.maybeShowSettings); }); - constraints.e.on("refresh", function(event, constraints) { + constraints.e.on("refresh", function(event, c) { var settings = $scope.master.settings; // Assert that selected devices are there. (function() { - var deferred = constraints.defer(); + var deferred = c.defer(); mediaSources.refresh(function() { $scope.checkDefaultMediaSources(); // Select microphone device by id. if (settings.microphoneId) { - constraints.add("audio", "sourceId", settings.microphoneId); + c.add("audio", "sourceId", settings.microphoneId); } // Select camera by device id. if (settings.cameraId) { - constraints.add("video", "sourceId", settings.cameraId); + c.add("video", "sourceId", settings.cameraId); } if (!mediaSources.hasAudio()) { - constraints.disable('audio'); + c.disable('audio'); console.info("Disabled audio input as no audio source was found."); } if (!mediaSources.hasVideo()) { - constraints.disable('video'); + c.disable('video'); console.info("Disabled video input as no video source was found."); } deferred.resolve("complete"); @@ -201,7 +201,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t })(); // Chrome only constraints. - if ($scope.isChrome) { + if (constraints.supported.chrome) { // Chrome specific constraints overview: // https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/mediaconstraintsinterface.cc @@ -211,24 +211,24 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t // Experimental audio settings. if (settings.experimental.enabled) { - constraints.add("audio", "googEchoCancellation", true); // defaults to true - constraints.add("audio", "googEchoCancellation2", settings.experimental.audioEchoCancellation2 && true); // defaults to false in Chrome - constraints.add("audio", "googAutoGainControl", true); // defaults to true - constraints.add("audio", "googAutoGainControl2", settings.experimental.audioAutoGainControl2 && true); // defaults to false in Chrome - constraints.add("audio", "googNoiseSuppression", true); // defaults to true - constraints.add("audio", "googgNoiseSuppression2", settings.experimental.audioNoiseSuppression2 && true); // defaults to false in Chrome - constraints.add("audio", "googHighpassFilter", true); // defaults to true - constraints.add("audio", "googTypingNoiseDetection", settings.experimental.audioTypingNoiseDetection && true); // defaults to true in Chrome + c.add("audio", "googEchoCancellation", true); // defaults to true + c.add("audio", "googEchoCancellation2", settings.experimental.audioEchoCancellation2 && true); // defaults to false in Chrome + c.add("audio", "googAutoGainControl", true); // defaults to true + c.add("audio", "googAutoGainControl2", settings.experimental.audioAutoGainControl2 && true); // defaults to false in Chrome + c.add("audio", "googNoiseSuppression", true); // defaults to true + c.add("audio", "googgNoiseSuppression2", settings.experimental.audioNoiseSuppression2 && true); // defaults to false in Chrome + c.add("audio", "googHighpassFilter", true); // defaults to true + c.add("audio", "googTypingNoiseDetection", settings.experimental.audioTypingNoiseDetection && true); // defaults to true in Chrome } - if ($scope.supported.renderToAssociatedSink) { + if (constraints.supported.renderToAssociatedSink) { // When true uses the default communications device on Windows. // https://codereview.chromium.org/155863003 - constraints.add("audio", "googDucking", true); // defaults to true on Windows. + c.add("audio", "googDucking", true); // defaults to true on Windows. // Chrome will start rendering mediastream output to an output device that's associated with // the input stream that was opened via getUserMedia. // https://chromiumcodereview.appspot.com/23558010 - constraints.add("audio", "chromeRenderToAssociatedSink", settings.audioRenderToAssociatedSkin && true); // defaults to false in Chrome + c.add("audio", "chromeRenderToAssociatedSink", settings.audioRenderToAssociatedSkin && true); // defaults to false in Chrome } // Experimental video settings. @@ -236,44 +236,44 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t // Changes the way the video encoding adapts to the available bandwidth. // https://code.google.com/p/webrtc/issues/detail?id=3351 - constraints.add(["video", "screensharing"], "googLeakyBucket", settings.experimental.videoLeakyBucket && true); // defaults to false in Chrome + c.add(["video", "screensharing"], "googLeakyBucket", settings.experimental.videoLeakyBucket && true); // defaults to false in Chrome // Removes the noise in the captured video stream at the expense of CPU. - constraints.add(["video", "screensharing"], "googNoiseReduction", settings.experimental.videoNoiseReduction && true); // defaults to false in Chrome - constraints.add("pc", "googCpuOveruseDetection", settings.experimental.videoCpuOveruseDetection && true); // defaults to true in Chrome + c.add(["video", "screensharing"], "googNoiseReduction", settings.experimental.videoNoiseReduction && true); // defaults to false in Chrome + c.add("pc", "googCpuOveruseDetection", settings.experimental.videoCpuOveruseDetection && true); // defaults to true in Chrome } + } + + if (constraints.supported.audioVideo) { + // Set video quality. var videoQuality = videoQualityMap[settings.videoQuality]; if (videoQuality) { var mandatory = videoQuality.mandatory; _.forEach(videoQuality, function(v, k) { if (k !== "mandatory") { - constraints.add("video", k, v, mandatory ? false : true); + c.add("video", k, v, mandatory ? false : true); } }); if (mandatory) { _.forEach(mandatory, function(v, k) { - constraints.add("video", k, v, true); + c.add("video", k, v, true); }); } } // Set max frame rate if any was selected. if (settings.maxFrameRate && settings.maxFrameRate != "auto") { - constraints.add("video", "maxFrameRate", parseInt(settings.maxFrameRate, 10), true); + c.add("video", "maxFrameRate", parseInt(settings.maxFrameRate, 10), true); } // Disable AEC if stereo. // https://github.com/webrtc/apprtc/issues/23 if (settings.sendStereo) { - constraints.add("audio", "echoCancellation", false); + c.add("audio", "echoCancellation", false); } - } else { - - // Other browsers constraints (there are none as of now.); - } }); diff --git a/static/js/directives/socialshare.js b/static/js/directives/socialshare.js index bd9292b8..a0576251 100644 --- a/static/js/directives/socialshare.js +++ b/static/js/directives/socialshare.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/statusmessage.js b/static/js/directives/statusmessage.js index ed45ee75..da50591d 100644 --- a/static/js/directives/statusmessage.js +++ b/static/js/directives/statusmessage.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/title.js b/static/js/directives/title.js index a62e5c16..a30a4750 100644 --- a/static/js/directives/title.js +++ b/static/js/directives/title.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/ui.js b/static/js/directives/ui.js new file mode 100644 index 00000000..53e95b5d --- /dev/null +++ b/static/js/directives/ui.js @@ -0,0 +1,38 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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(['text!partials/ui.html'], function(template) { + + // ui + return [function() { + + return { + restrict: 'E', + replace: true, + scope: false, + controller: 'UiController', + template: template + } + + }]; + +}); diff --git a/static/js/directives/usability.js b/static/js/directives/usability.js index ce6bd434..260a57ac 100644 --- a/static/js/directives/usability.js +++ b/static/js/directives/usability.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/welcome.js b/static/js/directives/welcome.js index 801827c9..117ac279 100644 --- a/static/js/directives/welcome.js +++ b/static/js/directives/welcome.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/directives/youtubevideo.js b/static/js/directives/youtubevideo.js index 2264a74a..98806f1a 100644 --- a/static/js/directives/youtubevideo.js +++ b/static/js/directives/youtubevideo.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,20 +20,81 @@ */ "use strict"; -define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], function($, _, template, BigScreen) { +define(['require', 'jquery', 'underscore', 'moment', 'text!partials/youtubevideo.html', 'text!partials/youtubevideo_sandbox.html', 'bigscreen'], function(require, $, _, moment, template, sandboxTemplate, BigScreen) { - return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", "$q", function($window, $document, mediaStream, alertify, translation, safeApply, appData, $q) { + return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", "$q", "restURL", "sandbox", function($window, $document, mediaStream, alertify, translation, safeApply, appData, $q, restURL, sandbox) { var YOUTUBE_IFRAME_API_URL = "//www.youtube.com/iframe_api"; - var isYouTubeIframeAPIReady = (function() { - var d = $q.defer(); - $window.onYouTubeIframeAPIReady = function() { - console.log("YouTube IFrame ready"); - d.resolve(); - }; - return d.promise; - })(); + var isYouTubeIframeAPIReadyDefer = $q.defer(); + var isYouTubeIframeAPIReady = isYouTubeIframeAPIReadyDefer.promise; + + var SandboxPlayer = function(sandbox, params) { + this.sandbox = sandbox; + this.state = -1; + this.position = 0; + this.lastPositionUpdate = null; + this.sandbox.postMessage("loadPlayer", params); + }; + + SandboxPlayer.prototype.destroy = function() { + this.sandbox.postMessage("destroyPlayer", {"destroy": true}); + }; + + SandboxPlayer.prototype.loadVideoById = function(id, position) { + var msg = {"id": id}; + if (typeof(position) !== "undefined") { + msg.position = position; + } + this.sandbox.postMessage("loadVideo", msg); + }; + + SandboxPlayer.prototype.playVideo = function() { + this.sandbox.postMessage("playVideo", {"play": true}); + }; + + SandboxPlayer.prototype.pauseVideo = function() { + this.sandbox.postMessage("pauseVideo", {"pause": true}); + }; + + SandboxPlayer.prototype.stopVideo = function() { + this.sandbox.postMessage("stopVideo", {"stop": true}); + }; + + SandboxPlayer.prototype.seekTo = function(position, allowSeekAhead) { + var msg = {"position": position}; + if (typeof(allowSeekAhead) !== "undefined") { + msg.allowSeekAhead = allowSeekAhead; + } + this.sandbox.postMessage("seekTo", msg); + }; + + SandboxPlayer.prototype.setVolume = function(volume) { + this.sandbox.postMessage("setVolume", {"volume": volume}); + }; + + SandboxPlayer.prototype.setCurrentTime = function(time) { + this.position = time; + this.lastPositionUpdate = moment(); + }; + + SandboxPlayer.prototype.getCurrentTime = function() { + if (!this.lastPositionUpdate) { + return this.position; + } + + var now = moment(); + var deltaTime = now.diff(this.lastPositionUpdate, 'seconds', true); + return this.position + deltaTime; + }; + + SandboxPlayer.prototype.setPlayerState = function(state) { + this.state = state; + }; + + SandboxPlayer.prototype.getPlayerState = function() { + return this.state; + }; var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) { @@ -41,27 +102,84 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], var player = null; var playerReady = null; var isPaused = null; - var seekDetector = null; var playReceivedNow = null; - var prevTime = null; - var prevNow = null; var initialState = null; + var sandboxApi = null; - var stateEvents = { - "-1": "youtube.unstarted", - "0": "youtube.ended", - "1": "youtube.playing", - "2": "youtube.paused", - "3": "youtube.buffering", - "5": "youtube.videocued" - }; - var errorIds = { - "2": "invalidParameter", - "5": "htmlPlayerError", - "100": "videoNotFound", - "101": "notAllowedEmbedded", - "150": "notAllowedEmbedded" - }; + var createSandboxApi = function(force) { + if (sandboxApi && force) { + sandboxApi.destroy(); + sandboxApi = null; + } + if (!sandboxApi) { + var sandboxFrame = $(".youtubeplayer", $element)[0]; + + var template = sandboxTemplate; + template = template.replace(/__PARENT_ORIGIN__/g, $window.location.protocol + "//" + $window.location.host); + template = template.replace(/__YOUTUBE_SANDBOX_JS_URL__/g, restURL.createAbsoluteUrl(require.toUrl('sandboxes/youtube') + ".js")); + sandboxApi = sandbox.createSandbox(sandboxFrame, template); + + sandboxApi.e.on("message", function(event, message) { + var msg = message.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "ready": + sandboxApi.postMessage("loadApi", {"url": $window.location.protocol + YOUTUBE_IFRAME_API_URL}); + break; + case "youtube.apiReady": + $scope.$apply(function() { + console.log("YouTube IFrame ready"); + isYouTubeIframeAPIReadyDefer.resolve(); + }); + break; + case "youtube.error": + $scope.$apply(function(scope) { + console.log("YouTube error", data); + scope.$emit("youtube.error", data.msgid); + }); + break; + case "youtube.playerReady": + $scope.$apply(function() { + playerReady.resolve(); + }); + break; + case "youtube.volume": + $scope.$apply(function(scope) { + scope.volume = data.volume; + }); + break; + case "youtube.event": + $scope.$apply(function(scope) { + console.log("State change", data); + if (player) { + player.setPlayerState(data.state); + } + scope.$emit(data.event, data.position); + }); + break; + case "youtube.position": + if (player) { + player.setCurrentTime(data.position); + } + break; + default: + console.log("Unknown message received", message); + break; + } + }); + } + } + + $scope.$on("$destroy", function() { + if (player) { + player.destroy(); + player = null; + } + if (sandboxApi) { + sandboxApi.destroy(); + sandboxApi = null; + } + }); $scope.isPublisher = null; $scope.playbackActive = false; @@ -79,33 +197,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }); }); - var onPlayerReady = function(event) { - $scope.$apply(function(scope) { - scope.volume = player.getVolume(); - playerReady.resolve(); - }); - }; - - var onPlayerError = function(event) { - var error = errorIds[event.data] || "unknownError"; - $scope.$apply(function(scope) { - scope.$emit("youtube.error", error); - }); - }; - - var onPlayerStateChange = function(event) { - var msg = stateEvents[event.data]; - if (typeof msg === "undefined") { - console.warn("Unknown YouTube player state", event) - return; - } - - $scope.$apply(function(scope) { - console.log("State change", msg, event.target); - scope.$emit(msg, event.target); - }); - }; - var getYouTubeId = function(url) { /* * Supported URLs: @@ -130,79 +221,34 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], return null; } - var startDetectSeek = function() { - var checkSeek = function() { - if (!player) { - return; - } - var now = new Date(); - var time = player.getCurrentTime(); - if (prevTime === null) { - prevTime = time; - } - if (prevNow === null) { - prevNow = now; - } - var deltaTime = Math.abs(time - prevTime); - var deltaNow = (now - prevNow) * 0.001; - if (deltaTime > deltaNow * 1.1) { - safeApply($scope, function(scope) { - scope.$emit("youtube.seeked", time); - }); - } - prevNow = now; - prevTime = time; - }; - - if (!seekDetector) { - seekDetector = $window.setInterval(function() { - checkSeek(); - }, 1000); - } - checkSeek(); - }; - - var stopDetectSeek = function() { - if (seekDetector) { - $window.clearInterval(seekDetector); - seekDetector = null; - } - prevNow = null; - }; - - $scope.$on("youtube.playing", function() { + $scope.$on("youtube.playing", function(event, position) { if (initialState === 2) { initialState = null; player.pauseVideo(); return; } - prevTime = null; - startDetectSeek(); if (isPaused) { isPaused = false; mediaStream.webrtc.callForEachCall(function(peercall) { mediaStreamSendYouTubeVideo(peercall, currentToken, { Type: "Resume", Resume: { - position: player.getCurrentTime() + position: position } }); }); } }); - $scope.$on("youtube.buffering", function() { + $scope.$on("youtube.buffering", function(event, position) { if (initialState === 2) { initialState = null; player.pauseVideo(); } - - startDetectSeek(); }); - $scope.$on("youtube.paused", function() { - stopDetectSeek(); + $scope.$on("youtube.paused", function(event, position) { if (!$scope.isPublisher || !currentToken) { return; } @@ -213,7 +259,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], mediaStreamSendYouTubeVideo(peercall, currentToken, { Type: "Pause", Pause: { - position: player.getCurrentTime() + position: position } }); }); @@ -221,7 +267,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }); $scope.$on("youtube.ended", function() { - stopDetectSeek(); }); $scope.$on("youtube.seeked", function($event, position) { @@ -239,12 +284,43 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }); }); + $scope.$on("youtube.error", function($event, msgid) { + var message; + switch (msgid) { + case "loadScriptFailed": + message = translation._("Could not load YouTube player API, please check your network / firewall settings."); + break; + case "invalidParameter": + message = translation._("The request contains an invalid parameter value. Please check the URL of the video you want to share and try again."); + break; + case "htmlPlayerError": + message = translation._("The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred. Please try again later."); + break; + case "videoNotFound": + message = translation._("The video requested was not found. Please check the URL of the video you want to share and try again."); + break; + case "notAllowedEmbedded": + message = translation._("The owner of the requested video does not allow it to be played in embedded players."); + break; + default: + if (msgid) { + message = translation._("An unknown error occurred while playing back the video (%s). Please try again later.", msgid); + } else { + message = translation._("An unknown error occurred while playing back the video. Please try again later."); + } + break; + } + if (player) { + player.destroy(); + player = null; + } + alertify.dialog.alert(message); + }); + + var playVideo = function(id, position, state) { playerReady.done(function() { - $("#youtubeplayer").show(); $scope.playbackActive = true; - prevTime = null; - prevNow = null; isPaused = null; if (playReceivedNow) { var delta = ((new Date()) - playReceivedNow) * 0.001; @@ -278,7 +354,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], isYouTubeIframeAPIReady.then(function() { if (!player) { var origin = $window.location.protocol + "//" + $window.location.host; - player = new $window.YT.Player("youtubeplayer", { + player = new SandboxPlayer(sandboxApi, { height: "390", width: "640", playerVars: { @@ -291,13 +367,8 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], "controls": with_controls ? "2" : "0", "disablekb": with_controls ? "0" : "1", "origin": origin - }, - events: { - "onReady": onPlayerReady, - "onStateChange": onPlayerStateChange } }); - $("#youtubeplayer").show(); safeApply($scope, function(scope) { // YT player events don't fire in Firefox if // player is not visible, so show while loading @@ -475,23 +546,11 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], }; $scope.loadYouTubeAPI = function() { - if (!addedIframeScript) { - var head = $document[0].getElementsByTagName('head')[0]; - var script = $document[0].createElement('script'); - script.type = "text/javascript"; - script.src = YOUTUBE_IFRAME_API_URL; - script.onerror = function(event) { - alertify.dialog.alert(translation._("Could not load YouTube player API, please check your network / firewall settings.")); - head.removeChild(script); - addedIframeScript = false; - }; - head.appendChild(script); - addedIframeScript = true; - } + createSandboxApi(true); }; $scope.showYouTubeVideo = function() { - $scope.loadYouTubeAPI(); + createSandboxApi(); $scope.layout.youtubevideo = true; $scope.$emit("mainview", "youtubevideo", true); if (currentToken) { @@ -538,7 +597,6 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], $scope.currentVideoUrl = null; $scope.currentVideoId = null; peers = {}; - stopDetectSeek(); playerReady = null; initialState = null; mediaStream.webrtc.e.off("statechange", updater); @@ -568,7 +626,11 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], $scope.toggleFullscreen = function(elem) { if (BigScreen.enabled) { - BigScreen.toggle(elem); + BigScreen.toggle(elem, function() { + $(elem).addClass("fullscreen"); + }, function() { + $(elem).removeClass("fullscreen"); + }); } }; @@ -577,7 +639,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], var compile = function(tElement, tAttr) { return function(scope, iElement, iAttrs, controller) { - $(iElement).find("#youtubecontainer").on("dblclick", _.debounce(function(event) { + $(iElement).find(".youtubecontainer").on("dblclick", _.debounce(function(event) { scope.toggleFullscreen(event.delegateTarget); }, 100, true)); } diff --git a/static/js/filters/buddyimagesrc.js b/static/js/filters/buddyimagesrc.js index f1ac0d96..6abce770 100644 --- a/static/js/filters/buddyimagesrc.js +++ b/static/js/filters/buddyimagesrc.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/filters/displayconference.js b/static/js/filters/displayconference.js index 1979fe19..a8a0fee2 100644 --- a/static/js/filters/displayconference.js +++ b/static/js/filters/displayconference.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/filters/displayname.js b/static/js/filters/displayname.js index 20003426..a76d8fa0 100644 --- a/static/js/filters/displayname.js +++ b/static/js/filters/displayname.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/filters/displaynameforsession.js b/static/js/filters/displaynameforsession.js index bde9ebc2..0f2f1841 100644 --- a/static/js/filters/displaynameforsession.js +++ b/static/js/filters/displaynameforsession.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/filters/displayuserid.js b/static/js/filters/displayuserid.js index e8c0db61..e6afe04c 100644 --- a/static/js/filters/displayuserid.js +++ b/static/js/filters/displayuserid.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/filters/filters.js b/static/js/filters/filters.js index b84df186..4f0cdcac 100644 --- a/static/js/filters/filters.js +++ b/static/js/filters/filters.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/filters/formatbase1000.js b/static/js/filters/formatbase1000.js index e675e2a5..28666c96 100644 --- a/static/js/filters/formatbase1000.js +++ b/static/js/filters/formatbase1000.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/libs/jed.js b/static/js/libs/jed.js index 4e958e87..87074f33 100644 --- a/static/js/libs/jed.js +++ b/static/js/libs/jed.js @@ -1,8 +1,7 @@ +/** + * @preserve jed.js 1.1.0 https://github.com/SlexAxton/Jed + */ /* -jed.js -v0.5.4 - -https://github.com/SlexAxton/Jed ----------- A gettext compatible i18n library for modern JavaScript Applications @@ -91,7 +90,9 @@ in order to offer easy upgrades -- jsgettext.berlios.de } }, // The default domain if one is missing - "domain" : "messages" + "domain" : "messages", + // enable debug mode to log untranslated strings to the console + "debug" : false }; // Mix in the sent options with the default options @@ -136,7 +137,7 @@ in order to offer easy upgrades -- jsgettext.berlios.de }, fetch : function ( sArr ) { if ( {}.toString.call( sArr ) != '[object Array]' ) { - sArr = [].slice.call(arguments); + sArr = [].slice.call(arguments, 0); } return ( sArr && sArr.length ? Jed.sprintf : function(x){ return x; } )( this._i18n.dcnpgettext(this._domain, this._context, this._key, this._pkey, this._val), @@ -221,9 +222,6 @@ in order to offer easy upgrades -- jsgettext.berlios.de // isn't explicitly passed in domain = domain || this._textdomain; - // Default the value to the singular case - val = typeof val == 'undefined' ? 1 : val; - var fallback; // Handle special cases @@ -257,23 +255,34 @@ in order to offer easy upgrades -- jsgettext.berlios.de throw new Error('No translation key found.'); } - // Handle invalid numbers, but try casting strings for good measure - if ( typeof val != 'number' ) { - val = parseInt( val, 10 ); - - if ( isNaN( val ) ) { - throw new Error('The number that was passed in is not a number.'); - } - } - var key = context ? context + Jed.context_delimiter + singular_key : singular_key, locale_data = this.options.locale_data, dict = locale_data[ domain ], - pluralForms = dict[""].plural_forms || (locale_data.messages || this.defaults.locale_data.messages)[""].plural_forms, - val_idx = getPluralFormFunc(pluralForms)(val) + 1, + defaultConf = (locale_data.messages || this.defaults.locale_data.messages)[""], + pluralForms = dict[""].plural_forms || dict[""]["Plural-Forms"] || dict[""]["plural-forms"] || defaultConf.plural_forms || defaultConf["Plural-Forms"] || defaultConf["plural-forms"], val_list, res; + var val_idx; + if (val === undefined) { + // No value passed in; assume singular key lookup. + val_idx = 0; + + } else { + // Value has been passed in; use plural-forms calculations. + + // Handle invalid numbers, but try casting strings for good measure + if ( typeof val != 'number' ) { + val = parseInt( val, 10 ); + + if ( isNaN( val ) ) { + throw new Error('The number that was passed in is not a number.'); + } + } + + val_idx = getPluralFormFunc(pluralForms)(val); + } + // Throw an error if a domain isn't found if ( ! dict ) { throw new Error('No domain named `' + domain + '` could be found.'); @@ -283,20 +292,25 @@ in order to offer easy upgrades -- jsgettext.berlios.de // If there is no match, then revert back to // english style singular/plural with the keys passed in. - if ( ! val_list || val_idx >= val_list.length ) { + if ( ! val_list || val_idx > val_list.length ) { if (this.options.missing_key_callback) { - this.options.missing_key_callback(key); + this.options.missing_key_callback(key, domain); + } + res = [ singular_key, plural_key ]; + + // collect untranslated strings + if (this.options.debug===true) { + console.log(res[ getPluralFormFunc(pluralForms)( val ) ]); } - res = [ null, singular_key, plural_key ]; - return res[ getPluralFormFunc(pluralForms)( val ) + 1 ]; + return res[ getPluralFormFunc()( val ) ]; } res = val_list[ val_idx ]; // This includes empty strings on purpose if ( ! res ) { - res = [ null, singular_key, plural_key ]; - return res[ getPluralFormFunc(pluralForms)( val ) + 1 ]; + res = [ singular_key, plural_key ]; + return res[ getPluralFormFunc()( val ) ]; } return res; } @@ -600,15 +614,15 @@ performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { var $0 = $$.length - 1; switch (yystate) { -case 1: return { type : 'GROUP', expr: $$[$0-1] }; +case 1: return { type : 'GROUP', expr: $$[$0-1] }; break; -case 2:this.$ = { type: 'TERNARY', expr: $$[$0-4], truthy : $$[$0-2], falsey: $$[$0] }; +case 2:this.$ = { type: 'TERNARY', expr: $$[$0-4], truthy : $$[$0-2], falsey: $$[$0] }; break; case 3:this.$ = { type: "OR", left: $$[$0-2], right: $$[$0] }; break; case 4:this.$ = { type: "AND", left: $$[$0-2], right: $$[$0] }; break; -case 5:this.$ = { type: 'LT', left: $$[$0-2], right: $$[$0] }; +case 5:this.$ = { type: 'LT', left: $$[$0-2], right: $$[$0] }; break; case 6:this.$ = { type: 'LTE', left: $$[$0-2], right: $$[$0] }; break; @@ -622,11 +636,11 @@ case 10:this.$ = { type: 'EQ', left: $$[$0-2], right: $$[$0] }; break; case 11:this.$ = { type: 'MOD', left: $$[$0-2], right: $$[$0] }; break; -case 12:this.$ = { type: 'GROUP', expr: $$[$0-1] }; +case 12:this.$ = { type: 'GROUP', expr: $$[$0-1] }; break; -case 13:this.$ = { type: 'VAR' }; +case 13:this.$ = { type: 'VAR' }; break; -case 14:this.$ = { type: 'NUM', val: Number(yytext) }; +case 14:this.$ = { type: 'NUM', val: Number(yytext) }; break; } }, @@ -912,7 +926,7 @@ next:function () { if (this._input === "") { return this.EOF; } else { - this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), + this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), {text: "", token: null, line: this.yylineno}); } }, diff --git a/static/js/libs/pdf/compatibility.js b/static/js/libs/pdf/compatibility.js index 967e312a..7fc076c4 100644 --- a/static/js/libs/pdf/compatibility.js +++ b/static/js/libs/pdf/compatibility.js @@ -1,5 +1,3 @@ -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/static/js/libs/pdf/pdf.js b/static/js/libs/pdf/pdf.js index 1ba993ba..3627683f 100644 --- a/static/js/libs/pdf/pdf.js +++ b/static/js/libs/pdf/pdf.js @@ -1,5 +1,3 @@ -/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,8 +20,8 @@ if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.0.907'; -PDFJS.build = 'e9072ac'; +PDFJS.version = '1.0.1040'; +PDFJS.build = '997096f'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it @@ -601,10 +599,10 @@ var Util = PDFJS.Util = (function UtilClosure() { // makeCssRgb() can be called thousands of times. Using |rgbBuf| avoids // creating many intermediate strings. - Util.makeCssRgb = function Util_makeCssRgb(rgb) { - rgbBuf[1] = rgb[0]; - rgbBuf[3] = rgb[1]; - rgbBuf[5] = rgb[2]; + Util.makeCssRgb = function Util_makeCssRgb(r, g, b) { + rgbBuf[1] = r; + rgbBuf[3] = g; + rgbBuf[5] = b; return rgbBuf.join(''); }; @@ -3111,9 +3109,17 @@ var Metadata = PDFJS.Metadata = (function MetadataClosure() { // Minimal font size that would be used during canvas fillText operations. var MIN_FONT_SIZE = 16; +// Maximum font size that would be used during canvas fillText operations. +var MAX_FONT_SIZE = 100; var MAX_GROUP_SIZE = 4096; +// Heuristic value used when enforcing minimum line widths. +var MIN_WIDTH_FACTOR = 0.65; + var COMPILE_TYPE3_GLYPHS = true; +var MAX_SIZE_TO_COMPILE = 1000; + +var FULL_CHUNK_HEIGHT = 16; function createScratchCanvas(width, height) { var canvas = document.createElement('canvas'); @@ -3247,7 +3253,7 @@ var CachedCanvases = (function CachedCanvasesClosure() { getCanvas: function CachedCanvases_getCanvas(id, width, height, trackTransform) { var canvasEntry; - if (id in cache) { + if (cache[id] !== undefined) { canvasEntry = cache[id]; canvasEntry.canvas.width = width; canvasEntry.canvas.height = height; @@ -3461,6 +3467,7 @@ var CanvasExtraState = (function CanvasExtraStateClosure() { // Default fore and background colors this.fillColor = '#000000'; this.strokeColor = '#000000'; + this.patternFill = false; // Note: fill alpha applies to all non-stroking operations this.fillAlpha = 1; this.strokeAlpha = 1; @@ -3513,6 +3520,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (canvasCtx) { addContextCurrentTransform(canvasCtx); } + this.cachedGetSinglePixelWidth = null; } function putBinaryImageData(ctx, imgData) { @@ -3533,13 +3541,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { // that's ok; any such pixels are ignored. var height = imgData.height, width = imgData.width; - var fullChunkHeight = 16; - var fracChunks = height / fullChunkHeight; - var fullChunks = Math.floor(fracChunks); - var totalChunks = Math.ceil(fracChunks); - var partialChunkHeight = height - fullChunks * fullChunkHeight; + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; - var chunkImgData = ctx.createImageData(width, fullChunkHeight); + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); var srcPos = 0, destPos; var src = imgData.data; var dest = chunkImgData.data; @@ -3559,7 +3565,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { 0xFF000000 : 0x000000FF; for (i = 0; i < totalChunks; i++) { thisChunkHeight = - (i < fullChunks) ? fullChunkHeight : partialChunkHeight; + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; destPos = 0; for (j = 0; j < thisChunkHeight; j++) { var srcDiff = srcLength - srcPos; @@ -3594,19 +3600,19 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { dest32[destPos++] = 0; } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } else if (imgData.kind === ImageKind.RGBA_32BPP) { // RGBA, 32-bits per pixel. j = 0; - elemsInThisChunk = width * fullChunkHeight * 4; + elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4; for (i = 0; i < fullChunks; i++) { dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); srcPos += elemsInThisChunk; ctx.putImageData(chunkImgData, 0, j); - j += fullChunkHeight; + j += FULL_CHUNK_HEIGHT; } if (i < totalChunks) { elemsInThisChunk = width * partialChunkHeight * 4; @@ -3616,11 +3622,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } else if (imgData.kind === ImageKind.RGB_24BPP) { // RGB, 24-bits per pixel. - thisChunkHeight = fullChunkHeight; + thisChunkHeight = FULL_CHUNK_HEIGHT; elemsInThisChunk = width * thisChunkHeight; for (i = 0; i < totalChunks; i++) { if (i >= fullChunks) { - thisChunkHeight =partialChunkHeight; + thisChunkHeight = partialChunkHeight; elemsInThisChunk = width * thisChunkHeight; } @@ -3631,7 +3637,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { dest[destPos++] = src[srcPos++]; dest[destPos++] = 255; } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } else { error('bad image kind: ' + imgData.kind); @@ -3640,20 +3646,18 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { function putBinaryImageMask(ctx, imgData) { var height = imgData.height, width = imgData.width; - var fullChunkHeight = 16; - var fracChunks = height / fullChunkHeight; - var fullChunks = Math.floor(fracChunks); - var totalChunks = Math.ceil(fracChunks); - var partialChunkHeight = height - fullChunks * fullChunkHeight; + var partialChunkHeight = height % FULL_CHUNK_HEIGHT; + var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT; + var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1; - var chunkImgData = ctx.createImageData(width, fullChunkHeight); + var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT); var srcPos = 0; var src = imgData.data; var dest = chunkImgData.data; for (var i = 0; i < totalChunks; i++) { var thisChunkHeight = - (i < fullChunks) ? fullChunkHeight : partialChunkHeight; + (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight; // Expand the mask so it can be used by the canvas. Any required // inversion has already been handled. @@ -3670,7 +3674,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { mask >>= 1; } } - ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } @@ -3680,14 +3684,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { 'globalCompositeOperation', 'font']; for (var i = 0, ii = properties.length; i < ii; i++) { var property = properties[i]; - if (property in sourceCtx) { + if (sourceCtx[property] !== undefined) { destCtx[property] = sourceCtx[property]; } } - if ('setLineDash' in sourceCtx) { + if (sourceCtx.setLineDash !== undefined) { destCtx.setLineDash(sourceCtx.getLineDash()); destCtx.lineDashOffset = sourceCtx.lineDashOffset; - } else if ('mozDash' in sourceCtx) { + } else if (sourceCtx.mozDashOffset !== undefined) { destCtx.mozDash = sourceCtx.mozDash; destCtx.mozDashOffset = sourceCtx.mozDashOffset; } @@ -3722,9 +3726,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { function composeSMaskLuminosity(maskData, layerData) { var length = maskData.length; for (var i = 3; i < length; i += 4) { - var y = ((maskData[i - 3] * 77) + // * 0.3 / 255 * 0x10000 - (maskData[i - 2] * 152) + // * 0.59 .... - (maskData[i - 1] * 28)) | 0; // * 0.11 .... + var y = (maskData[i - 3] * 77) + // * 0.3 / 255 * 0x10000 + (maskData[i - 2] * 152) + // * 0.59 .... + (maskData[i - 1] * 28); // * 0.11 .... layerData[i] = (layerData[i] * y) >> 16; } } @@ -3744,7 +3748,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } // processing image in chunks to save memory - var PIXELS_TO_PROCESS = 65536; + var PIXELS_TO_PROCESS = 1048576; var chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width)); for (var row = 0; row < height; row += chunkSize) { var chunkHeight = Math.min(chunkSize, height - row); @@ -3914,7 +3918,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { }, setDash: function CanvasGraphics_setDash(dashArray, dashPhase) { var ctx = this.ctx; - if ('setLineDash' in ctx) { + if (ctx.setLineDash !== undefined) { ctx.setLineDash(dashArray); ctx.lineDashOffset = dashPhase; } else { @@ -4049,10 +4053,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.current = this.stateStack.pop(); this.ctx.restore(); + + this.cachedGetSinglePixelWidth = null; } }, transform: function CanvasGraphics_transform(a, b, c, d, e, f) { this.ctx.transform(a, b, c, d, e, f); + + this.cachedGetSinglePixelWidth = null; }, // Path @@ -4126,9 +4134,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var strokeColor = this.current.strokeColor; - if (this.current.lineWidth === 0) { - ctx.lineWidth = this.getSinglePixelWidth(); - } + // Prevent drawing too thin lines by enforcing a minimum line width. + ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, + this.current.lineWidth); // For stroke we want to temporarily change the global alpha to the // stroking alpha. ctx.globalAlpha = this.current.strokeAlpha; @@ -4157,10 +4165,10 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { consumePath = typeof consumePath !== 'undefined' ? consumePath : true; var ctx = this.ctx; var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var needRestore = false; - if (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') { + if (isPatternFill) { ctx.save(); ctx.fillStyle = fillColor.getPattern(ctx, this); needRestore = true; @@ -4311,9 +4319,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { // Keeping the font at minimal size and using the fontSizeScale to change // the current transformation matrix before the fillText/strokeText. // See https://bugzilla.mozilla.org/show_bug.cgi?id=726227 - var browserFontSize = size >= MIN_FONT_SIZE ? size : MIN_FONT_SIZE; - this.current.fontSizeScale = browserFontSize !== MIN_FONT_SIZE ? 1.0 : - size / MIN_FONT_SIZE; + var browserFontSize = size < MIN_FONT_SIZE ? MIN_FONT_SIZE : + size > MAX_FONT_SIZE ? MAX_FONT_SIZE : size; + this.current.fontSizeScale = size / browserFontSize; var rule = italic + ' ' + bold + ' ' + browserFontSize + 'px ' + typeface; this.ctx.font = rule; @@ -4453,7 +4461,13 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var lineWidth = current.lineWidth; var scale = current.textMatrixScale; if (scale === 0 || lineWidth === 0) { - lineWidth = this.getSinglePixelWidth(); + var fillStrokeMode = current.textRenderingMode & + TextRenderingMode.FILL_STROKE_MASK; + if (fillStrokeMode === TextRenderingMode.STROKE || + fillStrokeMode === TextRenderingMode.FILL_STROKE) { + this.cachedGetSinglePixelWidth = null; + lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR; + } } else { lineWidth /= scale; } @@ -4548,9 +4562,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var textHScale = current.textHScale * fontDirection; var fontMatrix = current.fontMatrix || FONT_IDENTITY_MATRIX; var glyphsLength = glyphs.length; + var isTextInvisible = + current.textRenderingMode === TextRenderingMode.INVISIBLE; var i, glyph, width; - if (fontSize === 0) { + if (isTextInvisible || fontSize === 0) { return; } @@ -4632,16 +4648,18 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { }, setFillColorN: function CanvasGraphics_setFillColorN(/*...*/) { this.current.fillColor = this.getColorN_Pattern(arguments); + this.current.patternFill = true; }, setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.ctx.strokeStyle = color; this.current.strokeColor = color; }, setFillRGBColor: function CanvasGraphics_setFillRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.ctx.fillStyle = color; this.current.fillColor = color; + this.current.patternFill = false; }, shadingFill: function CanvasGraphics_shadingFill(patternIR) { @@ -4900,11 +4918,12 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) { var ctx = this.ctx; var width = img.width, height = img.height; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var glyph = this.processingType3; - if (COMPILE_TYPE3_GLYPHS && glyph && !('compiled' in glyph)) { - var MAX_SIZE_TO_COMPILE = 1000; + if (COMPILE_TYPE3_GLYPHS && glyph && glyph.compiled === undefined) { if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) { glyph.compiled = compileType3Glyph({data: img.data, width: width, height: height}); @@ -4926,9 +4945,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? + maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); @@ -4942,7 +4959,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { scaleY, positions) { var width = imgData.width; var height = imgData.height; - var ctx = this.ctx; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); var maskCtx = maskCanvas.context; @@ -4952,14 +4970,13 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? - fillColor.getPattern(maskCtx, this) : fillColor; + maskCtx.fillStyle = isPatternFill ? + fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); maskCtx.restore(); + var ctx = this.ctx; for (var i = 0, ii = positions.length; i < ii; i += 2) { ctx.save(); ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]); @@ -4974,6 +4991,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { function CanvasGraphics_paintImageMaskXObjectGroup(images) { var ctx = this.ctx; + var fillColor = this.current.fillColor; + var isPatternFill = this.current.patternFill; for (var i = 0, ii = images.length; i < ii; i++) { var image = images[i]; var width = image.width, height = image.height; @@ -4986,9 +5005,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { maskCtx.globalCompositeOperation = 'source-in'; - var fillColor = this.current.fillColor; - maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && - fillColor.type === 'Pattern') ? + maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this) : fillColor; maskCtx.fillRect(0, 0, width, height); @@ -5191,11 +5208,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { ctx.beginPath(); }, getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) { - var inverse = this.ctx.mozCurrentTransformInverse; - // max of the current horizontal and vertical scale - return Math.sqrt(Math.max( - (inverse[0] * inverse[0] + inverse[1] * inverse[1]), - (inverse[2] * inverse[2] + inverse[3] * inverse[3]))); + if (this.cachedGetSinglePixelWidth === null) { + var inverse = this.ctx.mozCurrentTransformInverse; + // max of the current horizontal and vertical scale + this.cachedGetSinglePixelWidth = Math.sqrt(Math.max( + (inverse[0] * inverse[0] + inverse[1] * inverse[1]), + (inverse[2] * inverse[2] + inverse[3] * inverse[3]))); + } + return this.cachedGetSinglePixelWidth; }, getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) { var transform = this.ctx.mozCurrentTransform; @@ -5263,7 +5283,7 @@ var WebGLUtils = (function WebGLUtilsClosure() { } var currentGL, currentCanvas; - function generageGL() { + function generateGL() { if (currentGL) { return; } @@ -5321,7 +5341,7 @@ var WebGLUtils = (function WebGLUtilsClosure() { function initSmaskGL() { var canvas, gl; - generageGL(); + generateGL(); canvas = currentCanvas; currentCanvas = null; gl = currentGL; @@ -5453,7 +5473,7 @@ var WebGLUtils = (function WebGLUtilsClosure() { function initFiguresGL() { var canvas, gl; - generageGL(); + generateGL(); canvas = currentCanvas; currentCanvas = null; gl = currentGL; @@ -5621,7 +5641,7 @@ var WebGLUtils = (function WebGLUtilsClosure() { } var enabled = false; try { - generageGL(); + generateGL(); enabled = !!currentGL; } catch (e) { } return shadow(this, 'isEnabled', enabled); @@ -6014,7 +6034,7 @@ var TilingPattern = (function TilingPatternClosure() { context.strokeStyle = ctx.strokeStyle; break; case PaintType.UNCOLORED: - var cssColor = Util.makeCssRgb(color); + var cssColor = Util.makeCssRgb(color[0], color[1], color[2]); context.fillStyle = cssColor; context.strokeStyle = cssColor; break; @@ -6373,7 +6393,6 @@ var FontFaceObject = (function FontFaceObjectClosure() { })(); -var HIGHLIGHT_OFFSET = 4; // px var ANNOT_MIN_SIZE = 10; // px var AnnotationUtils = (function AnnotationUtilsClosure() { @@ -6400,43 +6419,36 @@ var AnnotationUtils = (function AnnotationUtilsClosure() { style.fontFamily = fontFamily + fallbackName; } - // TODO(mack): Remove this, it's not really that helpful. - function getEmptyContainer(tagName, rect, borderWidth) { - var bWidth = borderWidth || 0; - var element = document.createElement(tagName); - element.style.borderWidth = bWidth + 'px'; - var width = rect[2] - rect[0] - 2 * bWidth; - var height = rect[3] - rect[1] - 2 * bWidth; - element.style.width = width + 'px'; - element.style.height = height + 'px'; - return element; - } - - function initContainer(item) { - var container = getEmptyContainer('section', item.rect, item.borderWidth); - container.style.backgroundColor = item.color; - - var color = item.color; - var rgb = []; - for (var i = 0; i < 3; ++i) { - rgb[i] = Math.round(color[i] * 255); + function initContainer(item, drawBorder) { + var container = document.createElement('section'); + var cstyle = container.style; + var width = item.rect[2] - item.rect[0]; + var height = item.rect[3] - item.rect[1]; + + var bWidth = item.borderWidth || 0; + if (bWidth) { + width = width - 2 * bWidth; + height = height - 2 * bWidth; + cstyle.borderWidth = bWidth + 'px'; + var color = item.color; + if (drawBorder && color) { + cstyle.borderStyle = 'solid'; + cstyle.borderColor = Util.makeCssRgb(Math.round(color[0] * 255), + Math.round(color[1] * 255), + Math.round(color[2] * 255)); + } } - item.colorCssRgb = Util.makeCssRgb(rgb); - - var highlight = document.createElement('div'); - highlight.className = 'annotationHighlight'; - highlight.style.left = highlight.style.top = -HIGHLIGHT_OFFSET + 'px'; - highlight.style.right = highlight.style.bottom = -HIGHLIGHT_OFFSET + 'px'; - highlight.setAttribute('hidden', true); - - item.highlightElement = highlight; - container.appendChild(item.highlightElement); - + cstyle.width = width + 'px'; + cstyle.height = height + 'px'; return container; } function getHtmlElementForTextWidgetAnnotation(item, commonObjs) { - var element = getEmptyContainer('div', item.rect, 0); + var element = document.createElement('div'); + var width = item.rect[2] - item.rect[0]; + var height = item.rect[3] - item.rect[1]; + element.style.width = width + 'px'; + element.style.height = height + 'px'; element.style.display = 'table'; var content = document.createElement('div'); @@ -6466,7 +6478,7 @@ var AnnotationUtils = (function AnnotationUtilsClosure() { rect[2] = rect[0] + (rect[3] - rect[1]); // make it square } - var container = initContainer(item); + var container = initContainer(item, false); container.className = 'annotText'; var image = document.createElement('img'); @@ -6491,13 +6503,15 @@ var AnnotationUtils = (function AnnotationUtilsClosure() { var i, ii; if (item.hasBgColor) { var color = item.color; - var rgb = []; - for (i = 0; i < 3; ++i) { - // Enlighten the color (70%) - var c = Math.round(color[i] * 255); - rgb[i] = Math.round((255 - c) * 0.7) + c; - } - content.style.backgroundColor = Util.makeCssRgb(rgb); + + // Enlighten the color (70%) + var BACKGROUND_ENLIGHT = 0.7; + var r = BACKGROUND_ENLIGHT * (1.0 - color[0]) + color[0]; + var g = BACKGROUND_ENLIGHT * (1.0 - color[1]) + color[1]; + var b = BACKGROUND_ENLIGHT * (1.0 - color[2]) + color[2]; + content.style.backgroundColor = Util.makeCssRgb((r * 255) | 0, + (g * 255) | 0, + (b * 255) | 0); } var title = document.createElement('h1'); @@ -6573,12 +6587,9 @@ var AnnotationUtils = (function AnnotationUtilsClosure() { } function getHtmlElementForLinkAnnotation(item) { - var container = initContainer(item); + var container = initContainer(item, true); container.className = 'annotLink'; - container.style.borderColor = item.colorCssRgb; - container.style.borderStyle = 'solid'; - var link = document.createElement('a'); link.href = link.title = item.url || ''; @@ -7409,11 +7420,11 @@ var SVGGraphics = (function SVGGraphicsClosure() { this.current.miterLimit = limit; }, setStrokeRGBColor: function SVGGraphics_setStrokeRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.current.strokeColor = color; }, setFillRGBColor: function SVGGraphics_setFillRGBColor(r, g, b) { - var color = Util.makeCssRgb(arguments); + var color = Util.makeCssRgb(r, g, b); this.current.fillColor = color; this.current.tspan = document.createElementNS(NS, 'svg:tspan'); this.current.xcoords = []; diff --git a/static/js/libs/pdf/pdf.worker.js b/static/js/libs/pdf/pdf.worker.js index 9820c100..e68c9ad1 100644 --- a/static/js/libs/pdf/pdf.worker.js +++ b/static/js/libs/pdf/pdf.worker.js @@ -22,8 +22,8 @@ if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.0.907'; -PDFJS.build = 'e9072ac'; +PDFJS.version = '1.0.1040'; +PDFJS.build = '997096f'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it @@ -601,10 +601,10 @@ var Util = PDFJS.Util = (function UtilClosure() { // makeCssRgb() can be called thousands of times. Using |rgbBuf| avoids // creating many intermediate strings. - Util.makeCssRgb = function Util_makeCssRgb(rgb) { - rgbBuf[1] = rgb[0]; - rgbBuf[3] = rgb[1]; - rgbBuf[5] = rgb[2]; + Util.makeCssRgb = function Util_makeCssRgb(r, g, b) { + rgbBuf[1] = r; + rgbBuf[3] = g; + rgbBuf[5] = b; return rgbBuf.join(''); }; @@ -1687,7 +1687,10 @@ var NetworkManager = (function NetworkManagerClosure() { } if (args.onProgressiveData) { - xhr.responseType = 'moz-chunked-arraybuffer'; + // Some legacy browsers might throw an exception. + try { + xhr.responseType = 'moz-chunked-arraybuffer'; + } catch(e) {} if (xhr.responseType === 'moz-chunked-arraybuffer') { pendingRequest.onProgressiveData = args.onProgressiveData; pendingRequest.mozChunked = true; @@ -2845,6 +2848,10 @@ var Page = (function PageClosure() { * `PDFDocument` objects on the main thread created. */ var PDFDocument = (function PDFDocumentClosure() { + var FINGERPRINT_FIRST_BYTES = 1024; + var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' + + '\x00\x00\x00\x00\x00\x00\x00\x00\x00'; + function PDFDocument(pdfManager, arg, password) { if (isStream(arg)) { init.call(this, pdfManager, arg, password); @@ -3056,16 +3063,25 @@ var PDFDocument = (function PDFDocumentClosure() { return shadow(this, 'documentInfo', docInfo); }, get fingerprint() { - var xref = this.xref, hash, fileID = ''; + var xref = this.xref, idArray, hash, fileID = ''; if (xref.trailer.has('ID')) { - hash = stringToBytes(xref.trailer.get('ID')[0]); + idArray = xref.trailer.get('ID'); + } + if (idArray && isArray(idArray) && idArray[0] !== EMPTY_FINGERPRINT) { + hash = stringToBytes(idArray[0]); } else { - hash = calculateMD5(this.stream.bytes.subarray(0, 100), 0, 100); + if (this.stream.ensureRange) { + this.stream.ensureRange(0, + Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end)); + } + hash = calculateMD5(this.stream.bytes.subarray(0, + FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES); } for (var i = 0, n = hash.length; i < n; i++) { - fileID += hash[i].toString(16); + var hex = hash[i].toString(16); + fileID += hex.length === 1 ? '0' + hex : hex; } return shadow(this, 'fingerprint', fileID); @@ -4921,12 +4937,26 @@ var Annotation = (function AnnotationClosure() { data.annotationFlags = dict.get('F'); var color = dict.get('C'); - if (isArray(color) && color.length === 3) { - // TODO(mack): currently only supporting rgb; need support different - // colorspaces - data.color = color; - } else { + if (!color) { + // The PDF spec does not mention how a missing color array is interpreted. + // Adobe Reader seems to default to black in this case. data.color = [0, 0, 0]; + } else if (isArray(color)) { + switch (color.length) { + case 0: + // Empty array denotes transparent border. + data.color = null; + break; + case 1: + // TODO: implement DeviceGray + break; + case 3: + data.color = color; + break; + case 4: + // TODO: implement DeviceCMYK + break; + } } // Some types of annotations have border style dict which has more @@ -4943,7 +4973,7 @@ var Annotation = (function AnnotationClosure() { if (data.borderWidth > 0 && dashArray) { if (!isArray(dashArray)) { // Ignore the border if dashArray is not actually an array, - // this is consistent with the behaviour in Adobe Reader. + // this is consistent with the behaviour in Adobe Reader. data.borderWidth = 0; } else { var dashArrayLength = dashArray.length; @@ -5189,7 +5219,7 @@ var WidgetAnnotation = (function WidgetAnnotationClosure() { var name = namedItem.get('T'); if (name) { fieldName.unshift(stringToPDFString(name)); - } else { + } else if (parent && ref) { // The field name is absent, that means more than one field // with the same name may exist. Replacing the empty name // with the '`' plus index in the parent's 'Kids' array. @@ -5366,11 +5396,7 @@ var LinkAnnotation = (function LinkAnnotationClosure() { return url; } - Util.inherit(LinkAnnotation, InteractiveAnnotation, { - hasOperatorList: function LinkAnnotation_hasOperatorList() { - return false; - } - }); + Util.inherit(LinkAnnotation, InteractiveAnnotation, { }); return LinkAnnotation; })(); @@ -5790,7 +5816,7 @@ var PDFFunction = (function PDFFunctionClosure() { var cachedValue = cache[key]; if (cachedValue !== undefined) { - cachedValue.set(dest, destOffset); + dest.set(cachedValue, destOffset); return; } @@ -5814,7 +5840,7 @@ var PDFFunction = (function PDFFunctionClosure() { cache_available--; cache[key] = output; } - output.set(dest, destOffset); + dest.set(output, destOffset); }; } }; @@ -9769,19 +9795,27 @@ var Pattern = (function PatternClosure() { var dict = isStream(shading) ? shading.dict : shading; var type = dict.get('ShadingType'); - switch (type) { - case PatternType.AXIAL: - case PatternType.RADIAL: - // Both radial and axial shadings are handled by RadialAxial shading. - return new Shadings.RadialAxial(dict, matrix, xref, res); - case PatternType.FREE_FORM_MESH: - case PatternType.LATTICE_FORM_MESH: - case PatternType.COONS_PATCH_MESH: - case PatternType.TENSOR_PATCH_MESH: - return new Shadings.Mesh(shading, matrix, xref, res); - default: - UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern); - return new Shadings.Dummy(); + try { + switch (type) { + case PatternType.AXIAL: + case PatternType.RADIAL: + // Both radial and axial shadings are handled by RadialAxial shading. + return new Shadings.RadialAxial(dict, matrix, xref, res); + case PatternType.FREE_FORM_MESH: + case PatternType.LATTICE_FORM_MESH: + case PatternType.COONS_PATCH_MESH: + case PatternType.TENSOR_PATCH_MESH: + return new Shadings.Mesh(shading, matrix, xref, res); + default: + throw new Error('Unknown PatternType: ' + type); + } + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + UnsupportedManager.notify(UNSUPPORTED_FEATURES.shadingPattern); + warn(ex); + return new Shadings.Dummy(); } }; return Pattern; @@ -9866,14 +9900,14 @@ Shadings.RadialAxial = (function RadialAxialClosure() { ratio[0] = i; fn(ratio, 0, color, 0); rgbColor = cs.getRgb(color, 0); - var cssColor = Util.makeCssRgb(rgbColor); + var cssColor = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); colorStops.push([(i - t0) / diff, cssColor]); } var background = 'transparent'; if (dict.has('Background')) { rgbColor = cs.getRgb(dict.get('Background'), 0); - background = Util.makeCssRgb(rgbColor); + background = Util.makeCssRgb(rgbColor[0], rgbColor[1], rgbColor[2]); } if (!extendStart) { @@ -10659,7 +10693,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { buildPaintImageXObject: function PartialEvaluator_buildPaintImageXObject(resources, image, inline, operatorList, - cacheKey, cache) { + cacheKey, imageCache) { var self = this; var dict = image.dict; var w = dict.get('Width', 'W'); @@ -10697,9 +10731,10 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { args = [imgData]; operatorList.addOp(OPS.paintImageMaskXObject, args); if (cacheKey) { - cache.key = cacheKey; - cache.fn = OPS.paintImageMaskXObject; - cache.args = args; + imageCache[cacheKey] = { + fn: OPS.paintImageMaskXObject, + args: args + }; } return; } @@ -10741,16 +10776,17 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var imgData = imageObj.createImageData(/* forceRGBA = */ false); self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], [imgData.data.buffer]); - }).then(null, function (reason) { + }).then(undefined, function (reason) { warn('Unable to decode image: ' + reason); self.handler.send('obj', [objId, self.pageIndex, 'Image', null]); }); operatorList.addOp(OPS.paintImageXObject, args); if (cacheKey) { - cache.key = cacheKey; - cache.fn = OPS.paintImageXObject; - cache.args = args; + imageCache[cacheKey] = { + fn: OPS.paintImageXObject, + args: args + }; } }, @@ -11144,8 +11180,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } // eagerly compile XForm objects var name = args[0].name; - if (imageCache.key === name) { - operatorList.addOp(imageCache.fn, imageCache.args); + if (imageCache[name] !== undefined) { + operatorList.addOp(imageCache[name].fn, imageCache[name].args); args = null; continue; } @@ -11194,10 +11230,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, reject); case OPS.endInlineImage: var cacheKey = args[0].cacheKey; - if (cacheKey && imageCache.key === cacheKey) { - operatorList.addOp(imageCache.fn, imageCache.args); - args = null; - continue; + if (cacheKey) { + var cacheEntry = imageCache[cacheKey]; + if (cacheEntry !== undefined) { + operatorList.addOp(cacheEntry.fn, cacheEntry.args); + args = null; + continue; + } } self.buildPaintImageXObject(resources, args[0], true, operatorList, cacheKey, imageCache); @@ -14364,9 +14403,12 @@ var stdFontMap = { 'CourierNewPS-BoldMT': 'Courier-Bold', 'CourierNewPS-ItalicMT': 'Courier-Oblique', 'CourierNewPSMT': 'Courier', + 'Helvetica': 'Helvetica', 'Helvetica-Bold': 'Helvetica-Bold', 'Helvetica-BoldItalic': 'Helvetica-BoldOblique', + 'Helvetica-BoldOblique': 'Helvetica-BoldOblique', 'Helvetica-Italic': 'Helvetica-Oblique', + 'Helvetica-Oblique':'Helvetica-Oblique', 'Symbol-Bold': 'Symbol', 'Symbol-BoldItalic': 'Symbol', 'Symbol-Italic': 'Symbol', @@ -14392,6 +14434,10 @@ var stdFontMap = { * fonts without glyph data. */ var nonStdFontMap = { + 'CenturyGothic': 'Helvetica', + 'CenturyGothic-Bold': 'Helvetica-Bold', + 'CenturyGothic-BoldItalic': 'Helvetica-BoldOblique', + 'CenturyGothic-Italic': 'Helvetica-Oblique', 'ComicSansMS': 'Comic Sans MS', 'ComicSansMS-Bold': 'Comic Sans MS-Bold', 'ComicSansMS-BoldItalic': 'Comic Sans MS-BoldItalic', @@ -14415,7 +14461,8 @@ var nonStdFontMap = { 'MS-PMincho': 'MS PMincho', 'MS-PMincho-Bold': 'MS PMincho-Bold', 'MS-PMincho-BoldItalic': 'MS PMincho-BoldItalic', - 'MS-PMincho-Italic': 'MS PMincho-Italic' + 'MS-PMincho-Italic': 'MS PMincho-Italic', + 'Wingdings': 'ZapfDingbats' }; var serifFonts = { @@ -16498,7 +16545,8 @@ var Font = (function FontClosure() { // The file data is not specified. Trying to fix the font name // to be used with the canvas.font. var fontName = name.replace(/[,_]/g, '-'); - var isStandardFont = fontName in stdFontMap; + var isStandardFont = !!stdFontMap[fontName] || + (nonStdFontMap[fontName] && !!stdFontMap[nonStdFontMap[fontName]]); fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName; this.bold = (fontName.search(/bold/gi) !== -1); @@ -16544,6 +16592,10 @@ var Font = (function FontClosure() { this.toFontChar[charCode] = fontChar; } } else if (/Dingbats/i.test(fontName)) { + if (/Wingdings/i.test(name)) { + warn('Wingdings font without embedded font file, ' + + 'falling back to the ZapfDingbats encoding.'); + } var dingbats = Encodings.ZapfDingbatsEncoding; for (charCode in dingbats) { fontChar = DingbatsGlyphsUnicode[dingbats[charCode]]; @@ -16729,6 +16781,7 @@ var Font = (function FontClosure() { fontCharCode <= 0x1f || // Control chars fontCharCode === 0x7F || // Control char fontCharCode === 0xAD || // Soft hyphen + fontCharCode === 0xA0 || // Non breaking space (fontCharCode >= 0x80 && fontCharCode <= 0x9F) || // Control chars // Prevent drawing characters in the specials unicode block. (fontCharCode >= 0xFFF0 && fontCharCode <= 0xFFFF) || @@ -18747,6 +18800,8 @@ function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) { glyphId = glyphNames.indexOf(baseEncoding[charCode]); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } else if (!!(properties.flags & FontFlags.Symbolic)) { @@ -18763,6 +18818,8 @@ function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) { glyphId = glyphNames.indexOf(baseEncoding[charCode]); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } @@ -18775,6 +18832,8 @@ function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) { glyphId = glyphNames.indexOf(glyphName); if (glyphId >= 0) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } @@ -29950,16 +30009,14 @@ function isEOF(v) { return (v === EOF); } +var MAX_LENGTH_TO_CACHE = 1000; + var Parser = (function ParserClosure() { function Parser(lexer, allowStreams, xref) { this.lexer = lexer; this.allowStreams = allowStreams; this.xref = xref; - this.imageCache = { - length: 0, - adler32: 0, - stream: null - }; + this.imageCache = {}; this.refill(); } @@ -30050,31 +30107,14 @@ var Parser = (function ParserClosure() { // simple object return buf1; }, - makeInlineImage: function Parser_makeInlineImage(cipherTransform) { - var lexer = this.lexer; - var stream = lexer.stream; - - // parse dictionary - var dict = new Dict(null); - while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { - if (!isName(this.buf1)) { - error('Dictionary key must be a name object'); - } - - var key = this.buf1.name; - this.shift(); - if (isEOF(this.buf1)) { - break; - } - dict.set(key, this.getObj(cipherTransform)); - } - - // parse image stream - var startPos = stream.pos; - - // searching for the /EI\s/ - var state = 0, ch, i, ii; - var E = 0x45, I = 0x49, SPACE = 0x20, NL = 0xA, CR = 0xD; + /** + * Find the end of the stream by searching for the /EI\s/. + * @returns {number} The inline stream length. + */ + findDefaultInlineStreamEnd: + function Parser_findDefaultInlineStreamEnd(stream) { + var E = 0x45, I = 0x49, SPACE = 0x20, LF = 0xA, CR = 0xD; + var startPos = stream.pos, state = 0, ch, i, n, followingBytes; while ((ch = stream.getByte()) !== -1) { if (state === 0) { state = (ch === E) ? 1 : 0; @@ -30082,13 +30122,13 @@ var Parser = (function ParserClosure() { state = (ch === I) ? 2 : 0; } else { assert(state === 2); - if (ch === SPACE || ch === NL || ch === CR) { + if (ch === SPACE || ch === LF || ch === CR) { // Let's check the next five bytes are ASCII... just be sure. - var n = 5; - var followingBytes = stream.peekBytes(n); + n = 5; + followingBytes = stream.peekBytes(n); for (i = 0; i < n; i++) { ch = followingBytes[i]; - if (ch !== NL && ch !== CR && (ch < SPACE || ch > 0x7F)) { + if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7F)) { // Not a LF, CR, SPACE or any visible ASCII character, i.e. // it's binary stuff. Resetting the state. state = 0; @@ -30096,45 +30136,138 @@ var Parser = (function ParserClosure() { } } if (state === 2) { - break; // finished! + break; // Finished! } } else { state = 0; } } } + return ((stream.pos - 4) - startPos); + }, + /** + * Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCII85DecodeInlineStreamEnd: + function Parser_findASCII85DecodeInlineStreamEnd(stream) { + var TILDE = 0x7E, GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === TILDE && stream.peekByte() === GT) { + stream.skip(); + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCII85Decode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Find the EOD (end-of-data) marker '>' (i.e. GT) of the stream. + * @returns {number} The inline stream length. + */ + findASCIIHexDecodeInlineStreamEnd: + function Parser_findASCIIHexDecodeInlineStreamEnd(stream) { + var GT = 0x3E; + var startPos = stream.pos, ch, length; + while ((ch = stream.getByte()) !== -1) { + if (ch === GT) { + break; + } + } + length = stream.pos - startPos; + if (ch === -1) { + warn('Inline ASCIIHexDecode image stream: ' + + 'EOD marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, + /** + * Skip over the /EI/ for streams where we search for an EOD marker. + */ + inlineStreamSkipEI: function Parser_inlineStreamSkipEI(stream) { + var E = 0x45, I = 0x49; + var state = 0, ch; + while ((ch = stream.getByte()) !== -1) { + if (state === 0) { + state = (ch === E) ? 1 : 0; + } else if (state === 1) { + state = (ch === I) ? 2 : 0; + } else if (state === 2) { + break; + } + } + }, + makeInlineImage: function Parser_makeInlineImage(cipherTransform) { + var lexer = this.lexer; + var stream = lexer.stream; - var length = (stream.pos - 4) - startPos; + // Parse dictionary. + var dict = new Dict(null); + while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) { + if (!isName(this.buf1)) { + error('Dictionary key must be a name object'); + } + var key = this.buf1.name; + this.shift(); + if (isEOF(this.buf1)) { + break; + } + dict.set(key, this.getObj(cipherTransform)); + } + + // Extract the name of the first (i.e. the current) image filter. + var filter = this.fetchIfRef(dict.get('Filter', 'F')), filterName; + if (isName(filter)) { + filterName = filter.name; + } else if (isArray(filter) && isName(filter[0])) { + filterName = filter[0].name; + } + + // Parse image stream. + var startPos = stream.pos, length, i, ii; + if (filterName === 'ASCII85Decide' || filterName === 'A85') { + length = this.findASCII85DecodeInlineStreamEnd(stream); + } else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') { + length = this.findASCIIHexDecodeInlineStreamEnd(stream); + } else { + length = this.findDefaultInlineStreamEnd(stream); + } var imageStream = stream.makeSubStream(startPos, length, dict); - // trying to cache repeat images, first we are trying to "warm up" caching - // using length, then comparing adler32 - var MAX_LENGTH_TO_CACHE = 1000; - var cacheImage = false, adler32; - if (length < MAX_LENGTH_TO_CACHE && this.imageCache.length === length) { + // Cache all images below the MAX_LENGTH_TO_CACHE threshold by their + // adler32 checksum. + var adler32; + if (length < MAX_LENGTH_TO_CACHE) { var imageBytes = imageStream.getBytes(); imageStream.reset(); var a = 1; var b = 0; for (i = 0, ii = imageBytes.length; i < ii; ++i) { - a = (a + (imageBytes[i] & 0xff)) % 65521; - b = (b + a) % 65521; + // No modulo required in the loop if imageBytes.length < 5552. + a += imageBytes[i] & 0xff; + b += a; } - adler32 = (b << 16) | a; + adler32 = ((b % 65521) << 16) | (a % 65521); - if (this.imageCache.stream && this.imageCache.adler32 === adler32) { + if (this.imageCache.adler32 === adler32) { this.buf2 = Cmd.get('EI'); this.shift(); - this.imageCache.stream.reset(); - return this.imageCache.stream; + this.imageCache[adler32].reset(); + return this.imageCache[adler32]; } - cacheImage = true; - } - if (!cacheImage && !this.imageCache.stream) { - this.imageCache.length = length; - this.imageCache.stream = null; } if (cipherTransform) { @@ -30143,10 +30276,9 @@ var Parser = (function ParserClosure() { imageStream = this.filter(imageStream, dict, length); imageStream.dict = dict; - if (cacheImage) { + if (adler32 !== undefined) { imageStream.cacheKey = 'inline_' + length + '_' + adler32; - this.imageCache.adler32 = adler32; - this.imageCache.stream = imageStream; + this.imageCache[adler32] = imageStream; } this.buf2 = Cmd.get('EI'); @@ -30294,22 +30426,6 @@ var Parser = (function ParserClosure() { return new LZWStream(stream, maybeLength, earlyChange); } if (name === 'DCTDecode' || name === 'DCT') { - // According to the specification: for inline images, the ID operator - // shall be followed by a single whitespace character (unless it uses - // ASCII85Decode or ASCIIHexDecode filters). - // In practice this only seems to be followed for inline JPEG images, - // and generally ignoring the first byte of the stream if it is a - // whitespace char can even *cause* issues (e.g. in the CCITTFaxDecode - // filters used in issue2984.pdf). - // Hence when the first byte of the stream of an inline JPEG image is - // a whitespace character, we thus simply skip over it. - if (isCmd(this.buf1, 'ID')) { - var firstByte = stream.peekByte(); - if (firstByte === 0x0A /* LF */ || firstByte === 0x0D /* CR */ || - firstByte === 0x20 /* SPACE */) { - stream.skip(); - } - } xrefStreamStats[StreamType.DCT] = true; return new JpegStream(stream, maybeLength, stream.dict, this.xref); } @@ -31857,8 +31973,15 @@ var PredictorStream = (function PredictorStreamClosure() { */ var JpegStream = (function JpegStreamClosure() { function JpegStream(stream, maybeLength, dict, xref) { - // TODO: per poppler, some images may have 'junk' before that - // need to be removed + // Some images may contain 'junk' before the SOI (start-of-image) marker. + // Note: this seems to mainly affect inline images. + var ch; + while ((ch = stream.getByte()) !== -1) { + if (ch === 0xFF) { // Find the first byte of the SOI marker (0xFFD8). + stream.skip(-1); // Reset the stream position to the SOI. + break; + } + } this.stream = stream; this.maybeLength = maybeLength; this.dict = dict; @@ -33033,7 +33156,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { if (!this.eoblock && this.row === this.rows - 1) { this.eof = true; - } else if (this.eoline || !this.byteAlign) { + } else { code1 = this.lookBits(12); if (this.eoline) { while (code1 !== EOF && code1 !== 1) { @@ -34198,9 +34321,8 @@ var JpegImage = (function jpegImage() { function decodeHuffman(tree) { var node = tree; - var bit; - while ((bit = readBit()) !== null) { - node = node[bit]; + while (true) { + node = node[readBit()]; if (typeof node === 'number') { return node; } @@ -34208,17 +34330,12 @@ var JpegImage = (function jpegImage() { throw 'invalid huffman sequence'; } } - return null; } function receive(length) { var n = 0; while (length > 0) { - var bit = readBit(); - if (bit === null) { - return; - } - n = (n << 1) | bit; + n = (n << 1) | readBit(); length--; } return n; @@ -34577,7 +34694,7 @@ var JpegImage = (function jpegImage() { // stage 3 // Shift v0 by 128.5 << 5 here, so we don't need to shift p0...p7 when - // converting to UInt8 range later. + // converting to UInt8 range later. v0 = ((v0 + v1 + 1) >> 1) + 4112; v1 = v0 - v1; t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12; @@ -34934,8 +35051,7 @@ var JpegImage = (function jpegImage() { } } - // decodeTransform will contains pairs of multiplier (-256..256) and - // additive + // decodeTransform contains pairs of multiplier (-256..256) and additive var transform = this.decodeTransform; if (transform) { for (i = 0; i < dataLength;) { @@ -34972,7 +35088,7 @@ var JpegImage = (function jpegImage() { }, _convertYcckToRgb: function convertYcckToRgb(data) { - var Y, Cb, Cr, k, CbCb, CbCr, CbY, Cbk, CrCr, Crk, CrY, YY, Yk, kk; + var Y, Cb, Cr, k; var offset = 0; for (var i = 0, length = data.length; i < length; i += 4) { Y = data[i]; @@ -34980,43 +35096,35 @@ var JpegImage = (function jpegImage() { Cr = data[i + 2]; k = data[i + 3]; - CbCb = Cb * Cb; - CbCr = Cb * Cr; - CbY = Cb * Y; - Cbk = Cb * k; - CrCr = Cr * Cr; - Crk = Cr * k; - CrY = Cr * Y; - YY = Y * Y; - Yk = Y * k; - kk = k * k; - - var r = - 122.67195406894 - - 6.60635669420364e-5 * CbCb + 0.000437130475926232 * CbCr - - 5.4080610064599e-5* CbY + 0.00048449797120281* Cbk - - 0.154362151871126 * Cb - 0.000957964378445773 * CrCr + - 0.000817076911346625 * CrY - 0.00477271405408747 * Crk + - 1.53380253221734 * Cr + 0.000961250184130688 * YY - - 0.00266257332283933 * Yk + 0.48357088451265 * Y - - 0.000336197177618394 * kk + 0.484791561490776 * k; + var r = -122.67195406894 + + Cb * (-6.60635669420364e-5 * Cb + 0.000437130475926232 * Cr - + 5.4080610064599e-5 * Y + 0.00048449797120281 * k - + 0.154362151871126) + + Cr * (-0.000957964378445773 * Cr + 0.000817076911346625 * Y - + 0.00477271405408747 * k + 1.53380253221734) + + Y * (0.000961250184130688 * Y - 0.00266257332283933 * k + + 0.48357088451265) + + k * (-0.000336197177618394 * k + 0.484791561490776); var g = 107.268039397724 + - 2.19927104525741e-5 * CbCb - 0.000640992018297945 * CbCr + - 0.000659397001245577* CbY + 0.000426105652938837* Cbk - - 0.176491792462875 * Cb - 0.000778269941513683 * CrCr + - 0.00130872261408275 * CrY + 0.000770482631801132 * Crk - - 0.151051492775562 * Cr + 0.00126935368114843 * YY - - 0.00265090189010898 * Yk + 0.25802910206845 * Y - - 0.000318913117588328 * kk - 0.213742400323665 * k; - - var b = - 20.810012546947 - - 0.000570115196973677 * CbCb - 2.63409051004589e-5 * CbCr + - 0.0020741088115012* CbY - 0.00288260236853442* Cbk + - 0.814272968359295 * Cb - 1.53496057440975e-5 * CrCr - - 0.000132689043961446 * CrY + 0.000560833691242812 * Crk - - 0.195152027534049 * Cr + 0.00174418132927582 * YY - - 0.00255243321439347 * Yk + 0.116935020465145 * Y - - 0.000343531996510555 * kk + 0.24165260232407 * k; + Cb * (2.19927104525741e-5 * Cb - 0.000640992018297945 * Cr + + 0.000659397001245577 * Y + 0.000426105652938837 * k - + 0.176491792462875) + + Cr * (-0.000778269941513683 * Cr + 0.00130872261408275 * Y + + 0.000770482631801132 * k - 0.151051492775562) + + Y * (0.00126935368114843 * Y - 0.00265090189010898 * k + + 0.25802910206845) + + k * (-0.000318913117588328 * k - 0.213742400323665); + + var b = -20.810012546947 + + Cb * (-0.000570115196973677 * Cb - 2.63409051004589e-5 * Cr + + 0.0020741088115012 * Y - 0.00288260236853442 * k + + 0.814272968359295) + + Cr * (-1.53496057440975e-5 * Cr - 0.000132689043961446 * Y + + 0.000560833691242812 * k - 0.195152027534049) + + Y * (0.00174418132927582 * Y - 0.00255243321439347 * k + + 0.116935020465145) + + k * (-0.000343531996510555 * k + 0.24165260232407); data[offset++] = clamp0to255(r); data[offset++] = clamp0to255(g); @@ -35157,18 +35265,52 @@ var JpxImage = (function JpxImageClosure() { var dataLength = lbox - headerSize; var jumpDataLength = true; switch (tbox) { - case 0x6A501A1A: // 'jP\032\032' - // TODO - break; case 0x6A703268: // 'jp2h' jumpDataLength = false; // parsing child boxes break; case 0x636F6C72: // 'colr' - // TODO + // Colorspaces are not used, the CS from the PDF is used. + var method = data[position]; + var precedence = data[position + 1]; + var approximation = data[position + 2]; + if (method === 1) { + // enumerated colorspace + var colorspace = readUint32(data, position + 3); + switch (colorspace) { + case 16: // this indicates a sRGB colorspace + case 17: // this indicates a grayscale colorspace + case 18: // this indicates a YUV colorspace + break; + default: + warn('Unknown colorspace ' + colorspace); + break; + } + } else if (method === 2) { + info('ICC profile not supported'); + } break; case 0x6A703263: // 'jp2c' this.parseCodestream(data, position, position + dataLength); break; + case 0x6A502020: // 'jP\024\024' + if (0x0d0a870a !== readUint32(data, position)) { + warn('Invalid JP2 signature'); + } + break; + // The following header types are valid but currently not used: + case 0x6A501A1A: // 'jP\032\032' + case 0x66747970: // 'ftyp' + case 0x72726571: // 'rreq' + case 0x72657320: // 'res ' + case 0x69686472: // 'ihdr' + break; + default: + var headerType = String.fromCharCode((tbox >> 24) & 0xFF, + (tbox >> 16) & 0xFF, + (tbox >> 8) & 0xFF, + tbox & 0xFF); + warn('Unsupported header type ' + tbox + ' (' + headerType + ')'); + break; } if (jumpDataLength) { position += dataLength; @@ -35384,12 +35526,6 @@ var JpxImage = (function JpxImageClosure() { cod.precinctsSizes = precinctsSizes; } var unsupported = []; - if (cod.sopMarkerUsed) { - unsupported.push('sopMarkerUsed'); - } - if (cod.ephMarkerUsed) { - unsupported.push('ephMarkerUsed'); - } if (cod.selectiveArithmeticCodingBypass) { unsupported.push('selectiveArithmeticCodingBypass'); } @@ -35540,6 +35676,23 @@ var JpxImage = (function JpxImageClosure() { // Section B.6 Division resolution to precincts var precinctWidth = 1 << dimensions.PPx; var precinctHeight = 1 << dimensions.PPy; + // Jasper introduces codeblock groups for mapping each subband codeblocks + // to precincts. Precinct partition divides a resolution according to width + // and height parameters. The subband that belongs to the resolution level + // has a different size than the level, unless it is the zero resolution. + + // From Jasper documentation: jpeg2000.pdf, section K: Tier-2 coding: + // The precinct partitioning for a particular subband is derived from a + // partitioning of its parent LL band (i.e., the LL band at the next higher + // resolution level)... The LL band associated with each resolution level is + // divided into precincts... Each of the resulting precinct regions is then + // mapped into its child subbands (if any) at the next lower resolution + // level. This is accomplished by using the coordinate transformation + // (u, v) = (ceil(x/2), ceil(y/2)) where (x, y) and (u, v) are the + // coordinates of a point in the LL band and child subband, respectively. + var isZeroRes = resolution.resLevel === 0; + var precinctWidthInSubband = 1 << (dimensions.PPx + (isZeroRes ? 0 : -1)); + var precinctHeightInSubband = 1 << (dimensions.PPy + (isZeroRes ? 0 : -1)); var numprecinctswide = (resolution.trx1 > resolution.trx0 ? Math.ceil(resolution.trx1 / precinctWidth) - Math.floor(resolution.trx0 / precinctWidth) : 0); @@ -35547,18 +35700,15 @@ var JpxImage = (function JpxImageClosure() { Math.ceil(resolution.try1 / precinctHeight) - Math.floor(resolution.try0 / precinctHeight) : 0); var numprecincts = numprecinctswide * numprecinctshigh; - var precinctXOffset = Math.floor(resolution.trx0 / precinctWidth) * - precinctWidth; - var precinctYOffset = Math.floor(resolution.try0 / precinctHeight) * - precinctHeight; + resolution.precinctParameters = { - precinctXOffset: precinctXOffset, - precinctYOffset: precinctYOffset, precinctWidth: precinctWidth, precinctHeight: precinctHeight, numprecinctswide: numprecinctswide, numprecinctshigh: numprecinctshigh, - numprecincts: numprecincts + numprecincts: numprecincts, + precinctWidthInSubband: precinctWidthInSubband, + precinctHeightInSubband: precinctHeightInSubband }; } function buildCodeblocks(context, subband, dimensions) { @@ -35585,21 +35735,29 @@ var JpxImage = (function JpxImageClosure() { tbx1: codeblockWidth * (i + 1), tby1: codeblockHeight * (j + 1) }; - // calculate precinct number - var pi = Math.floor((codeblock.tbx0 - - precinctParameters.precinctXOffset) / - precinctParameters.precinctWidth); - var pj = Math.floor((codeblock.tby0 - - precinctParameters.precinctYOffset) / - precinctParameters.precinctHeight); - precinctNumber = pj + pi * precinctParameters.numprecinctswide; + codeblock.tbx0_ = Math.max(subband.tbx0, codeblock.tbx0); codeblock.tby0_ = Math.max(subband.tby0, codeblock.tby0); codeblock.tbx1_ = Math.min(subband.tbx1, codeblock.tbx1); codeblock.tby1_ = Math.min(subband.tby1, codeblock.tby1); + + // Calculate precinct number for this codeblock, codeblock position + // should be relative to its subband, use actual dimension and position + // See comment about codeblock group width and height + var pi = Math.floor((codeblock.tbx0_ - subband.tbx0) / + precinctParameters.precinctWidthInSubband); + var pj = Math.floor((codeblock.tby0_ - subband.tby0) / + precinctParameters.precinctHeightInSubband); + precinctNumber = pi + (pj * precinctParameters.numprecinctswide); + codeblock.precinctNumber = precinctNumber; codeblock.subbandType = subband.type; codeblock.Lblock = 3; + + if (codeblock.tbx1_ <= codeblock.tbx0_ || + codeblock.tby1_ <= codeblock.tby0_) { + continue; + } codeblocks.push(codeblock); // building precinct for the sub-band var precinct = precincts[precinctNumber]; @@ -35735,6 +35893,230 @@ var JpxImage = (function JpxImageClosure() { throw new Error('JPX Error: Out of packets'); }; } + function ResolutionPositionComponentLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var l, r, c, p; + var maxDecompositionLevelsCount = 0; + for (c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount, + component.codingStyleParameters.decompositionLevelsCount); + } + var maxNumPrecinctsInLevel = new Int32Array( + maxDecompositionLevelsCount + 1); + for (r = 0; r <= maxDecompositionLevelsCount; ++r) { + var maxNumPrecincts = 0; + for (c = 0; c < componentsCount; ++c) { + var resolutions = tile.components[c].resolutions; + if (r < resolutions.length) { + maxNumPrecincts = Math.max(maxNumPrecincts, + resolutions[r].precinctParameters.numprecincts); + } + } + maxNumPrecinctsInLevel[r] = maxNumPrecincts; + } + l = 0; + r = 0; + c = 0; + p = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.3 Resolution-position-component-layer + for (; r <= maxDecompositionLevelsCount; r++) { + for (; p < maxNumPrecinctsInLevel[r]; p++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + if (r > component.codingStyleParameters.decompositionLevelsCount) { + continue; + } + var resolution = component.resolutions[r]; + var numprecincts = resolution.precinctParameters.numprecincts; + if (p >= numprecincts) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, p, l); + l++; + return packet; + } + l = 0; + } + c = 0; + } + p = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function PositionComponentResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var precinctsIterationSizes = precinctsSizes; + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.4 Position-component-resolution-layer + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = + precinctsSizes.components[c].resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + c = 0; + } + px = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function ComponentPositionResolutionLayerIterator(context) { + var siz = context.SIZ; + var tileIndex = context.currentTile.index; + var tile = context.tiles[tileIndex]; + var layersCount = tile.codingStyleDefaultParameters.layersCount; + var componentsCount = siz.Csiz; + var precinctsSizes = getPrecinctSizesInImageScale(tile); + var l = 0, r = 0, c = 0, px = 0, py = 0; + + this.nextPacket = function JpxImage_nextPacket() { + // Section B.12.1.5 Component-position-resolution-layer + for (; c < componentsCount; ++c) { + var component = tile.components[c]; + var precinctsIterationSizes = precinctsSizes.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + for (; py < precinctsIterationSizes.maxNumHigh; py++) { + for (; px < precinctsIterationSizes.maxNumWide; px++) { + for (; r <= decompositionLevelsCount; r++) { + var resolution = component.resolutions[r]; + var sizeInImageScale = precinctsIterationSizes.resolutions[r]; + var k = getPrecinctIndexIfExist( + px, + py, + sizeInImageScale, + precinctsIterationSizes, + resolution); + if (k === null) { + continue; + } + for (; l < layersCount;) { + var packet = createPacket(resolution, k, l); + l++; + return packet; + } + l = 0; + } + r = 0; + } + px = 0; + } + py = 0; + } + throw new Error('JPX Error: Out of packets'); + }; + } + function getPrecinctIndexIfExist( + pxIndex, pyIndex, sizeInImageScale, precinctIterationSizes, resolution) { + var posX = pxIndex * precinctIterationSizes.minWidth; + var posY = pyIndex * precinctIterationSizes.minHeight; + if (posX % sizeInImageScale.width !== 0 || + posY % sizeInImageScale.height !== 0) { + return null; + } + var startPrecinctRowIndex = + (posY / sizeInImageScale.width) * + resolution.precinctParameters.numprecinctswide; + return (posX / sizeInImageScale.height) + startPrecinctRowIndex; + } + function getPrecinctSizesInImageScale(tile) { + var componentsCount = tile.components.length; + var minWidth = Number.MAX_VALUE; + var minHeight = Number.MAX_VALUE; + var maxNumWide = 0; + var maxNumHigh = 0; + var sizePerComponent = new Array(componentsCount); + for (var c = 0; c < componentsCount; c++) { + var component = tile.components[c]; + var decompositionLevelsCount = + component.codingStyleParameters.decompositionLevelsCount; + var sizePerResolution = new Array(decompositionLevelsCount + 1); + var minWidthCurrentComponent = Number.MAX_VALUE; + var minHeightCurrentComponent = Number.MAX_VALUE; + var maxNumWideCurrentComponent = 0; + var maxNumHighCurrentComponent = 0; + var scale = 1; + for (var r = decompositionLevelsCount; r >= 0; --r) { + var resolution = component.resolutions[r]; + var widthCurrentResolution = + scale * resolution.precinctParameters.precinctWidth; + var heightCurrentResolution = + scale * resolution.precinctParameters.precinctHeight; + minWidthCurrentComponent = Math.min( + minWidthCurrentComponent, + widthCurrentResolution); + minHeightCurrentComponent = Math.min( + minHeightCurrentComponent, + heightCurrentResolution); + maxNumWideCurrentComponent = Math.max(maxNumWideCurrentComponent, + resolution.precinctParameters.numprecinctswide); + maxNumHighCurrentComponent = Math.max(maxNumHighCurrentComponent, + resolution.precinctParameters.numprecinctshigh); + sizePerResolution[r] = { + width: widthCurrentResolution, + height: heightCurrentResolution + }; + scale <<= 1; + } + minWidth = Math.min(minWidth, minWidthCurrentComponent); + minHeight = Math.min(minHeight, minHeightCurrentComponent); + maxNumWide = Math.max(maxNumWide, maxNumWideCurrentComponent); + maxNumHigh = Math.max(maxNumHigh, maxNumHighCurrentComponent); + sizePerComponent[c] = { + resolutions: sizePerResolution, + minWidth: minWidthCurrentComponent, + minHeight: minHeightCurrentComponent, + maxNumWide: maxNumWideCurrentComponent, + maxNumHigh: maxNumHighCurrentComponent + }; + } + return { + components: sizePerComponent, + minWidth: minWidth, + minHeight: minHeight, + maxNumWide: maxNumWide, + maxNumHigh: maxNumHigh + }; + } function buildPackets(context) { var siz = context.SIZ; var tileIndex = context.currentTile.index; @@ -35756,6 +36138,7 @@ var JpxImage = (function JpxImageClosure() { resolution.try0 = Math.ceil(component.tcy0 / scale); resolution.trx1 = Math.ceil(component.tcx1 / scale); resolution.try1 = Math.ceil(component.tcy1 / scale); + resolution.resLevel = r; buildPrecincts(context, resolution, blocksDimensions); resolutions.push(resolution); @@ -35826,6 +36209,18 @@ var JpxImage = (function JpxImageClosure() { tile.packetsIterator = new ResolutionLayerComponentPositionIterator(context); break; + case 2: + tile.packetsIterator = + new ResolutionPositionComponentLayerIterator(context); + break; + case 3: + tile.packetsIterator = + new PositionComponentResolutionLayerIterator(context); + break; + case 4: + tile.packetsIterator = + new ComponentPositionResolutionLayerIterator(context); + break; default: throw new Error('JPX Error: Unsupported progression order ' + progressionOrder); @@ -35853,6 +36248,21 @@ var JpxImage = (function JpxImageClosure() { bufferSize -= count; return (buffer >>> bufferSize) & ((1 << count) - 1); } + function skipMarkerIfEqual(value) { + if (data[offset + position - 1] === 0xFF && + data[offset + position] === value) { + skipBytes(1); + return true; + } else if (data[offset + position] === 0xFF && + data[offset + position + 1] === value) { + skipBytes(2); + return true; + } + return false; + } + function skipBytes(count) { + position += count; + } function alignToByte() { bufferSize = 0; if (skipNextBit) { @@ -35880,11 +36290,17 @@ var JpxImage = (function JpxImageClosure() { } var tileIndex = context.currentTile.index; var tile = context.tiles[tileIndex]; + var sopMarkerUsed = context.COD.sopMarkerUsed; + var ephMarkerUsed = context.COD.ephMarkerUsed; var packetsIterator = tile.packetsIterator; while (position < dataLength) { + alignToByte(); + if (sopMarkerUsed && skipMarkerIfEqual(0x91)) { + // Skip also marker segment length and packet sequence ID + skipBytes(4); + } var packet = packetsIterator.nextPacket(); if (!readBits(1)) { - alignToByte(); continue; } var layerNumber = packet.layerNumber; @@ -35897,13 +36313,13 @@ var JpxImage = (function JpxImageClosure() { var codeblockIncluded = false; var firstTimeInclusion = false; var valueReady; - if ('included' in codeblock) { + if (codeblock['included'] !== undefined) { codeblockIncluded = !!readBits(1); } else { // reading inclusion tree precinct = codeblock.precinct; var inclusionTree, zeroBitPlanesTree; - if ('inclusionTree' in precinct) { + if (precinct['inclusionTree'] !== undefined) { inclusionTree = precinct.inclusionTree; } else { // building inclusion and zero bit-planes trees @@ -35965,10 +36381,13 @@ var JpxImage = (function JpxImageClosure() { }); } alignToByte(); + if (ephMarkerUsed) { + skipMarkerIfEqual(0x92); + } while (queue.length > 0) { var packetItem = queue.shift(); codeblock = packetItem.codeblock; - if (!('data' in codeblock)) { + if (codeblock['data'] === undefined) { codeblock.data = []; } codeblock.data.push({ @@ -35998,7 +36417,7 @@ var JpxImage = (function JpxImageClosure() { if (blockWidth === 0 || blockHeight === 0) { continue; } - if (!('data' in codeblock)) { + if (codeblock['data'] === undefined) { continue; } @@ -36253,10 +36672,10 @@ var JpxImage = (function JpxImageClosure() { var tile = context.tiles[tileIndex]; for (var c = 0; c < componentsCount; c++) { var component = tile.components[c]; - var qcdOrQcc = (c in context.currentTile.QCC ? + var qcdOrQcc = (context.currentTile.QCC[c] !== undefined ? context.currentTile.QCC[c] : context.currentTile.QCD); component.quantizationParameters = qcdOrQcc; - var codOrCoc = (c in context.currentTile.COC ? + var codOrCoc = (context.currentTile.COC[c] !== undefined ? context.currentTile.COC[c] : context.currentTile.COD); component.codingStyleParameters = codOrCoc; } @@ -36285,7 +36704,7 @@ var JpxImage = (function JpxImageClosure() { while (currentLevel < this.levels.length) { level = this.levels[currentLevel]; var index = i + j * level.width; - if (index in level.items) { + if (level.items[index] !== undefined) { value = level.items[index]; break; } diff --git a/static/js/libs/webfont.js b/static/js/libs/webfont.js new file mode 100644 index 00000000..1f514ea7 --- /dev/null +++ b/static/js/libs/webfont.js @@ -0,0 +1,30 @@ +/* Web Font Loader v1.5.18 - (c) Adobe Systems, Google. License: Apache 2.0 */ +;(function(window,document,undefined){function aa(a,b,c){return a.call.apply(a.bind,arguments)}function ba(a,b,c){if(!a)throw Error();if(2a.c||this.c===a.c&&this.g>a.g||this.c===a.c&&this.g===a.g&&this.D>a.D?1:this.cd.c||536==d.c&&11>d.g))} +function E(a,b,c){return(a=a.match(b))&&a[c]?a[c]:""};function G(a){this.ma=a||"-"}G.prototype.e=function(a){for(var b=[],c=0;c=a.X?a.m.ga&&R(a,b,c)&&(null===a.ca||a.ca.hasOwnProperty(a.o.getName()))?S(a,a.$):S(a,a.ka):ja(a):S(a,a.$)}function ja(a){setTimeout(k(function(){Q(this)},a),50)}function S(a,b){a.t.remove();a.u.remove();a.H.remove();b(a.o)};function T(a,b,c,d){this.d=b;this.A=c;this.S=0;this.ea=this.ba=!1;this.X=d;this.m=a.m}function ka(a,b,c,d,e){c=c||{};if(0===b.length&&e)J(a.A);else for(a.S+=b.length,e&&(a.ba=e),e=0;e. + * + */ + +"use strict"; +define([], function() { + + // Dummy stream implementation. + var DummyStream = function(id) { + this.id = id ? id : "defaultDummyStream"; + }; + DummyStream.prototype.stop = function() {}; + DummyStream.prototype.getAudioTracks = function() { return [] }; + DummyStream.prototype.getVideoTracks = function() { return [] }; + DummyStream.not = function(stream) { + // Helper to test if stream is a dummy. + return !stream || stream.stop !== DummyStream.prototype.stop; + }; + DummyStream.is = function(stream) { + return stream && stream.stop === DummyStream.prototype.stop; + }; + + return DummyStream; + +}); \ No newline at end of file diff --git a/static/js/mediastream/peercall.js b/static/js/mediastream/peercall.js index 27055cc1..97a09f3f 100644 --- a/static/js/mediastream/peercall.js +++ b/static/js/mediastream/peercall.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -142,13 +142,19 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection // reason we always trigger onRemoteStream added for all streams which are available // after the remote SDP was set successfully. _.defer(_.bind(function() { + var streams = 0; _.each(peerconnection.getRemoteStreams(), _.bind(function(stream) { if (!this.streams.hasOwnProperty(stream.id) && (stream.getAudioTracks().length > 0 || stream.getVideoTracks().length > 0)) { // NOTE(longsleep): Add stream here when it has at least one audio or video track, to avoid FF >= 33 to add it multiple times. console.log("Adding stream after remote SDP success.", stream); this.onRemoteStreamAdded(stream); + streams++; } }, this)); + if (streams === 0 && this.sdpConstraints.mandatory && (this.sdpConstraints.mandatory.OfferToReceiveAudio || this.sdpConstraints.mandatory.OfferToReceiveVideo)) { + // We assume that we will eventually receive a stream, so we trigger the event to let the UI prepare for it. + this.e.triggerHandler("remoteStreamAdded", [null, this]); + } }, this)); }, this), _.bind(function(err) { console.error("Set remote session description failed", err); @@ -165,6 +171,8 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection sessionDescription.sdp = utils.maybePreferVideoReceiveCodec(sessionDescription.sdp, params); sessionDescription.sdp = utils.maybeSetAudioReceiveBitRate(sessionDescription.sdp, params); sessionDescription.sdp = utils.maybeSetVideoReceiveBitRate(sessionDescription.sdp, params); + // Apply workarounds. + sessionDescription.sdp = utils.fixLocal(sessionDescription.sdp, params); }; @@ -177,6 +185,8 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection sessionDescription.sdp = utils.maybeSetAudioSendBitRate(sessionDescription.sdp, params); sessionDescription.sdp = utils.maybeSetVideoSendBitRate(sessionDescription.sdp, params); sessionDescription.sdp = utils.maybeSetVideoSendInitialBitRate(sessionDescription.sdp, params); + // Apply workarounds. + sessionDescription.sdp = utils.fixRemote(sessionDescription.sdp, params); }; diff --git a/static/js/mediastream/peerconference.js b/static/js/mediastream/peerconference.js index 0f5aa805..a5500274 100644 --- a/static/js/mediastream/peerconference.js +++ b/static/js/mediastream/peerconference.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -40,6 +40,12 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall this.id = id; } + this.usermedia = webrtc.usermedia; + webrtc.e.on("usermedia", _.bind(function(event, um) { + console.log("Conference user media changed", um); + this.usermedia = um; + }, this)); + console.log("Created conference", this.id); }; @@ -94,16 +100,12 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall console.log("Creating PeerConnection", call); call.createPeerConnection(_.bind(function(peerconnection) { // Success call. + if (this.usermedia) { + this.usermedia.addToPeerConnection(peerconnection); + } call.e.on("negotiationNeeded", _.bind(function(event, extracall) { this.webrtc.sendOfferWhenNegotiationNeeded(extracall); }, this)); - if (this.webrtc.usermedia) { - this.webrtc.usermedia.addToPeerConnection(peerconnection); - } - /*call.createOffer(_.bind(function(sessionDescription, extracall) { - console.log("Sending offer with sessionDescription", sessionDescription, extracall.id); - this.webrtc.api.sendOffer(extracall.id, sessionDescription); - }, this));*/ }, this), _.bind(function() { // Error call. console.error("Failed to create peer connection for conference call."); @@ -143,9 +145,12 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall call.createPeerConnection(_.bind(function(peerconnection) { // Success call. call.setRemoteDescription(rtcsdp, _.bind(function() { - if (this.webrtc.usermedia) { - this.webrtc.usermedia.addToPeerConnection(peerconnection); + if (this.usermedia) { + this.usermedia.addToPeerConnection(peerconnection); } + call.e.on("negotiationNeeded", _.bind(function(event, extracall) { + this.webrtc.sendOfferWhenNegotiationNeeded(extracall); + }, this)); call.createAnswer(_.bind(function(sessionDescription, extracall) { console.log("Sending answer", sessionDescription, extracall.id); this.webrtc.api.sendAnswer(extracall.id, sessionDescription); diff --git a/static/js/mediastream/peerconnection.js b/static/js/mediastream/peerconnection.js index 61df5d12..24b36580 100644 --- a/static/js/mediastream/peerconnection.js +++ b/static/js/mediastream/peerconnection.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -72,10 +72,16 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) { // for example https://bugzilla.mozilla.org/show_bug.cgi?id=998546. pc.onaddstream = _.bind(this.onRemoteStreamAdded, this); pc.onremovestream = _.bind(this.onRemoteStreamRemoved, this); + // NOTE(longsleep): Firefox 38 has support for onaddtrack. Unfortunately Chrome does + // not support this and thus both are not compatible. For the time being this means + // that renegotiation does not work between Firefox and Chrome. Even worse, current + // spec says that the event should really be named ontrack. if (window.webrtcDetectedBrowser === "firefox") { - // NOTE(longsleep): onnegotiationneeded is not supported by Firefox. We trigger it + // NOTE(longsleep): onnegotiationneeded is not supported by Firefox < 38. + // Also firefox does not care about streams, but has the newer API for tracks + // implemented. This does not work together with Chrome, so we trigger negotiation // manually when a stream is added or removed. - // https://bugzilla.mozilla.org/show_bug.cgi?id=840728 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1017888 this.negotiationNeeded = _.bind(function() { if (this.currentcall.initiate) { // Trigger onNegotiationNeeded once for Firefox. @@ -262,6 +268,16 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) { }; + PeerConnection.prototype.hasRemoteDescription = function() { + + // NOTE(longsleep): Chrome seems to return empty sdp even if no remoteDescription was set. + if (!this.pc || !this.pc.remoteDescription || !this.pc.remoteDescription.sdp) { + return false + } + return true; + + }; + PeerConnection.prototype.setRemoteDescription = function() { return this.pc.setRemoteDescription.apply(this.pc, arguments); @@ -308,12 +324,18 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) { PeerConnection.prototype.getRemoteStreams = function() { + if (!this.pc) { + return []; + } return this.pc.getRemoteStreams.apply(this.pc, arguments); }; PeerConnection.prototype.getLocalStreams = function() { + if (!this.pc) { + return []; + } return this.pc.getRemoteStreams.apply(this.pc, arguments); }; diff --git a/static/js/mediastream/peerscreenshare.js b/static/js/mediastream/peerscreenshare.js index ec5fbf1b..a38a8f15 100644 --- a/static/js/mediastream/peerscreenshare.js +++ b/static/js/mediastream/peerscreenshare.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/mediastream/peerxfer.js b/static/js/mediastream/peerxfer.js index bdc3dfb9..bba8d6e7 100644 --- a/static/js/mediastream/peerxfer.js +++ b/static/js/mediastream/peerxfer.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/mediastream/tokens.js b/static/js/mediastream/tokens.js index 1bf48dfc..bf3dc98d 100644 --- a/static/js/mediastream/tokens.js +++ b/static/js/mediastream/tokens.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/mediastream/usermedia.js b/static/js/mediastream/usermedia.js index ebc7dc17..2c9b1795 100644 --- a/static/js/mediastream/usermedia.js +++ b/static/js/mediastream/usermedia.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,16 +20,106 @@ */ "use strict"; -define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _, AudioContext) { +define(['jquery', 'underscore', 'audiocontext', 'mediastream/dummystream', 'webrtc.adapter'], function($, _, AudioContext, DummyStream) { // Create AudioContext singleton, if supported. var context = AudioContext ? new AudioContext() : null; - var peerconnections = {}; - // Disabled for now until browser support matures. If enabled this totally breaks - // Firefox and Chrome with Firefox interop. - var enableRenegotiationSupport = false; + // Converter helpers to convert media constraints to new API. + var mergeConstraints = function(constraints, k, v, mandatory) { + var prefix = k.substring(0, 3); + switch (prefix) { + case "min": + case "max": + var suffix = k[3].toLowerCase()+k.substring(4); + if (!constraints.hasOwnProperty(suffix)) { + constraints[suffix]={}; + } + if (mandatory && prefix === "min" && constraints[suffix].hasOwnProperty(prefix)) { + // Use existing min constraint as ideal. + constraints[suffix].ideal = constraints[suffix].min; + } + constraints[suffix][prefix]=v; + break; + default: + constraints[k] = v; + break; + } + }; + var convertConstraints = function(constraints) { + if (!constraints) { + return false; + } + if (!constraints.hasOwnProperty("optional") && !constraints.hasOwnProperty("mandatory")) { + // No old style members. + return constraints; + } + var c = {}; + // Process optional constraints. + if (constraints.optional) { + _.each(constraints.optional, function(o) { + _.each(o, function(v, k) { + mergeConstraints(c, k, v); + }) + }); + } + // Process mandatory constraints. + if (constraints.mandatory) { + _.each(constraints.mandatory, function(v, k) { + mergeConstraints(c, k, v, true); + }); + } + // Fastpath. + if (_.isEmpty(c)) { + return true; + } + // Use ideal if there is only one value set. + _.each(c, function(v, k) { + if (_.isObject(v)) { + var values = _.values(v); + if (values.length === 1) { + // Use as ideal value if only one given. + c[k] = {ideal: values[0]}; + } + } + }); + if (window.webrtcDetectedBrowser === "firefox" && window.webrtcDetectedVersion < 38) { + // Firefox < 38 needs a extra require field. + var r = []; + if (c.height) { + r.push("height"); + } + if (c.width) { + r.push("width"); + } + if (r.length) { + c.require = r; + } + } + return c; + }; + // Adapter to support navigator.mediaDevices API. + // http://w3c.github.io/mediacapture-main/getusermedia.html#mediadevices + var getUserMedia = (function() { + if (window.navigator.mediaDevices) { + console.info("Enabled mediaDevices adapter ..."); + return function(constraints, success, error) { + // Full constraints syntax with plain values and ideal-algorithm supported in FF38+. + // Note on FF32-37: Plain values and ideal are not supported. + // See https://wiki.mozilla.org/Media/getUserMedia for details. + // Examples here: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia + var c = {audio: convertConstraints(constraints.audio), video: convertConstraints(constraints.video)}; + // mediaDevices API returns a promise. + console.log("Constraints for mediaDevices", c); + window.navigator.mediaDevices.getUserMedia(c).then(success).catch(error); + } + } else { + // Use existing adapter. + return window.getUserMedia; + } + })(); + // UserMedia. var UserMedia = function(options) { this.options = $.extend({}, options); @@ -37,15 +127,23 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ this.localStream = null; this.started = false; - this.delay = 0; - this.audioMute = false; - this.videoMute = false; + this.peerconnections = {}; + + // If true, mute/unmute of audio/video creates a new stream which + // will trigger renegotiation on the peer connection. + this.renegotiation = options.renegotiation && true; + if (this.renegotiation) { + console.info("User media with renegotiation created ..."); + } + + this.audioMute = options.audioMute && true; + this.videoMute = options.videoMute && true; this.mediaConstraints = null; // Audio level. this.audioLevel = 0; - if (!this.options.noaudio && context && context.createScriptProcessor) { + if (!this.options.noAudio && context && context.createScriptProcessor) { this.audioSource = null; this.audioProcessor = context.createScriptProcessor(2048, 1, 1); @@ -73,6 +171,11 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ if (this.audioSource) { this.audioSource.disconnect(); } + var audioTracks = stream.getAudioTracks(); + if (audioTracks.length < 1) { + this.audioSource = null; + return; + } // Connect to audioProcessor. this.audioSource = context.createMediaStreamSource(stream); //console.log("got source", this.audioSource); @@ -86,9 +189,13 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ this.e.on("localstream", _.bind(function(event, stream, oldstream) { // Update stream support. if (oldstream) { - _.each(peerconnections, function(pc) { - pc.removeStream(oldstream); - pc.addStream(stream); + _.each(this.peerconnections, function(pc) { + if (DummyStream.not(oldstream)) { + pc.removeStream(oldstream); + } + if (DummyStream.not(stream)) { + pc.addStream(stream); + } console.log("Updated usermedia stream at peer connection", pc, stream); }); } @@ -128,7 +235,7 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ error_cb.apply(this, args); }; try { - window.getUserMedia({ + getUserMedia({ video: true, audio: true }, success_helper, error_helper); @@ -150,7 +257,6 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ if (!mediaConstraints) { mediaConstraints = currentcall.mediaConstraints; } - this.mediaConstraints = mediaConstraints; return this.doGetUserMediaWithConstraints(mediaConstraints); @@ -160,6 +266,21 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ if (!mediaConstraints) { mediaConstraints = this.mediaConstraints; + } else { + this.mediaConstraints = mediaConstraints; + if (this.localStream) { + // Release stream early if any to be able to apply new constraints. + this.replaceStream(null); + } + } + + if (this.renegotiation && this.audioMute && this.videoMute) { + // Fast path as nothing should be shared. + _.defer(_.bind(function() { + this.onUserMediaSuccess(new DummyStream()); + }, this)); + this.started = true; + return true } var constraints = $.extend(true, {}, mediaConstraints); @@ -173,7 +294,7 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ try { console.log('Requesting access to local media with mediaConstraints:\n' + ' \'' + JSON.stringify(constraints) + '\'', constraints); - window.getUserMedia(constraints, _.bind(this.onUserMediaSuccess, this), _.bind(this.onUserMediaError, this)); + getUserMedia(constraints, _.bind(this.onUserMediaSuccess, this), _.bind(this.onUserMediaError, this)); this.started = true; return true; } catch (e) { @@ -207,34 +328,48 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ }; - UserMedia.prototype.onLocalStream = function(stream) { + UserMedia.prototype.replaceStream = function(stream) { var oldStream = this.localStream; - if (oldStream) { - oldStream.onended = function() {}; + if (oldStream && oldStream.active) { + // Let old stream silently end. + oldStream.onended = function() { + console.log("Silently ended replaced user media stream."); + }; oldStream.stop(); + } + + if (stream) { + // Get notified of end events. + stream.onended = _.bind(function(event) { + console.log("User media stream ended."); + if (this.started) { + this.stop(); + } + }, this); + // Set new stream. + this.localStream = stream; + this.e.triggerHandler("localstream", [stream, oldStream, this]); + } + + return oldStream && stream; + + }; + + UserMedia.prototype.onLocalStream = function(stream) { + + if (this.replaceStream(stream)) { + // We replaced a stream. setTimeout(_.bind(function() { this.e.triggerHandler("mediachanged", [this]); }, this), 0); } else { - // Let webrtc handle the rest. + // We are new. setTimeout(_.bind(function() { this.e.triggerHandler("mediasuccess", [this]); - }, this), this.delay); + }, this), 0); } - // Get notified of end events. - stream.onended = _.bind(function(event) { - console.log("User media stream ended."); - if (this.started) { - this.stop(); - } - }, this); - - // Set new stream. - this.localStream = stream; - this.e.triggerHandler("localstream", [stream, oldStream, this]); - }; UserMedia.prototype.stop = function() { @@ -258,11 +393,7 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ this.mediaConstraints = null; console.log("Stopped user media."); this.e.triggerHandler("stopped", [this]); - - this.delay = 1500; - setTimeout(_.bind(function() { - this.delay = 0; - }, this), 2000); + this.e.off(); }; @@ -270,7 +401,7 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ var m = !!mute; - if (!enableRenegotiationSupport) { + if (!this.renegotiation) { // Disable streams only - does not require renegotiation but keeps mic // active and the stream will transmit silence. @@ -300,7 +431,7 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ // Remove audio stream, by creating a new stream and doing renegotiation. This // is the way to go to disable the mic when audio is muted. - if (this.localStream) { + if (this.started) { if (this.audioMute !== m) { this.audioMute = m; this.doGetUserMediaWithConstraints(); @@ -319,7 +450,7 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ var m = !!mute; - if (!enableRenegotiationSupport) { + if (!this.renegotiation) { // Disable streams only - does not require renegotiation but keeps camera // active and the stream will transmit black. @@ -344,10 +475,10 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ } } else { - // Removevideo stream, by creating a new stream and doing renegotiation. This + // Remove video stream, by creating a new stream and doing renegotiation. This // is the way to go to disable the camera when video is muted. - if (this.localStream) { + if (this.started) { if (this.videoMute !== m) { this.videoMute = m; this.doGetUserMediaWithConstraints(); @@ -366,13 +497,15 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ console.log("Add usermedia stream to peer connection", pc, this.localStream); if (this.localStream) { - pc.addStream(this.localStream); + if (DummyStream.not(this.localStream)) { + pc.addStream(this.localStream); + } var id = pc.id; - if (!peerconnections.hasOwnProperty(id)) { - peerconnections[id] = pc; - pc.currentcall.e.one("closed", function() { - delete peerconnections[id]; - }); + if (!this.peerconnections.hasOwnProperty(id)) { + this.peerconnections[id] = pc; + pc.currentcall.e.one("closed", _.bind(function() { + delete this.peerconnections[id]; + }, this)); } } @@ -382,9 +515,11 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ console.log("Remove usermedia stream from peer connection", pc, this.localStream); if (this.localStream) { - pc.removeStream(this.localStream); - if (peerconnections.hasOwnProperty(pc.id)) { - delete peerconnections[pc.id]; + if (DummyStream.not(this.localStream)) { + pc.removeStream(this.localStream); + } + if (this.peerconnections.hasOwnProperty(pc.id)) { + delete this.peerconnections[pc.id]; } } @@ -392,7 +527,9 @@ define(['jquery', 'underscore', 'audiocontext', 'webrtc.adapter'], function($, _ UserMedia.prototype.attachMediaStream = function(video) { - window.attachMediaStream(video, this.localStream); + if (this.localStream && DummyStream.not(this.localStream)) { + window.attachMediaStream(video, this.localStream); + } }; diff --git a/static/js/mediastream/utils.js b/static/js/mediastream/utils.js index 9f41acda..d5bd62b1 100644 --- a/static/js/mediastream/utils.js +++ b/static/js/mediastream/utils.js @@ -81,6 +81,14 @@ define([], function() { sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec'); } + // Set Opus DTX, if opusdtx is true, unset it, if opusdtx is false, and + // do nothing if otherwise. + if (params.opusDtx === 'true') { + sdp = setCodecParam(sdp, 'opus/48000', 'usedtx', '1'); + } else if (params.opusDtx === 'false') { + sdp = removeCodecParam(sdp, 'opus/48000', 'usedtx'); + } + // Set Opus maxplaybackrate, if requested. if (params.opusMaxPbr) { sdp = setCodecParam( @@ -417,7 +425,21 @@ define([], function() { maybePreferAudioSendCodec: maybePreferAudioSendCodec, maybePreferAudioReceiveCodec: maybePreferAudioReceiveCodec, maybePreferVideoSendCodec: maybePreferVideoSendCodec, - maybePreferVideoReceiveCodec: maybePreferVideoReceiveCodec + maybePreferVideoReceiveCodec: maybePreferVideoReceiveCodec, + fixLocal: function(sdp) { + if (window.webrtcDetectedBrowser === "chrome") { + // Remove all rtx support from locally generated sdp. Chrome + // does create this sometimes wrong. + // TODO(longsleep): Limit to Chrome version, once it is fixed upstream. + // See https://code.google.com/p/webrtc/issues/detail?id=3962 + sdp = sdp.replace(/a=rtpmap:\d+ rtx\/\d+\r\n/i, ""); + sdp = sdp.replace(/a=fmtp:\d+ apt=\d+\r\n/i, ""); + } + return sdp; + }, + fixRemote: function(sdp) { + return sdp; + } } }); diff --git a/static/js/mediastream/webrtc.js b/static/js/mediastream/webrtc.js index 28298fa3..946bbae0 100644 --- a/static/js/mediastream/webrtc.js +++ b/static/js/mediastream/webrtc.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -54,6 +54,8 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u this.started = false; this.initiator = null; + + this.usermedia = null; this.audioMute = false; this.videoMute = false; @@ -112,7 +114,8 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u videoSendCodec: "VP8/90000" //videoRecvBitrate: , //videoRecvCodec - } + }, + renegotiation: true }; this.screensharingSettings = { @@ -121,17 +124,6 @@ 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)); - // Create default media (audio/video). - this.usermedia = new UserMedia(); - this.usermedia.e.on("mediasuccess mediaerror", _.bind(function() { - // Start always, no matter what. - this.maybeStart(); - }, this)); - this.usermedia.e.on("mediachanged", _.bind(function() { - // Propagate media change events. - this.e.triggerHandler("usermedia", [this.usermedia]); - }, this)); - }; WebRTC.prototype.processReceived = function(event, to, data, type, to2, from) { @@ -245,6 +237,12 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u console.log("Offer process."); targetcall = this.findTargetCall(from); if (targetcall) { + if (!this.settings.renegotiation && targetcall.peerconnection && targetcall.peerconnection.hasRemoteDescription()) { + // Call replace support without renegotiation. + this.doHangup("unsupported", from); + console.error("Processing new offers is not implemented without renegotiation."); + return; + } // Hey we know this call. targetcall.setRemoteDescription(new window.RTCSessionDescription(data), _.bind(function(sessionDescription, currentcall) { if (currentcall === this.currentcall) { @@ -396,6 +394,39 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u }; + WebRTC.prototype.doUserMedia = function(currentcall) { + + // Create default media (audio/video). + var usermedia = new UserMedia({ + renegotiation: this.settings.renegotiation, + audioMute: this.audioMute, + videoMute: this.videoMute + }); + usermedia.e.on("mediasuccess mediaerror", _.bind(function(event, um) { + this.e.triggerHandler("usermedia", [um]); + // Start always, no matter what. + this.maybeStart(um); + }, this)); + usermedia.e.on("mediachanged", _.bind(function(event, um) { + // Propagate media change events. + this.e.triggerHandler("usermedia", [um]); + }, this)); + usermedia.e.on("stopped", _.bind(function(event, um) { + if (um === this.usermedia) { + this.e.triggerHandler("usermedia", [null]); + this.usermedia = null; + } + }, this)); + this.e.one("stop", function() { + usermedia.stop(); + }); + this.usermedia = usermedia; + this.e.triggerHandler("usermedia", [usermedia]); + + return usermedia.doGetUserMedia(currentcall); + + }; + WebRTC.prototype.doCall = function(id) { if (this.currentcall) { @@ -413,7 +444,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u } else { var currentcall = this.currentcall = this.createCall(id, null, id); this.e.triggerHandler("peercall", [currentcall]); - var ok = this.usermedia.doGetUserMedia(currentcall); + var ok = this.doUserMedia(currentcall); if (ok) { this.e.triggerHandler("waitforusermedia", [currentcall]); } else { @@ -432,7 +463,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u console.warn("Trying to accept without a call.", currentcall); return; } - var ok = this.usermedia.doGetUserMedia(currentcall); + var ok = this.doUserMedia(currentcall); if (ok) { this.e.triggerHandler("waitforusermedia", [currentcall]); } else { @@ -501,7 +532,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u WebRTC.prototype.doScreenshare = function(options) { var usermedia = new UserMedia({ - noaudio: true + noAudio: true }); var ok = usermedia.doGetUserMedia(null, PeerScreenshare.getCaptureMediaConstraints(this, options)); if (ok) { @@ -579,11 +610,9 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u this.currentcall.close(); this.currentcall = null; } - if (this.usermedia) { - this.usermedia.stop(); - } this.e.triggerHandler("peerconference", [null]); this.e.triggerHandler("peercall", [null]); + this.e.triggerHandler("stop"); this.msgQueue.length = 0; this.initiator = null; this.started = false; @@ -629,7 +658,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u } - WebRTC.prototype.maybeStart = function() { + WebRTC.prototype.maybeStart = function(usermedia) { //console.log("maybeStart", this.started); if (!this.started) { @@ -640,14 +669,7 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u console.log('Creating PeerConnection.', currentcall); currentcall.createPeerConnection(_.bind(function(peerconnection) { // Success call. - if (this.usermedia) { - this.usermedia.applyVideoMute(this.videoMute); - this.usermedia.applyAudioMute(this.audioMute); - this.e.triggerHandler("usermedia", [this.usermedia]); - this.usermedia.addToPeerConnection(peerconnection); - } else { - _.defer(peerconnection.negotiationNeeded); - } + usermedia.addToPeerConnection(peerconnection); this.started = true; if (!this.initiator) { this.calleeStart(); diff --git a/static/js/modules/angular-humanize.js b/static/js/modules/angular-humanize.js index 822afd56..aeeae8b7 100644 --- a/static/js/modules/angular-humanize.js +++ b/static/js/modules/angular-humanize.js @@ -1,6 +1,6 @@ /* * @license angular-humanize - * Copyright 2013-2014 struktur AG, http://www.struktur.de + * Copyright 2013-2015 struktur AG, http://www.struktur.de * License: MIT */ (function(window, angular, humanize, undefined) { diff --git a/static/js/sandboxes/pdf.js b/static/js/sandboxes/pdf.js new file mode 100644 index 00000000..d38f45cd --- /dev/null +++ b/static/js/sandboxes/pdf.js @@ -0,0 +1,281 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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"; +(function () { + + var script = document.getElementsByTagName("script")[0]; + var PARENT_ORIGIN = script.getAttribute("data-parent-origin"); + var PDFJS_URL = script.getAttribute("data-pdfjs-url"); + var PDFJS_WORKER_URL = script.getAttribute("data-pdfjs-worker-url"); + var PDFJS_COMPATIBILITY_URL = script.getAttribute("data-pdfjs-compatibility-url"); + var container = document.getElementById("container"); + + var pdfScript = null; + var pdfjs = null; + + var PdfJsSandbox = function(window) { + this.head = document.getElementsByTagName('head')[0]; + this.canvases = document.getElementsByTagName('canvas'); + this.window = window; + this.doc = null; + this.currentPage = null; + this.canvasIndex = 0; + this.renderTask = null; + }; + + PdfJsSandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type}; + msg[type] = message; + this.window.parent.postMessage(msg, PARENT_ORIGIN); + }; + + PdfJsSandbox.prototype.openFile = function(source) { + if (!pdfScript) { + var that = this; + var compat = document.createElement('script'); + compat.type = "text/javascript"; + compat.src = PDFJS_COMPATIBILITY_URL; + this.head.appendChild(compat); + + pdfScript = document.createElement('script'); + pdfScript.type = "text/javascript"; + pdfScript.src = PDFJS_URL; + pdfScript.onerror = function(evt) { + that.postMessage("pdfjs.error", {"msgid": "loadScriptFailed"}); + that.head.removeChild(pdfScript); + pdfScript = null; + }; + pdfScript.onload = function(evt) { + pdfjs = that.window.PDFJS; + if (PDFJS_WORKER_URL) { + // NOTE: the worker script won't actually be run inside a + // real Worker object as it can't be loaded cross-domain + // from the sandboxed iframe ("data:" vs. "https"). + pdfjs.workerSrc = PDFJS_WORKER_URL; + } + console.log("Using pdf.js " + pdfjs.version + " (build " + pdfjs.build + ")"); + that._doOpenFile(source); + }; + this.head.appendChild(pdfScript); + } else { + this._doOpenFile(source); + } + }; + + PdfJsSandbox.prototype.closeFile = function() { + this._stopRendering(); + if (this.currentPage) { + this.currentPage.destroy(); + this.currentPage = null; + } + if (this.doc) { + this.doc.cleanup(); + this.doc.destroy(); + this.doc = null; + } + // clear visible canvas so it's empty when we show the next document + var canvas = this.canvases[this.canvasIndex]; + canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); + }; + + PdfJsSandbox.prototype._doOpenFile = function(source) { + var that = this; + this.postMessage("pdfjs.loading", {"source": source}); + pdfjs.getDocument(source).then(function(doc) { + that._pdfLoaded(source, doc); + }, function(error, exception) { + that._pdfLoadError(source, error, exception); + }); + }; + + PdfJsSandbox.prototype._pdfLoaded = function(source, doc) { + console.log("PDF loaded", doc); + this.doc = doc; + this.postMessage("pdfjs.loaded", {"source": source, "doc": {"numPages": doc.numPages}}); + }; + + PdfJsSandbox.prototype._pdfLoadError = function(source, error, exception) { + this.postMessage("pdfjs.loadError", {"source": source, "error": error}); + }; + + PdfJsSandbox.prototype.loadPage = function(page) { + if (this.currentPage) { + this.currentPage.destroy(); + this.currentPage = null; + } + var that = this; + this.doc.getPage(page).then(function(pageObject) { + that._pageLoaded(page, pageObject); + }, function(error, exception) { + that._pageLoadError(page, error, exception); + }); + }; + + PdfJsSandbox.prototype._pageLoaded = function(page, pageObject) { + console.log("Got page", pageObject); + this.currentPage = pageObject; + this.postMessage("pdfjs.pageLoaded", {"page": page}); + this.drawPage(pageObject); + }; + + PdfJsSandbox.prototype._pageLoadError = function(page, error, exception) { + console.error("Could not load page", page, error, exception); + this.postMessage("pdfjs.pageLoadError", {"page": page, "error": error}); + }; + + PdfJsSandbox.prototype._stopRendering = function() { + if (this.renderTask) { + if (this.renderTask.internalRenderTask && this.renderTask.internalRenderTask.cancel) { + this.renderTask.internalRenderTask.cancel(); + } + this.renderTask = null; + } + } + + PdfJsSandbox.prototype.drawPage = function(pageObject) { + var pdfView = pageObject.view; + var pdfWidth = pdfView[2] - pdfView[0]; + var pdfHeight = pdfView[3] - pdfView[1]; + var w = container.offsetWidth; + var h = container.offsetHeight; + var scale = w / pdfWidth; + if (pdfHeight * scale > h) { + scale = container.offsetHeight / pdfHeight; + } + + // use double-buffering to avoid flickering while + // the new page is rendered... + var canvas = this.canvases[1 - this.canvasIndex]; + var viewport = pageObject.getViewport(scale); + canvas.width = Math.round(viewport.width); + canvas.height = Math.round(viewport.height); + var renderContext = { + canvasContext: canvas.getContext("2d"), + viewport: viewport + }; + + console.log("Rendering page", pageObject); + this.postMessage("pdfjs.renderingPage", {"page": pageObject.pageNumber}); + + // TODO(fancycode): also render images in different resolutions for subscribed peers and send to them when ready + this._stopRendering(); + var renderTask = pageObject.render(renderContext); + this.renderTask = renderTask; + var that = this; + renderTask.promise.then(function() { + that._pageRendered(pageObject); + }, function(error, exception) { + that._pageRenderError(pageObject, error, exception); + }); + }; + + PdfJsSandbox.prototype._pageRendered = function(pageObject) { + this.renderTask = null; + console.log("Rendered page", pageObject.pageNumber); + this.postMessage("pdfjs.pageRendered", {"page": pageObject.pageNumber}); + // ...and flip the buffers... + this.canvases[this.canvasIndex].style.display = "none"; + this.canvasIndex = 1 - this.canvasIndex; + this.canvases[this.canvasIndex].style.display = "block"; + }; + + PdfJsSandbox.prototype._pageRenderError = function(pageObject, error, exception) { + if (error === "cancelled") { + return; + } + console.error("Could not render page", pageObject, error, exception); + this.renderTask = null; + this.postMessage("pdfjs.pageRenderError", {"page": pageObject.pageNumber, "error": error}); + }; + + PdfJsSandbox.prototype.redrawPage = function() { + if (this.currentPage !== null) { + this.drawPage(this.currentPage); + } + }; + + var sandbox = new PdfJsSandbox(window); + + window.addEventListener("message", function(event) { + if (event.origin !== PARENT_ORIGIN) { + // only accept messages from spreed-webrtc + return; + } + var msg = event.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "openFile": + sandbox.openFile(data.source); + break; + case "closeFile": + sandbox.closeFile(); + break; + case "loadPage": + sandbox.loadPage(data.page); + break; + case "redrawPage": + sandbox.redrawPage(); + break; + default: + console.log("Unknown message received", event); + break; + } + }, false); + + document.addEventListener("keyup", function(event) { + sandbox.postMessage("pdfjs.keyUp", {"key": event.keyCode}); + event.preventDefault(); + }); + + var toggleFullscreen = function(elem) { + var fullScreenElement = document.fullscreenElement || document.msFullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.webkitCurrentFullScreenElement; + if (fullScreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { + elem.webkitRequestFullscreen(); + } else if (elem.mozRequestFullScreen) { + elem.mozRequestFullScreen(); + } else if (elem.msRequestFullscreen) { + elem.msRequestFullscreen(); + } + } + }; + + container.addEventListener("dblclick", function(event) { + toggleFullscreen(container); + }); + + console.log("pdf.js sandbox ready."); + sandbox.postMessage("ready", {"ready": true}); + +})(); diff --git a/static/js/sandboxes/webodf.js b/static/js/sandboxes/webodf.js new file mode 100644 index 00000000..140a370c --- /dev/null +++ b/static/js/sandboxes/webodf.js @@ -0,0 +1,276 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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"; +(function () { + + var DOCUMENT_TYPE_PRESENTATION = "presentation"; + var DOCUMENT_TYPE_SPREADSHEET = "spreadsheet"; + var DOCUMENT_TYPE_TEXT = "text"; + + var nsResolver = function(prefix) { + var ns = { + 'draw': "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", + 'presentation': "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0", + 'text': "urn:oasis:names:tc:opendocument:xmlns:text:1.0", + 'office': "urn:oasis:names:tc:opendocument:xmlns:office:1.0" + }; + return ns[prefix] || console.log('prefix [' + prefix + '] unknown.'); + } + + var body = document.getElementsByTagName("body")[0]; + var script = document.getElementsByTagName("script")[0]; + var PARENT_ORIGIN = script.getAttribute("data-parent-origin"); + var WEBODF_URL = script.getAttribute("data-webodf-url"); + var container = document.getElementById("container"); + + var webodfScript = null; + var webodf = null; + var runtime = null; + + var ODFCanvas_readFile = function(path, encoding, callback) { + if (typeof path === "string") { + runtime.orig_readFile.call(runtime, path, encoding, callback); + return; + } + + // we're loading typed arrays in the sandbox + callback(null, new Uint8Array(path)); + }; + + var ODFCanvas_loadXML = function(path, callback) { + if (typeof path === "string") { + runtime.orig_loadXML.call(runtime, path, callback); + return; + } + + // we're loading typed arrays in the sandbox + var bb = new Blob([new Uint8Array(path)]); + var f = new FileReader(); + f.onload = function(e) { + var parser = new DOMParser(); + var doc = parser.parseFromString(e.target.result, "text/xml"); + callback(null, doc); + }; + f.readAsText(bb); + }; + + var EmptyFakeStyle = function() { + }; + + EmptyFakeStyle.prototype.getPropertyValue = function(property) { + return null; + } + + var ODFCanvas_getWindow = function() { + var result = runtime.orig_getWindow.apply(runtime, arguments); + var orig_getComputedStyle = result.getComputedStyle + + // Firefox doesn't allow access to some styles, so return a + // fake style for WebODF to use in that case. + result.getComputedStyle = function() { + var style = orig_getComputedStyle.apply(result, arguments); + if (!style) { + style = new EmptyFakeStyle(); + } + return style; + } + + return result; + }; + + var WebODFSandbox = function(window) { + this.head = document.getElementsByTagName('head')[0]; + this.canvasDom = document.getElementById("odfcanvas"); + this.window = window; + this.canvas = null; + this.document_type = null; + }; + + WebODFSandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type}; + msg[type] = message; + this.window.parent.postMessage(msg, PARENT_ORIGIN); + }; + + WebODFSandbox.prototype.openFile = function(source) { + if (!webodfScript) { + var that = this; + webodfScript = document.createElement('script'); + webodfScript.type = "text/javascript"; + webodfScript.src = WEBODF_URL; + webodfScript.onerror = function(evt) { + that.postMessage("webodf.error", {"msgid": "loadScriptFailed"}); + that.head.removeChild(webodfScript); + webodfScript = null; + }; + webodfScript.onload = function(evt) { + console.log("Using webodf.js " + that.window.webodf.Version); + webodf = that.window.odf; + + // monkey-patch IO functions + runtime = that.window.runtime; + runtime.orig_readFile = runtime.readFile; + runtime.readFile = ODFCanvas_readFile; + runtime.orig_loadXML = runtime.loadXML; + runtime.loadXML = ODFCanvas_loadXML; + runtime.orig_getWindow = runtime.getWindow; + runtime.getWindow = ODFCanvas_getWindow; + + that._doOpenFile(source); + }; + this.head.appendChild(webodfScript); + } else { + this._doOpenFile(source); + } + }; + + WebODFSandbox.prototype.closeFile = function() { + if (this.canvas) { + this.canvas.destroy(function() { + // ignore callback + }); + this.canvas = null; + } + }; + + WebODFSandbox.prototype._doOpenFile = function(source) { + this.postMessage("webodf.loading", {"source": source}); + if (!this.canvas) { + var that = this; + this.canvas = new webodf.OdfCanvas(this.canvasDom); + this.canvas.addListener("statereadychange", function() { + that._odfLoaded(); + }); + } + + this.canvas.setZoomLevel(1); + this.canvas.load(source); + }; + + WebODFSandbox.prototype._odfLoaded = function() { + var odfcontainer = this.canvas.odfContainer(); + console.log("ODF loaded", odfcontainer); + this.document_type = odfcontainer.getDocumentType(); + var pages = []; + switch (this.document_type) { + case DOCUMENT_TYPE_PRESENTATION: + container.className += " showonepage"; + pages = odfcontainer.rootElement.getElementsByTagNameNS(nsResolver('draw'), 'page'); + break; + default: + container.className = this.canvasDom.className.replace(/(?:^|\s)showonepage(?!\S)/g, ""); + break; + } + + var numPages = Math.max(1, pages.length); + this.postMessage("webodf.loaded", {"url": odfcontainer.getUrl(), "type": this.document_type, "numPages": numPages}); + }; + + WebODFSandbox.prototype.showPage = function(page) { + this.canvas.showPage(page); + this.redrawPage(); + }; + + WebODFSandbox.prototype.redrawPage = function() { + if (this.canvas) { + switch (this.document_type) { + case DOCUMENT_TYPE_PRESENTATION: + this.canvas.fitToContainingElement(container.offsetWidth, container.offsetHeight); + break; + + default: + this.canvas.fitToWidth(container.offsetWidth); + break; + } + } + }; + + var sandbox = new WebODFSandbox(window); + + window.addEventListener("message", function(event) { + if (event.origin !== PARENT_ORIGIN) { + // only accept messages from spreed-webrtc + return; + } + var msg = event.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "openFile": + sandbox.openFile(data.source); + break; + case "closeFile": + sandbox.closeFile(); + break; + case "showPage": + sandbox.showPage(data.page); + break; + case "redrawPage": + sandbox.redrawPage(); + break; + default: + console.log("Unknown message received", event); + break; + } + }, false); + + document.addEventListener("keyup", function(event) { + sandbox.postMessage("webodf.keyUp", {"key": event.keyCode}); + event.preventDefault(); + }); + + window.addEventListener("resize", function() { + sandbox.redrawPage(); + }); + + var toggleFullscreen = function(elem) { + var fullScreenElement = document.fullscreenElement || document.msFullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.webkitCurrentFullScreenElement; + if (fullScreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } else if (elem.webkitRequestFullscreen) { + elem.webkitRequestFullscreen(); + } else if (elem.mozRequestFullScreen) { + elem.mozRequestFullScreen(); + } else if (elem.msRequestFullscreen) { + elem.msRequestFullscreen(); + } + } + }; + + container.addEventListener("dblclick", function(event) { + toggleFullscreen(container); + }); + + console.log("WebODF sandbox ready."); + sandbox.postMessage("ready", {"ready": true}); + +})(); diff --git a/static/js/sandboxes/youtube.js b/static/js/sandboxes/youtube.js new file mode 100644 index 00000000..b76f4213 --- /dev/null +++ b/static/js/sandboxes/youtube.js @@ -0,0 +1,261 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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"; +(function () { + + var script = document.getElementsByTagName("script")[0]; + var PARENT_ORIGIN = script.getAttribute("data-parent-origin"); + + var YouTubeSandbox = function(window) { + this.head = document.getElementsByTagName('head')[0]; + this.window = window; + this.addedIframeScript = false; + this.player = null; + this.seekDetector = null; + this.prevTime = null; + this.prevNow = null; + }; + + YouTubeSandbox.prototype.postMessage = function(type, message) { + var msg = {"type": type}; + msg[type] = message; + this.window.parent.postMessage(msg, PARENT_ORIGIN); + }; + + YouTubeSandbox.prototype.onYouTubeIframeAPIReady = function() { + this.postMessage("youtube.apiReady", {"apiReady": true}); + }; + + YouTubeSandbox.prototype.loadApi = function(url) { + if (!this.addedIframeScript) { + var that = this; + var script = document.createElement('script'); + script.type = "text/javascript"; + script.src = url; + script.onerror = function(evt) { + that.postMessage("youtube.error", {"msgid": "loadScriptFailed"}); + that.head.removeChild(script); + that.addedIframeScript = false; + }; + this.head.appendChild(script); + this.addedIframeScript = true; + } + }; + + YouTubeSandbox.prototype.loadPlayer = function(params) { + if (!this.player) { + var that = this; + var stateEvents = { + "-1": "youtube.unstarted", + "0": "youtube.ended", + "1": "youtube.playing", + "2": "youtube.paused", + "3": "youtube.buffering", + "5": "youtube.videocued" + }; + + var errorIds = { + "2": "invalidParameter", + "5": "htmlPlayerError", + "100": "videoNotFound", + "101": "notAllowedEmbedded", + "150": "notAllowedEmbedded" + }; + + var playerVars = params.playerVars || {}; + delete playerVars.origin; + this.player = new this.window.YT.Player("youtubeplayer", { + height: params.height || "390", + width: params.width || "640", + playerVars: playerVars, + events: { + "onReady": function(event) { + that.postMessage("youtube.volume", {"volume": that.player.getVolume()}); + that.postMessage("youtube.playerReady", {"playerReady": true}); + }, + "onStateChange": function(event) { + var msg = stateEvents[event.data]; + if (typeof msg === "undefined") { + console.warn("Unknown YouTube player state", event) + return; + } + + switch (msg) { + case "youtube.playing": + that.prevTime = null; + that.startDetectSeek(); + break; + case "youtube.buffering": + that.startDetectSeek(); + break; + case "youtube.paused": + that.stopDetectSeek(); + break; + case "youtube.ended": + that.stopDetectSeek(); + break; + } + + that.postMessage("youtube.event", {"event": msg, "state": event.data, "position": that.player.getCurrentTime()}); + }, + "onError": function(event) { + var error = errorIds[event.data] || "unknownError"; + that.postMessage("youtube.error", {"msgid": error, "code": event.data}); + } + } + }); + } + }; + + YouTubeSandbox.prototype.destroyPlayer = function() { + this.stopDetectSeek(); + if (this.player) { + this.player.destroy(); + this.player = null; + } + }; + + YouTubeSandbox.prototype.loadVideo = function(id, position) { + this.prevTime = null; + this.prevNow = null; + if (typeof(position) !== "undefined") { + this.player.loadVideoById(id, position); + } else { + this.player.loadVideoById(id); + } + }; + + YouTubeSandbox.prototype.playVideo = function() { + this.player.playVideo(); + }; + + YouTubeSandbox.prototype.pauseVideo = function() { + this.player.pauseVideo(); + }; + + YouTubeSandbox.prototype.stopVideo = function() { + this.player.stopVideo(); + }; + + YouTubeSandbox.prototype.seekTo = function(position, allowSeekAhead) { + if (typeof(allowSeekAhead) !== "undefined") { + this.player.seekTo(position, allowSeekAhead); + } else { + this.player.seekTo(position); + } + }; + + YouTubeSandbox.prototype.setVolume = function(volume) { + this.player.setVolume(volume); + }; + + YouTubeSandbox.prototype.startDetectSeek = function() { + var that = this; + var checkSeek = function() { + if (!that.player) { + return; + } + var now = new Date(); + var time = that.player.getCurrentTime(); + that.postMessage("youtube.position", {"position": time}); + if (that.prevTime === null) { + that.prevTime = time; + } + if (that.prevNow === null) { + that.prevNow = now; + } + var deltaTime = Math.abs(time - that.prevTime); + var deltaNow = (now - that.prevNow) * 0.001; + if (deltaTime > deltaNow * 1.1) { + that.postMessage("youtube.event", {"event": "youtube.seeked", "position": time}); + } + that.prevNow = now; + that.prevTime = time; + }; + + if (!this.seekDetector) { + this.seekDetector = this.window.setInterval(function() { + checkSeek(); + }, 1000); + } + checkSeek(); + }; + + YouTubeSandbox.prototype.stopDetectSeek = function() { + if (this.seekDetector) { + this.window.clearInterval(this.seekDetector); + this.seekDetector = null; + } + this.prevNow = null; + }; + + var sandbox = new YouTubeSandbox(window); + + window.onYouTubeIframeAPIReady = function() { + sandbox.onYouTubeIframeAPIReady(); + }; + + window.addEventListener("message", function(event) { + if (event.origin !== PARENT_ORIGIN) { + // only accept messages from spreed-webrtc + return; + } + var msg = event.data; + var data = msg[msg.type] || {}; + switch (msg.type) { + case "loadApi": + sandbox.loadApi(data.url); + break; + case "loadPlayer": + sandbox.loadPlayer(data); + break; + case "destroyPlayer": + sandbox.destroyPlayer(); + break; + case "loadVideo": + sandbox.loadVideo(data.id, data.position); + break; + case "playVideo": + sandbox.playVideo(); + break; + case "pauseVideo": + sandbox.pauseVideo(); + break; + case "stopVideo": + sandbox.stopVideo(); + break; + case "seekTo": + sandbox.seekTo(data.position, data.allowSeekAhead); + break; + case "setVolume": + sandbox.setVolume(data.volume); + break; + default: + console.log("Unknown message received", event); + break; + } + }, false); + + console.log("YouTube sandbox ready."); + sandbox.postMessage("ready", {"ready": true}); + +})(); diff --git a/static/js/services/alertify.js b/static/js/services/alertify.js index 945edb38..02e14e88 100644 --- a/static/js/services/alertify.js +++ b/static/js/services/alertify.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/animationframe.js b/static/js/services/animationframe.js index 67b56bfc..228b834b 100644 --- a/static/js/services/animationframe.js +++ b/static/js/services/animationframe.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/api.js b/static/js/services/api.js index a1a581eb..a63ce819 100644 --- a/static/js/services/api.js +++ b/static/js/services/api.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/appdata.js b/static/js/services/appdata.js index 9bcba939..d5c4e984 100644 --- a/static/js/services/appdata.js +++ b/static/js/services/appdata.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/buddydata.js b/static/js/services/buddydata.js index acbecbf6..25c4be77 100644 --- a/static/js/services/buddydata.js +++ b/static/js/services/buddydata.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/buddylist.js b/static/js/services/buddylist.js index 07e46768..032a324f 100644 --- a/static/js/services/buddylist.js +++ b/static/js/services/buddylist.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -675,18 +675,8 @@ define(['jquery', 'angular', 'underscore', 'modernizr', 'avltree', 'text!partial // Find session with help of contact. if (contact && contact.Token) { mediaStream.api.sendSessions(contact.Token, "contact", function(event, type, data) { - //console.log("oooooooooooooooo", type, data); var tmpSessionData = null; if (data.Users && data.Users.length > 0) { - /* - _.each(data.Users, function(s) { - buddyData.set(s.Id, scope); - // NOTE(longsleep): Not sure if its a good idea to add the retrieved sessions here. - session.add(s.Id, s); - }); - sessionData = session.get(); - deferred.resolve(sessionData.Id); - */ tmpSessionData = data.Users[0]; } // Check if we got a session in the meantime. diff --git a/static/js/services/buddypicture.js b/static/js/services/buddypicture.js index e5738a21..bc7fa744 100644 --- a/static/js/services/buddypicture.js +++ b/static/js/services/buddypicture.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/buddysession.js b/static/js/services/buddysession.js index 32da83d1..3006714a 100644 --- a/static/js/services/buddysession.js +++ b/static/js/services/buddysession.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/chromeextension.js b/static/js/services/chromeextension.js index ae1308eb..3f26a0df 100644 --- a/static/js/services/chromeextension.js +++ b/static/js/services/chromeextension.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/connector.js b/static/js/services/connector.js index 91cf5fc7..bc404a43 100644 --- a/static/js/services/connector.js +++ b/static/js/services/connector.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/constraints.js b/static/js/services/constraints.js index 6dbecefb..29c3b02e 100644 --- a/static/js/services/constraints.js +++ b/static/js/services/constraints.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,7 +20,7 @@ */ "use strict"; - define(["jquery", "underscore"], function($, _) { + define(["jquery", "underscore", "webrtc.adapter"], function($, _) { // constraints return ["webrtc", "$window", "$q", function(webrtc, $window, $q) { @@ -99,6 +99,8 @@ // Define our service helpers service.e = $({}); // events + service.stun = []; + service.turn = {}; // Create as WebRTC data structure. service.mediaConstraints = function(constraints) { @@ -125,6 +127,26 @@ webrtc.settings.pcConstraints.optional = constraints.pc; }; + service.iceServers = function(constraints) { + + var iceServers = []; + var iceServer; + if (service.stun && service.stun.length) { + iceServer = $window.createIceServers(service.stun); + if (iceServer.length) { + iceServers.push.apply(iceServers, iceServer) + } + } + if (service.turn && service.turn.urls && service.turn.urls.length) { + iceServer = $window.createIceServers(service.turn.urls, service.turn.username, service.turn.password); + if (iceServer.length) { + iceServers.push.apply(iceServers, iceServer) + } + } + webrtc.settings.pcConfig.iceServers = iceServers; + + }; + // Some default constraints. service.e.on("refresh", function(event, constraints) { @@ -156,15 +178,32 @@ return $q.all(constraints.promises).then(function() { service.mediaConstraints(constraints); service.pcConstraints(constraints); + service.iceServers(constraints); }); }, + // Setters for TURN and STUN data. turn: function(turnData) { - // Set TURN server details. service.turn = turnData; }, stun: function(stunData) { service.stun = stunData; - } + }, + supported: (function() { + var isChrome = $window.webrtcDetectedBrowser === "chrome"; + var isFirefox = $window.webrtcDetectedBrowser === "firefox"; + var version = $window.webrtcDetectedVersion; + // Constraints support table. + return { + // Chrome supports it. FF supports new spec starting 38. See https://wiki.mozilla.org/Media/getUserMedia for FF details. + audioVideo: isChrome || (isFirefox && version >= 38), + // HD constraints in Chrome no issue. In FF we MJPEG is fixed with 38 (see https://bugzilla.mozilla.org/show_bug.cgi?id=1151628). + hdVideo: isChrome || (isFirefox && version >= 38), + // Chrome supports this on Windows only. + renderToAssociatedSink: isChrome && $window.navigator.platform.indexOf("Win") === 0, + chrome: isChrome, + firefox: isFirefox + }; + })() }; }]; diff --git a/static/js/services/contactdata.js b/static/js/services/contactdata.js index d6daaf8a..3e40f89f 100644 --- a/static/js/services/contactdata.js +++ b/static/js/services/contactdata.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/contacts.js b/static/js/services/contacts.js index 7e7ddffa..8de55f51 100644 --- a/static/js/services/contacts.js +++ b/static/js/services/contacts.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/continueconnector.js b/static/js/services/continueconnector.js index b02b75c8..afa55346 100644 --- a/static/js/services/continueconnector.js +++ b/static/js/services/continueconnector.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/desktopnotify.js b/static/js/services/desktopnotify.js index 7ccdfb8f..5484e720 100644 --- a/static/js/services/desktopnotify.js +++ b/static/js/services/desktopnotify.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/dialogs.js b/static/js/services/dialogs.js index 2328bbeb..87e3ebd1 100644 --- a/static/js/services/dialogs.js +++ b/static/js/services/dialogs.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/dummystream.js b/static/js/services/dummystream.js new file mode 100644 index 00000000..29fa5f75 --- /dev/null +++ b/static/js/services/dummystream.js @@ -0,0 +1,29 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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([ + 'mediastream/dummystream' +], function(DummyStream) { + return [function() { + return DummyStream; + }]; +}); diff --git a/static/js/services/enrichmessage.js b/static/js/services/enrichmessage.js index 328d394c..379f0bd5 100644 --- a/static/js/services/enrichmessage.js +++ b/static/js/services/enrichmessage.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/fastscroll.js b/static/js/services/fastscroll.js index e472268b..c18b0f0f 100644 --- a/static/js/services/fastscroll.js +++ b/static/js/services/fastscroll.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/filedata.js b/static/js/services/filedata.js index e31eaa0b..1ed4e83e 100644 --- a/static/js/services/filedata.js +++ b/static/js/services/filedata.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/filedownload.js b/static/js/services/filedownload.js index d47b7842..9bcd167d 100644 --- a/static/js/services/filedownload.js +++ b/static/js/services/filedownload.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/filetransfer.js b/static/js/services/filetransfer.js index 91411893..def147e2 100644 --- a/static/js/services/filetransfer.js +++ b/static/js/services/filetransfer.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/fileupload.js b/static/js/services/fileupload.js index b593415f..53275ce6 100644 --- a/static/js/services/fileupload.js +++ b/static/js/services/fileupload.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/geolocation.js b/static/js/services/geolocation.js index 281fbcc4..5eb7fe19 100644 --- a/static/js/services/geolocation.js +++ b/static/js/services/geolocation.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/localstatus.js b/static/js/services/localstatus.js index 55333972..7fb083b6 100644 --- a/static/js/services/localstatus.js +++ b/static/js/services/localstatus.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/localstorage.js b/static/js/services/localstorage.js index b2955087..e156b0ee 100644 --- a/static/js/services/localstorage.js +++ b/static/js/services/localstorage.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -25,7 +25,7 @@ define(["modernizr"], function(Modernizr) { // localStorage return ["$window", function($window) { - // PersistentStorage (c)2014 struktur AG. MIT license. + // PersistentStorage (c)2015 struktur AG. MIT license. var PersistentStorage = function(prefix) { this.prefix = prefix ? prefix : "ps"; this.isPersistentStorage = true; diff --git a/static/js/services/mediadevices.js b/static/js/services/mediadevices.js new file mode 100644 index 00000000..e11c9fce --- /dev/null +++ b/static/js/services/mediadevices.js @@ -0,0 +1,62 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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 . + * + */ + +/* global Promise */ +"use strict"; +define(['webrtc.adapter'], function() { + + // mediaDevices + return ["$window", function($window) { + + var mediaDevices = $window.navigator.mediaDevices || {}; + var getUserMedia = (function() { + // Implement a Promise based wrapper around getUserMedia. + if (mediaDevices.getUserMedia) { + // mediaDevices calls return Promise native. + return mediaDevices.getUserMedia.bind(mediaDevices); + } else { + return function getUserMedia(constraints) { + return new Promise(function(resolve, reject) { + var onSuccess = function(stream) { + resolve(stream) + }; + var onError = function(error) { + reject(error); + }; + try { + $window.getUserMedia(constraints, onSuccess, onError); + } catch(err) { + onError(err); + } + }); + } + } + })(); + + // Public api. + return { + shim: mediaDevices.getUserMedia ? false : true, + getUserMedia: getUserMedia + } + + }]; + +}); \ No newline at end of file diff --git a/static/js/services/mediasources.js b/static/js/services/mediasources.js index e16b2ab2..22172219 100644 --- a/static/js/services/mediasources.js +++ b/static/js/services/mediasources.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/mediastream.js b/static/js/services/mediastream.js index f1ddfc5f..27879784 100644 --- a/static/js/services/mediastream.js +++ b/static/js/services/mediastream.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -26,7 +26,8 @@ define([ 'ua-parser', 'sjcl', 'modernizr', - 'mediastream/tokens' + 'mediastream/tokens', + 'webrtc.adapter' ], function($, _, uaparser, sjcl, Modernizr, tokens) { @@ -43,6 +44,14 @@ define([ // Create encryption key from server token and browser name. var secureKey = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(context.Cfg.Token + uaparser().browser.name)); + // Apply configuration details. + webrtc.settings.renegotiation = context.Cfg.Renegotiation && true; + if (webrtc.settings.renegotiation && $window.webrtcDetectedBrowser === "firefox") { + console.warn("Disable renegotiation in Firefox for now."); + webrtc.settings.renegotiation = false; + } + + // mediaStream service API. var mediaStream = { version: version, ws: url, diff --git a/static/js/services/modules.js b/static/js/services/modules.js index 98d7ac51..99d9f844 100644 --- a/static/js/services/modules.js +++ b/static/js/services/modules.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/playsound.js b/static/js/services/playsound.js index b6acd876..eb682416 100644 --- a/static/js/services/playsound.js +++ b/static/js/services/playsound.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/randomgen.js b/static/js/services/randomgen.js index 647441a0..24c1a983 100644 --- a/static/js/services/randomgen.js +++ b/static/js/services/randomgen.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/resturl.js b/static/js/services/resturl.js index 381124c1..3ff406ee 100644 --- a/static/js/services/resturl.js +++ b/static/js/services/resturl.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -20,21 +20,55 @@ */ "use strict"; -define([ -], function() { +define(["underscore"], function(_) { + // restURL return ["globalContext", "$window", function(context, $window) { - return { - room: function(id) { - id = $window.encodeURIComponent(id); - return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + id; - }, - buddy: function(id) { - return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + "static/img/buddy/s46/" + id; - }, - api: function(path) { - return (context.Cfg.B || "/") + "api/v1/" + path; + var RestURL = function() {}; + RestURL.prototype.room = function(name) { + var url = this.encodeRoomURL(name); + return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + url; + }; + RestURL.prototype.buddy = function(id) { + return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + "static/img/buddy/s46/" + id; + }; + RestURL.prototype.api = function(path) { + return (context.Cfg.B || "/") + "api/v1/" + path; + }; + RestURL.prototype.encodeRoomURL = function(name, prefix, cb) { + // Split parts so slashes are allowed. + var parts = name.split("/"); + var url = []; + var nn = []; + if (typeof prefix !== "undefined") { + url.push(prefix); } + // Allow some things in room name parts. + _.each(parts, function(p) { + if (p === "") { + // Skip empty parts, effectly stripping spurious slashes. + return; + } + nn.push(p); + // URL encode. + p = $window.encodeURIComponent(p); + // Encode back certain stuff we allow. + p = p.replace(/^%40/, "@"); + p = p.replace(/^%24/, "$"); + p = p.replace(/^%2B/, "+"); + url.push(p); + }); + if (cb) { + cb(url.join("/")); + return nn.join("/"); + } + return url.join("/"); + }; + RestURL.prototype.createAbsoluteUrl = function(url) { + var link = $window.document.createElement("a"); + link.href = url; + return link.href; }; + return new RestURL(); }]; }); diff --git a/static/js/services/roompin.js b/static/js/services/roompin.js index b73aeaba..4eaa237a 100644 --- a/static/js/services/roompin.js +++ b/static/js/services/roompin.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/rooms.js b/static/js/services/rooms.js index df99a0ca..93c9b0fb 100644 --- a/static/js/services/rooms.js +++ b/static/js/services/rooms.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -103,7 +103,7 @@ define([ currentRoom = room; if (priorRoom) { priorRoomName = priorRoom.Name; - console.log("Left room", priorRoom.Name); + console.log("Left room", [priorRoom.Name]); $rootScope.$broadcast("room.left", priorRoom.Name); } if (currentRoom) { @@ -212,18 +212,16 @@ define([ return canJoinRooms; }, joinByName: function(name, replace) { - name = $window.encodeURIComponent(name); - name = name.replace(/^%40/, "@"); - name = name.replace(/^%24/, "$"); - name = name.replace(/^%2B/, "+"); - - safeApply($rootScope, function(scope) { - $location.path("/" + name); - if (replace) { - $location.replace(); - } + var nn = restURL.encodeRoomURL(name, "", function(url) { + // Apply new URL. + safeApply($rootScope, function(scope) { + $location.path(url); + if (replace) { + $location.replace(); + } + }); }); - return name; + return nn; }, joinDefault: function(replace) { return rooms.joinByName("", replace); diff --git a/static/js/services/safeapply.js b/static/js/services/safeapply.js index 84d313ec..1b943d84 100644 --- a/static/js/services/safeapply.js +++ b/static/js/services/safeapply.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/safedisplayname.js b/static/js/services/safedisplayname.js index ff781d95..6519592d 100644 --- a/static/js/services/safedisplayname.js +++ b/static/js/services/safedisplayname.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/safemessage.js b/static/js/services/safemessage.js index b0a7e2b0..c1687fa2 100644 --- a/static/js/services/safemessage.js +++ b/static/js/services/safemessage.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/sandbox.js b/static/js/services/sandbox.js new file mode 100644 index 00000000..f3046c93 --- /dev/null +++ b/static/js/services/sandbox.js @@ -0,0 +1,93 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2015 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This 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", "underscore"], function($, _) { + + return ["$window", function($window) { + + var Sandbox = function(iframe, template) { + this.iframe = iframe; + var blob = new $window.Blob([template], {type: "text/html;charset=utf-8"}); + this.url = $window.URL.createObjectURL(blob); + this.iframe.src = this.url; + this.target = this.iframe.contentWindow; + this.e = $({}); + this.handler = _.bind(this.onPostMessageReceived, this); + this.ready = false; + this.pending_messages = []; + this.origin = $window.location.protocol + "//" + $window.location.host; + $window.addEventListener("message", this.handler, false); + }; + + Sandbox.prototype.destroy = function() { + if (this.handler) { + $window.removeEventListener("message", this.handler, false); + this.handler = null; + } + if (this.url) { + $window.URL.revokeObjectURL(this.url); + this.url = null; + } + }; + + Sandbox.prototype.onPostMessageReceived = function(event) { + if ((event.origin !== "null" && event.origin !== this.origin) || event.source !== this.target) { + // the sandboxed data-url iframe has "null" as origin + return; + } + + if (event.data.type === "ready") { + this.ready = true; + this._sendPendingMessages(); + } + + this.e.triggerHandler("message", [event]); + }; + + Sandbox.prototype._sendPendingMessages = function() { + var i; + for (i=0; i= 36) { + this.supported = true; + this.prepare = function(options) { + // To work, the current domain must be whitelisted in + // media.getusermedia.screensharing.allowed_domains (about:config). + // See https://wiki.mozilla.org/Screensharing for reference. + var d = $q.defer(); + var dlg = dialogs.create('/dialogs/screensharedialogff.html', screenshareDialogFFController, {selection: "screen"}, {}); + dlg.result.then(function(source) { + if (source) { + var opts = _.extend({ + mediaSource: source + }, options); + d.resolve(opts); + } else { + d.resolve(null); + } + }, function(err) { + d.resolve(null); + }); + return d.promise; + }; + } + + } else { + // No support for screen sharing. } // Auto install support. diff --git a/static/js/services/services.js b/static/js/services/services.js index 18c5eeec..198b92ee 100644 --- a/static/js/services/services.js +++ b/static/js/services/services.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -66,7 +66,10 @@ define([ 'services/resturl', 'services/roompin', 'services/constraints', - 'services/modules'], function(_, + 'services/modules', + 'services/mediadevices', + 'services/sandbox', + 'services/dummystream'], function(_, desktopNotify, playSound, safeApply, @@ -110,7 +113,10 @@ rooms, restURL, roompin, constraints, -modules) { +modules, +mediaDevices, +sandbox, +dummyStream) { var services = { desktopNotify: desktopNotify, @@ -156,7 +162,10 @@ modules) { restURL: restURL, roompin: roompin, constraints: constraints, - modules: modules + modules: modules, + mediaDevices: mediaDevices, + sandbox: sandbox, + dummyStream: dummyStream }; var initialize = function(angModule) { diff --git a/static/js/services/toastr.js b/static/js/services/toastr.js index f2b521ba..ff70eb5b 100644 --- a/static/js/services/toastr.js +++ b/static/js/services/toastr.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/translation.js b/static/js/services/translation.js index 2641e85d..4ecc9545 100644 --- a/static/js/services/translation.js +++ b/static/js/services/translation.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/usersettingsdata.js b/static/js/services/usersettingsdata.js index 810a44f0..1772968e 100644 --- a/static/js/services/usersettingsdata.js +++ b/static/js/services/usersettingsdata.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/videolayout.js b/static/js/services/videolayout.js index fdbb3a4f..a607900a 100644 --- a/static/js/services/videolayout.js +++ b/static/js/services/videolayout.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * @@ -61,6 +61,13 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern // videoLayout return ["$window", function($window) { + // Invisible layout (essentially shows nothing). + var Invisible = function(container, scope, controller) {}; + Invisible.prototype.name = "invisible"; + Invisible.prototype.render = function() {}; + Invisible.prototype.close = function() {}; + + // Video layout with all videos rendered the same size. var OnePeople = function(container, scope, controller) {}; @@ -86,7 +93,6 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern if (scope.localVideo.style.opacity === '1') { videoWidth = scope.localVideo.videoWidth; videoHeight = scope.localVideo.videoHeight; - console.log("Local video size: ", videoWidth, videoHeight); videos = [null]; } } @@ -327,28 +333,30 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern }; // Register renderers. + renderers[Invisible.prototype.name] = Invisible; renderers[OnePeople.prototype.name] = OnePeople; renderers[Smally.prototype.name] = Smally; renderers[Democrazy.prototype.name] = Democrazy; renderers[ConferenceKiosk.prototype.name] = ConferenceKiosk; renderers[Auditorium.prototype.name] = Auditorium; + // Helper for class name generation. + var makeName = function(prefix, n, camel) { + var r = prefix; + if (camel) { + r = r + n.charAt(0).toUpperCase() + n.slice(1); + } else { + r = r + "-" + n; + } + return r; + }; + // Public api. var current = null; var body = $("body"); return { update: function(name, size, scope, controller) { - var makeName = function(prefix, n, camel) { - var r = prefix; - if (camel) { - r = r + n.charAt(0).toUpperCase() + n.slice(1); - } else { - r = r + "-" + n; - } - return r; - }; - var videos = _.keys(controller.streams); var streams = controller.streams; var container = scope.container; @@ -356,22 +364,20 @@ define(["jquery", "underscore", "modernizr", "injectCSS"], function($, _, Modern if (!current) { current = new renderers[name](container, scope, controller) - //console.log("Created new video layout renderer", name, current); + console.log("Created new video layout renderer", name, current); + $(layoutparent).addClass(makeName("renderer", name)); + body.addClass(makeName("videolayout", name, true)); + return true; + } else if (current && current.name !== name) { + current.close(container, scope, controller); + $(container).removeAttr("style"); + $(layoutparent).removeClass(makeName("renderer", current.name)); + body.removeClass(makeName("videolayout", current.name, true)); + current = new renderers[name](container, scope, controller) $(layoutparent).addClass(makeName("renderer", name)); - $(body).addClass(makeName("videolayout", name, true)); + body.addClass(makeName("videolayout", name, true)); + console.log("Switched to new video layout renderer", name, current); return true; - } else { - if (current.name !== name) { - current.close(container, scope, controller); - $(container).removeAttr("style"); - $(layoutparent).removeClass(makeName("renderer", current.name)); - $(body).removeClass(makeName("videolayout", current.name, true)); - current = new renderers[name](container, scope, controller) - $(layoutparent).addClass(makeName("renderer", name)); - $(body).addClass(makeName("videolayout", name, true)); - //console.log("Switched to new video layout renderer", name, current); - return true; - } } return current.render(container, size, scope, videos, streams); diff --git a/static/js/services/videowaiter.js b/static/js/services/videowaiter.js index bb15c78c..6a57af9b 100644 --- a/static/js/services/videowaiter.js +++ b/static/js/services/videowaiter.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/visibility.js b/static/js/services/visibility.js index 7296adf1..a487b30c 100644 --- a/static/js/services/visibility.js +++ b/static/js/services/visibility.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/js/services/webrtc.js b/static/js/services/webrtc.js index 7b8cdd08..fe3243fb 100644 --- a/static/js/services/webrtc.js +++ b/static/js/services/webrtc.js @@ -1,6 +1,6 @@ /* * Spreed WebRTC. - * Copyright (C) 2013-2014 struktur AG + * Copyright (C) 2013-2015 struktur AG * * This file is part of Spreed WebRTC. * diff --git a/static/partials/audiovideo.html b/static/partials/audiovideo.html index 4e0d93fc..bedfa271 100644 --- a/static/partials/audiovideo.html +++ b/static/partials/audiovideo.html @@ -1,13 +1,17 @@ -
+
+
+
+
+
diff --git a/static/partials/audiovideopeer.html b/static/partials/audiovideopeer.html index b0347140..5233d203 100644 --- a/static/partials/audiovideopeer.html +++ b/static/partials/audiovideopeer.html @@ -1,4 +1,4 @@ -
+
{{peerid|displayName}}
diff --git a/static/partials/odfcanvas_sandbox.html b/static/partials/odfcanvas_sandbox.html new file mode 100644 index 00000000..abe06482 --- /dev/null +++ b/static/partials/odfcanvas_sandbox.html @@ -0,0 +1,40 @@ + + + + WebODF Sandbox + + + + +
+
+
+ + + diff --git a/static/partials/pdfcanvas_sandbox.html b/static/partials/pdfcanvas_sandbox.html new file mode 100644 index 00000000..2a18428a --- /dev/null +++ b/static/partials/pdfcanvas_sandbox.html @@ -0,0 +1,34 @@ + + + + pdf.js Sandbox + + + + +
+ +
+ + + diff --git a/static/partials/picturehover.html b/static/partials/picturehover.html new file mode 100644 index 00000000..03954b49 --- /dev/null +++ b/static/partials/picturehover.html @@ -0,0 +1,11 @@ +
+
+ + +
+
+ diff --git a/static/partials/screensharedialogff.html b/static/partials/screensharedialogff.html new file mode 100644 index 00000000..67dfb763 --- /dev/null +++ b/static/partials/screensharedialogff.html @@ -0,0 +1,22 @@ +
+ + + +
\ No newline at end of file diff --git a/static/partials/settings.html b/static/partials/settings.html index 341ee594..c894303e 100644 --- a/static/partials/settings.html +++ b/static/partials/settings.html @@ -66,7 +66,7 @@
- + {{_('Media')}}
@@ -82,14 +82,14 @@
-
+
- - + +
@@ -130,7 +130,7 @@
-
+
@@ -143,7 +143,7 @@
-
+
@@ -154,7 +154,7 @@
-
+
@@ -165,7 +165,7 @@
-
+
@@ -178,7 +178,7 @@
-
+
@@ -191,7 +191,7 @@
-
+
@@ -203,7 +203,7 @@
-
+
@@ -217,7 +217,7 @@
-
+
@@ -228,7 +228,7 @@
-
+
@@ -241,7 +241,7 @@
-
+
diff --git a/static/partials/statusmessage.html b/static/partials/statusmessage.html index e60cb482..2660143d 100644 --- a/static/partials/statusmessage.html +++ b/static/partials/statusmessage.html @@ -1,6 +1,6 @@ {{_("Initializing")}} - {{id|displayName}} + {{id|displayName}} {{_("Calling")}} {{dialing|displayName}} {{_("Hangup")}} {{_("In call with")}} {{peer|displayName}} {{_("Hangup")}} {{_("Conference with")}} {{peer|displayName}}{{conferencePeers|displayConference}} {{_("Hangup")}} diff --git a/static/partials/ui.html b/static/partials/ui.html new file mode 100644 index 00000000..648c20ba --- /dev/null +++ b/static/partials/ui.html @@ -0,0 +1,52 @@ +
+ + +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/static/partials/usability.html b/static/partials/usability.html index 8f57cd65..9c5e9002 100644 --- a/static/partials/usability.html +++ b/static/partials/usability.html @@ -1,4 +1,4 @@ -
+
{{_("Checking camera and microphone access.")}}
diff --git a/static/partials/youtubevideo.html b/static/partials/youtubevideo.html index 5fe5e3d3..0cd01e76 100644 --- a/static/partials/youtubevideo.html +++ b/static/partials/youtubevideo.html @@ -29,11 +29,11 @@
-
+
-
+
-
+
{{_('Currently playing')}}
{{ currentVideoUrl }}
@@ -54,9 +54,9 @@
- +
- +
diff --git a/static/partials/youtubevideo_sandbox.html b/static/partials/youtubevideo_sandbox.html new file mode 100644 index 00000000..a50ea83e --- /dev/null +++ b/static/partials/youtubevideo_sandbox.html @@ -0,0 +1,27 @@ + + + + YouTube Player Sandbox + + + + +
+ + + diff --git a/static/translation/messages-de.json b/static/translation/messages-de.json index 3eca0fb4..35c2bbc5 100644 --- a/static/translation/messages-de.json +++ b/static/translation/messages-de.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=2; plural=(n != 1)"},"Your audio level":[null,"Ihr Audio-Pegel"],"Standard view":[null,"Standardansicht"],"Large view":[null,"Große Videos"],"Kiosk view":[null,"Kiosk-Ansicht"],"Auditorium":[null,"Auditorium"],"Start chat":[null,"Chat starten"],"Start video call":[null,"Video-Anruf starten"],"Start audio conference":[null,"Audio-Konferenz starten"],"No one else here":[null,"Niemand sonst hier"],"Take":[null,"Los"],"Retake":[null,"Nochmal"],"Cancel":[null,"Abbrechen"],"Set as Profile Picture":[null,"Als Bild setzen"],"Take picture":[null,"Bild machen"],"Upload picture":[null,"Bild hochladen"],"Waiting for camera":[null,"Warte auf die Kamera"],"Picture":[null,"Bild"],"The file couldn't be read.":[null,"Die Datei konnte nicht geöffnet werden."],"The file is not an image.":[null,"Diese Datei ist kein Bild."],"The file is too large. Max. %d MB.":[null,"Diese Datei ist zu groß. Max. %d MB."],"Select file":[null,"Datei wählen"],"Chat sessions":[null,"Chat-Sitzungen"],"Room chat":[null,"Raum-Chat"],"Peer to peer":[null,"Peer-to-peer"],"Close chat":[null,"Chat schließen"],"Upload files":[null,"Dateien hochladen"],"Share my location":[null,"Meinen Standort teilen"],"Clear chat":[null,"Chat löschen"],"is typing...":[null," schreibt gerade..."],"has stopped typing...":[null," schreibt nicht mehr..."],"Type here to chat...":[null,"Nachricht hier eingeben..."],"Send":[null,"Senden"],"Accept":[null,"Akzeptieren"],"Reject":[null,"Abweisen"],"You have no contacts.":[null,"Sie haben keine Kontakte."],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[null,"Betreten Sie einen Raum und klicken dann auf das Stern-Symbol eines anderen Nutzers um eine Kontaktanfrage zu starten."],"Edit contact":[null,"Kontakt bearbeiten"],"Edit":[null,"Bearbeiten"],"Name":[null,"Name"],"Remove":[null,"Entfernen"],"Refresh":[null,"Aktualisieren"],"Save":[null,"Speichern"],"Close":[null,"Schließen"],"File sharing":[null,"Datei-Austausch"],"File is no longer available":[null,"Datei ist nicht mehr verfügbar"],"Download":[null,"Laden"],"Open":[null,"Öffnen"],"Unshare":[null,"Zurückziehen"],"Retry":[null,"Nochmal versuchen"],"Download failed.":[null,"Fehler beim Download."],"Share a YouTube video":[null,"Ein YouTube Video teilen"],"Share a file as presentation":[null,"Datei als Präsentation teilen."],"Share your screen":[null,"Bildschirm freigeben"],"Chat":[null,"Chat"],"Contacts":[null,"Kontakte"],"Mute microphone":[null,"Mikrofon abschalten"],"Turn camera off":[null,"Kamera abschalten"],"Settings":[null,"Einstellungen"],"Loading presentation ...":[null,"Präsentation wird geladen..."],"Please upload a document":[null,"Bitte Dokument hochladen"],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[null,"Das Dokument wird mit allen Gesprächsteilnehmern geteilt. Unterstützt werden PDF und OpenDocument Dateien."],"Upload":[null,"Hochladen"],"You can drag files here too.":[null,"Sie können Dateien auch hierhin ziehen."],"Presentation controls":[null,"Präsentations-Steuerung"],"Prev":[null,"Zurück"],"Next":[null,"Vor"],"Change room":[null,"Raum wechseln"],"Room":[null,"Raum"],"Leave room":[null,"Raum verlassen"],"Main":[null,"Standard"],"Current room":[null,"Raum"],"Screen sharing options":[null,"Optionen für Bildschirmfreigabe"],"Fit screen.":[null,"Bildschirm einpassen."],"Profile":[null,"Profil"],"Your name":[null,"Ihr Name"],"Your picture":[null,"Ihr Bild"],"Status message":[null,"Status Nachricht"],"What's on your mind?":[null,"Was machen Sie gerade?"],"Your picture, name and status message identify yourself in calls, chats and rooms.":[null,"Ihr Bild, Name und Status Nachricht repräsentiert Sie in Anrufen, Chats und Räumen."],"Your ID":[null,"Ihre ID"],"Register":[null,"Registrieren"],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,"Mit Zertifikat angemeldet. Melden Sie sich ab indem Sie das Zertifikat aus dem Browser entfernen."],"Sign in":[null,"Anmelden"],"Create an account":[null,"Registrieren"],"Sign out":[null,"Abmelden"],"Manage account":[null,"Konto verwalten"],"Media":[null,"Kamera / Mikrofon"],"Microphone":[null,"Mikrofon"],"Camera":[null,"Kamera"],"Video quality":[null,"Video-Qualität"],"Low":[null,"Gering"],"High":[null,"Hoch"],"HD":[null,"HD"],"Full HD":[null,"Full HD"],"General":[null,"Allgemein"],"Language":[null,"Sprache"],"Language changes become active on reload.":[null,"Sie müssen die Seite neu laden, um die Spracheinstellung zu übernehmen."],"Default room":[null,"Standard Raum"],"Set alternative room to join at start.":[null," Raum wird beim Start automatisch betreten."],"Desktop notification":[null,"Desktop-Benachrichtigung"],"Enable":[null,"Aktivieren"],"Denied - check your browser settings":[null,"Verweigert - prüfen Sie die Browser-Einstellungen"],"Allowed":[null,"Aktiviert"],"Advanced settings":[null,"Erweiterte Einstellungen"],"Play audio on same device as selected microphone":[null,"Audioausgabe auf dem zum Mikrofon gehörenden Gerät"],"Experimental AEC":[null,"Experimentelle AEC"],"Experimental AGC":[null,"Experimentelle AGC"],"Experimental noise suppression":[null,"Experimentelle Geräuschunterdrückung"],"Max video frame rate":[null,"Max. Bildwiederholrate"],"auto":[null,"auto"],"Send stereo audio":[null,"Audio in Stereo übertragen"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[null,"Um Stereo zu übertragen wird die Echo-Unterdrückung deaktiviert. Nur aktivieren wenn das Eingangssignal Stereo ist."],"Detect CPU over use":[null,"CPU-Überlast erkennen"],"Automatically reduces video quality as needed.":[null,"Reduziert die Videoqualität wenn nötig."],"Optimize for high resolution video":[null,"Für hohe Auflösung optimieren"],"Reduce video noise":[null,"Rauschen reduzieren"],"Enable experiments":[null,"Experimente aktivieren"],"Show advanced settings":[null,"Erweiterte Einstellungen anzeigen"],"Hide advanced settings":[null,"Erweiterte Einstellungen ausblenden"],"Remember settings":[null,"Einstellungen merken"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,"Ihre ID bleibt dennoch gespeichert. Klicken Sie Ausloggen weiter oben um die ID zu löschen."],"Room link":[null,"Raum-Link"],"Invite by Email":[null,"Per E-Mail einladen"],"Invite with Facebook":[null,"Mit Facebook einladen"],"Invite with Twitter":[null,"Mit Twitter einladen"],"Invite with Google Plus":[null,"Mit Google Plus einladen"],"Invite with XING":[null,"Mit XING einladen"],"Initializing":[null,"Initialisiere"],"Online":[null,"Online"],"Calling":[null,"Verbinde mit"],"Hangup":[null,"Auflegen"],"In call with":[null,"Verbunden mit"],"Conference with":[null,"Konferenz mit"],"Your are offline":[null,"Sie sind offline"],"Go online":[null,"Online gehen"],"Connection interrupted":[null,"Verbindung unterbrochen"],"An error occured":[null,"Ein Fehler ist aufgetreten"],"Incoming call":[null,"Eingehender Anruf"],"from":[null,"von"],"Accept call":[null,"Anruf annehmen"],"Waiting for camera/microphone access":[null,"Warte auf Kamera/Mikrofon Freigabe"],"Checking camera and microphone access.":[null,"Prüfe Zugriff auf Kamera und Mikrofon."],"Please allow access to your camera and microphone.":[null,"Bitte gestatten Sie den Zugriff auf Ihre Kamera und Mikrofon."],"Camera / microphone access required.":[null,"Kamera / Mikrofon Zugriff wird benötigt."],"Please check your browser settings and allow camera and microphone access for this site.":[null,"Bitte prüfen Sie Ihre Browser-Einstellungen und gestatten Sie den Zugriff auf Kamera und Mikrofon für diese Seite."],"Skip check":[null,"Überspringen"],"Click here for help (Google Chrome).":[null,"Hier klicken für weitere Infos (Google Chrome)."],"Please set your user details and settings.":[null,"Bitte vervollständigen Sie Ihre Daten und Einstellungen."],"Enter a room name":[null,"Raum eingeben"],"Random room name":[null,"Zufälliger Raum"],"Enter room":[null,"Raum betreten"],"Enter the name of an existing room. You can create new rooms when you are signed in.":[null,"Geben Sie den Namen eines existierenden Raums ein. Melden Sie sich an um eigene Räume zu erstellen."],"Room history":[null,"Raum-Verlauf"],"Please sign in.":[null,"Bitte melden Sie sich an."],"Videos play simultaneously for everyone in this call.":[null,"Das Video wird bei allen Gesprächsteilnehmern angezeigt."],"YouTube URL":[null,"YouTube URL"],"Share":[null,"Teilen"],"Could not load YouTube player API, please check your network / firewall settings.":[null,"Es konnte keine Verbindung zu YouTube aufgebaut werden. Bitte prüfen Sie Ihre Internetverbindung / Firewall."],"Currently playing":[null,"Aktuelles Video"],"YouTube controls":[null,"YouTube Steuerung"],"YouTube video to share":[null,"YouTube Video teilen"],"Peer to peer chat active.":[null,"Peer-to-peer Chat ist aktiv."],"Peer to peer chat is now off.":[null,"Peer-to-peer Chat ist nicht mehr aktiv."]," is now offline.":[null," ist jetzt offline."]," is now online.":[null," ist jetzt online."],"You share file:":[null,"Sie geben eine Datei frei:"],"Incoming file:":[null,"Eingehende Datei:"],"You shared your location:":[null,"Sie haben Ihren Standort geteilt:"],"Location received:":[null,"Standort erhalten:"],"You accepted the contact request.":[null,"Sie haben die Kontaktanfrage angenommen."],"You rejected the contact request.":[null,"Sie haben die Kontaktanfrage abgelehnt."],"You sent a contact request.":[null,"Sie haben eine Kontaktanfrage gesendet."],"Your contact request was accepted.":[null,"Ihre Kontaktanfrage wurde angenommen."],"Incoming contact request.":[null,"Kontaktanfrage erhalten."],"Your contact request was rejected.":[null,"Ihre Kontaktanfrage wurde abgelehnt."],"Edit Contact":[null,"Kontakt bearbeiten"],"Close this window and disconnect?":[null,"Fenster schließen und die Verbindung trennen?"],"Contacts Manager":[null,"Kontakte"],"Restart required to apply updates. Click ok to restart now.":[null,"Es stehen Updates zur Verfügung. Klicken Sie Ok um die Anwendung neu zu starten."],"Failed to access camera/microphone.":[null,"Fehler beim Zugriff auf die Kamera / das Mikrofon."],"Failed to establish peer connection.":[null,"Fehler beim Verbindungsaufbau."],"We are sorry but something went wrong. Boo boo.":[null,"Leider ist ein Fehler aufgetreten. Buhuhu."],"Oops":[null,"Hoppla"],"Peer connection failed. Check your settings.":[null,"Verbindung fehlgeschlagen. Überprüfen Sie Ihre Einstellungen."],"User hung up because of error.":[null,"Teilnehmer hat aufgelegt, da ein Fehler aufgetreten ist."]," is busy. Try again later.":[null," ist in einem Gespräch. Probieren Sie es später."]," rejected your call.":[null," hat Ihren Anruf abgelehnt."]," does not pick up.":[null," nimmt nicht ab."]," tried to call you":[null," hat versucht Sie anzurufen"]," called you":[null," hat Sie angerufen"],"Your browser is not supported. Please upgrade to a current version.":[null,"Ihr Browser wird nicht unterstützt. Bitte aktualisieren Sie auf eine aktuelle Version."],"Your browser does not support WebRTC. No calls possible.":[null,"Ihr Browser unterstützt kein WebRTC. Keine Anrufe möglich."],"Chat with":[null,"Chat mit"],"Message from ":[null,"Nachricht von "],"You are now in room %s ...":[null,"Sie sind nun im Raum %s ..."],"Your browser does not support file transfer.":[null,"Mit Ihrem Browser können keine Dateien übertragen werden."],"Could not load PDF: Please make sure to select a PDF document.":[null,"PDF konnte nicht geladen werden - Bitte stellen Sie sicher, dass Sie ein gültiges PDF-Dokument ausgewählt haben."],"Could not load PDF: Missing PDF file.":[null,"Das PDF konnte nicht geladen werden: Datei fehlt."],"An error occurred while loading the PDF (%s).":[null,"Beim Laden des PDF's ist ein Fehler aufgetreten (%s)."],"An unknown error occurred while loading the PDF.":[null,"Beim Laden des PDF ist ein unbekannter Fehler aufgetreten."],"An error occurred while loading the PDF page (%s).":[null,"Beim Laden der PDF-Seite ist ein Fehler aufgetreten (%s)."],"An unknown error occurred while loading the PDF page.":[null,"Beim Laden der PDF-Seite ist ein unbekannter Fehler aufgetreten (%s)."],"An error occurred while rendering the PDF page (%s).":[null,"Beim Anzeigen der PDF-Seite ist ein Fehler aufgetreten (%s)."],"An unknown error occurred while rendering the PDF page.":[null,"Beim Anzeigen der PDF-Seite ist ein ubekannter Fehler aufgetreten."],"Only PDF documents and OpenDocument files can be shared at this time.":[null,"Es können nur Dokumente im PDF oder OpenDocument-Format als Präsentation verwendet werden."],"Failed to start screen sharing (%s).":[null,"Die Bildschirmfreigabe konnte nicht gestartet werden (%s)."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"Die Berechtigung für die Bildschirmaufzeichnung wurde verweigert. Bitte stellen Sie sicher die Unterstützung für Bildschimaufzeichnung in Ihrem Browser aktiviert ist. Kopieren Sie dazu chrome://flags/#enable-usermedia-screen-capture und öffnen Sie diese Adresse in Ihrem Browser. Aktivieren Sie die oberste Einstellung und starten dann den Browser neu. Anschließend können Sie die Bildschirmfreigabe benutzen."],"Permission to start screen sharing was denied.":[null,"Die Berechtigung den Bildschirm freizugeben wurde verweigert."],"Use browser language":[null,"Browsereinstellung"],"Meet with me here:":[null,"Meeting:"],"Room name":[null,"Raum-Name"],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[null,"Unbekanntes URL-Format. Bitte geben Sie eine gültige YouTube URL ein."],"Error":[null,"Fehler"],"Hint":[null,"Hinweis"],"Please confirm":[null,"Bitte bestätigen"],"More information required":[null,"Weitere Informationen nötig"],"Ok":[null,"OK"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[null,"Die Bildschrimfreigabe benötigt eine Browser-Erweiterung. Bitte fügen Sie die \"Spreed WebRTC screen sharing\" Erweiterung zu Chrome hinzu."],"Access code required":[null,"Bitte Zugriffscode eingeben"],"Access denied":[null,"Zugriff verweigert"],"Please provide a valid access code.":[null,"Bitte geben Sie einen gültigen Zugriffscode ein."],"Failed to verify access code. Check your Internet connection and try again.":[null,"Der Zugriffscode konnte nicht überprueft werden. Bitte prüfen Sie Ihre Internetverbindung."],"PIN for room %s is now '%s'.":[null,"PIN für Raum %s ist jetzt '%s'."],"PIN lock has been removed from room %s.":[null,"Raum %s ist nicht mehr PIN-geschützt."],"Enter the PIN for room %s":[null,"Geben Sie die PIN für Raum %s ein"],"Please sign in to create rooms.":[null,"Bitte melden Sie sich an um Räume zu erstellen."],"and %s":[null,"und %s"],"and %d others":[null,"und %d weiteren"],"User":[null,"Teilnehmer"],"Someone":[null,"Unbekannt"],"Me":[null,"Ich"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=2; plural=(n != 1)"},"Standard view":["Standardansicht"],"Large view":["Große Videos"],"Kiosk view":["Kiosk-Ansicht"],"Auditorium":["Auditorium"],"Start chat":["Chat starten"],"Start video call":["Video-Anruf starten"],"Start audio conference":["Audio-Konferenz starten"],"No one else here":["Niemand sonst hier"],"Take":["Los"],"Retake":["Nochmal"],"Cancel":["Abbrechen"],"Set as Profile Picture":["Als Bild setzen"],"Take picture":["Bild machen"],"Upload picture":["Bild hochladen"],"Waiting for camera":["Warte auf die Kamera"],"Picture":["Bild"],"The file couldn't be read.":["Die Datei konnte nicht geöffnet werden."],"The file is not an image.":["Diese Datei ist kein Bild."],"The file is too large. Max. %d MB.":["Diese Datei ist zu groß. Max. %d MB."],"Select file":["Datei wählen"],"Chat sessions":["Chat-Sitzungen"],"Room chat":["Raum-Chat"],"Peer to peer":["Peer-to-peer"],"Close chat":["Chat schließen"],"Upload files":["Dateien hochladen"],"Share my location":["Meinen Standort teilen"],"Clear chat":["Chat löschen"],"is typing...":[" schreibt gerade..."],"has stopped typing...":[" schreibt nicht mehr..."],"Type here to chat...":["Nachricht hier eingeben..."],"Send":["Senden"],"Accept":["Akzeptieren"],"Reject":["Abweisen"],"You have no contacts.":["Sie haben keine Kontakte."],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":["Betreten Sie einen Raum und klicken dann auf das Stern-Symbol eines anderen Nutzers um eine Kontaktanfrage zu starten."],"Edit contact":["Kontakt bearbeiten"],"Edit":["Bearbeiten"],"Name":["Name"],"Remove":["Entfernen"],"Refresh":["Aktualisieren"],"Save":["Speichern"],"Close":["Schließen"],"File sharing":["Datei-Austausch"],"File is no longer available":["Datei ist nicht mehr verfügbar"],"Download":["Laden"],"Open":["Öffnen"],"Unshare":["Zurückziehen"],"Retry":["Nochmal versuchen"],"Download failed.":["Fehler beim Download."],"Share a YouTube video":["Ein YouTube Video teilen"],"Share a file as presentation":["Datei als Präsentation teilen."],"Share your screen":["Bildschirm freigeben"],"Chat":["Chat"],"Contacts":["Kontakte"],"Mute microphone":["Mikrofon abschalten"],"Turn camera off":["Kamera abschalten"],"Settings":["Einstellungen"],"Loading presentation ...":["Präsentation wird geladen..."],"Please upload a document":["Bitte Dokument hochladen"],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":["Das Dokument wird mit allen Gesprächsteilnehmern geteilt. Unterstützt werden PDF und OpenDocument Dateien."],"Upload":["Hochladen"],"You can drag files here too.":["Sie können Dateien auch hierhin ziehen."],"Presentation controls":["Präsentations-Steuerung"],"Prev":["Zurück"],"Next":["Vor"],"Change room":["Raum wechseln"],"Room":["Raum"],"Leave room":["Raum verlassen"],"Main":["Standard"],"Current room":["Raum"],"Screen sharing options":["Optionen für Bildschirmfreigabe"],"Fit screen.":["Bildschirm einpassen."],"Share screen":["Bildschirm teilen"],"Please select what to share.":["Bitte wählen Sie aus, was geteilt werden soll."],"Screen":["Bildschirm"],"Window":["Fenster"],"Application":["Anwendung"],"Share the whole screen. Click share to select the screen.":["Gesamten Bildschirm teilen. Klicken Sie auf Teilen um den Bildschirm auszuwählen."],"Share a single window. Click share to select the window.":["Einzelnes Fenster teilen. Klicken Sie auf Teilen um das Fenster auszuwählen."],"Share all windows of a application. This can leak content behind windows when windows get moved. Click share to select the application.":["Alle Fenster einer Anwendung teilen. Es wird u.U. Inhalt hinter Fenstern der Anwendung geteilt, wenn diese verschoben werden. Klicken Sie auf Teilen um die Anwendung auszuwählen."],"Share":["Teilen"],"Profile":["Profil"],"Your name":["Ihr Name"],"Your picture":["Ihr Bild"],"Status message":["Status Nachricht"],"What's on your mind?":["Was machen Sie gerade?"],"Your picture, name and status message identify yourself in calls, chats and rooms.":["Ihr Bild, Name und Status Nachricht repräsentiert Sie in Anrufen, Chats und Räumen."],"Your ID":["Ihre ID"],"Register":["Registrieren"],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":["Mit Zertifikat angemeldet. Melden Sie sich ab indem Sie das Zertifikat aus dem Browser entfernen."],"Sign in":["Anmelden"],"Create an account":["Registrieren"],"Sign out":["Abmelden"],"Manage account":["Konto verwalten"],"Media":["Kamera / Mikrofon"],"Microphone":["Mikrofon"],"Camera":["Kamera"],"Video quality":["Video-Qualität"],"Low":["Gering"],"High":["Hoch"],"HD":["HD"],"Full HD":["Full HD"],"General":["Allgemein"],"Language":["Sprache"],"Language changes become active on reload.":["Sie müssen die Seite neu laden, um die Spracheinstellung zu übernehmen."],"Default room":["Standard Raum"],"Set alternative room to join at start.":[" Raum wird beim Start automatisch betreten."],"Desktop notification":["Desktop-Benachrichtigung"],"Enable":["Aktivieren"],"Denied - check your browser settings":["Verweigert - prüfen Sie die Browser-Einstellungen"],"Allowed":["Aktiviert"],"Advanced settings":["Erweiterte Einstellungen"],"Play audio on same device as selected microphone":["Audioausgabe auf dem zum Mikrofon gehörenden Gerät"],"Experimental AEC":["Experimentelle AEC"],"Experimental AGC":["Experimentelle AGC"],"Experimental noise suppression":["Experimentelle Geräuschunterdrückung"],"Max video frame rate":["Max. Bildwiederholrate"],"auto":["auto"],"Send stereo audio":["Audio in Stereo übertragen"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":["Um Stereo zu übertragen wird die Echo-Unterdrückung deaktiviert. Nur aktivieren wenn das Eingangssignal Stereo ist."],"Detect CPU over use":["CPU-Überlast erkennen"],"Automatically reduces video quality as needed.":["Reduziert die Videoqualität wenn nötig."],"Optimize for high resolution video":["Für hohe Auflösung optimieren"],"Reduce video noise":["Rauschen reduzieren"],"Enable experiments":["Experimente aktivieren"],"Show advanced settings":["Erweiterte Einstellungen anzeigen"],"Hide advanced settings":["Erweiterte Einstellungen ausblenden"],"Remember settings":["Einstellungen merken"],"Your ID will still be kept - press the log out button above to delete the ID.":["Ihre ID bleibt dennoch gespeichert. Klicken Sie Ausloggen weiter oben um die ID zu löschen."],"Room link":["Raum-Link"],"Invite by Email":["Per E-Mail einladen"],"Invite with Facebook":["Mit Facebook einladen"],"Invite with Twitter":["Mit Twitter einladen"],"Invite with Google Plus":["Mit Google Plus einladen"],"Invite with XING":["Mit XING einladen"],"Initializing":["Initialisiere"],"Online":["Online"],"Calling":["Verbinde mit"],"Hangup":["Auflegen"],"In call with":["Verbunden mit"],"Conference with":["Konferenz mit"],"Your are offline":["Sie sind offline"],"Go online":["Online gehen"],"Connection interrupted":["Verbindung unterbrochen"],"An error occured":["Ein Fehler ist aufgetreten"],"Incoming call":["Eingehender Anruf"],"from":["von"],"Accept call":["Anruf annehmen"],"Waiting for camera/microphone access":["Warte auf Kamera/Mikrofon Freigabe"],"Your audio level":["Ihr Audio-Pegel"],"Checking camera and microphone access.":["Prüfe Zugriff auf Kamera und Mikrofon."],"Please allow access to your camera and microphone.":["Bitte gestatten Sie den Zugriff auf Ihre Kamera und Mikrofon."],"Camera / microphone access required.":["Kamera / Mikrofon Zugriff wird benötigt."],"Please check your browser settings and allow camera and microphone access for this site.":["Bitte prüfen Sie Ihre Browser-Einstellungen und gestatten Sie den Zugriff auf Kamera und Mikrofon für diese Seite."],"Skip check":["Überspringen"],"Click here for help (Google Chrome).":["Hier klicken für weitere Infos (Google Chrome)."],"Please set your user details and settings.":["Bitte vervollständigen Sie Ihre Daten und Einstellungen."],"Enter a room name":["Raum eingeben"],"Random room name":["Zufälliger Raum"],"Enter room":["Raum betreten"],"Enter the name of an existing room. You can create new rooms when you are signed in.":["Geben Sie den Namen eines existierenden Raums ein. Melden Sie sich an um eigene Räume zu erstellen."],"Room history":["Raum-Verlauf"],"Please sign in.":["Bitte melden Sie sich an."],"Videos play simultaneously for everyone in this call.":["Das Video wird bei allen Gesprächsteilnehmern angezeigt."],"YouTube URL":["YouTube URL"],"Could not load YouTube player API, please check your network / firewall settings.":["Es konnte keine Verbindung zu YouTube aufgebaut werden. Bitte prüfen Sie Ihre Internetverbindung / Firewall."],"Currently playing":["Aktuelles Video"],"YouTube controls":["YouTube Steuerung"],"YouTube video to share":["YouTube Video teilen"],"Peer to peer chat active.":["Peer-to-peer Chat ist aktiv."],"Peer to peer chat is now off.":["Peer-to-peer Chat ist nicht mehr aktiv."]," is now offline.":[" ist jetzt offline."]," is now online.":[" ist jetzt online."],"You share file:":["Sie geben eine Datei frei:"],"Incoming file:":["Eingehende Datei:"],"You shared your location:":["Sie haben Ihren Standort geteilt:"],"Location received:":["Standort erhalten:"],"You accepted the contact request.":["Sie haben die Kontaktanfrage angenommen."],"You rejected the contact request.":["Sie haben die Kontaktanfrage abgelehnt."],"You sent a contact request.":["Sie haben eine Kontaktanfrage gesendet."],"Your contact request was accepted.":["Ihre Kontaktanfrage wurde angenommen."],"Incoming contact request.":["Kontaktanfrage erhalten."],"Your contact request was rejected.":["Ihre Kontaktanfrage wurde abgelehnt."],"Edit Contact":["Kontakt bearbeiten"],"Close this window and disconnect?":["Fenster schließen und die Verbindung trennen?"],"Contacts Manager":["Kontakte"],"Restart required to apply updates. Click ok to restart now.":["Es stehen Updates zur Verfügung. Klicken Sie Ok um die Anwendung neu zu starten."],"Failed to access camera/microphone.":["Fehler beim Zugriff auf die Kamera / das Mikrofon."],"Failed to establish peer connection.":["Fehler beim Verbindungsaufbau."],"We are sorry but something went wrong. Boo boo.":["Leider ist ein Fehler aufgetreten. Buhuhu."],"Oops":["Hoppla"],"Peer connection failed. Check your settings.":["Verbindung fehlgeschlagen. Überprüfen Sie Ihre Einstellungen."],"User hung up because of error.":["Teilnehmer hat aufgelegt, da ein Fehler aufgetreten ist."]," is busy. Try again later.":[" ist in einem Gespräch. Probieren Sie es später."]," rejected your call.":[" hat Ihren Anruf abgelehnt."]," does not pick up.":[" nimmt nicht ab."]," tried to call you":[" hat versucht Sie anzurufen"]," called you":[" hat Sie angerufen"],"Your browser is not supported. Please upgrade to a current version.":["Ihr Browser wird nicht unterstützt. Bitte aktualisieren Sie auf eine aktuelle Version."],"Your browser does not support WebRTC. No calls possible.":["Ihr Browser unterstützt kein WebRTC. Keine Anrufe möglich."],"Chat with":["Chat mit"],"Message from ":["Nachricht von "],"You are now in room %s ...":["Sie sind nun im Raum %s ..."],"Your browser does not support file transfer.":["Mit Ihrem Browser können keine Dateien übertragen werden."],"Could not load PDF: Please make sure to select a PDF document.":["PDF konnte nicht geladen werden - Bitte stellen Sie sicher, dass Sie ein gültiges PDF-Dokument ausgewählt haben."],"Could not load PDF: Missing PDF file.":["Das PDF konnte nicht geladen werden: Datei fehlt."],"An error occurred while loading the PDF (%s).":["Beim Laden des PDF's ist ein Fehler aufgetreten (%s)."],"An unknown error occurred while loading the PDF.":["Beim Laden des PDF ist ein unbekannter Fehler aufgetreten."],"An error occurred while loading the PDF page (%s).":["Beim Laden der PDF-Seite ist ein Fehler aufgetreten (%s)."],"An unknown error occurred while loading the PDF page.":["Beim Laden der PDF-Seite ist ein unbekannter Fehler aufgetreten (%s)."],"An error occurred while rendering the PDF page (%s).":["Beim Anzeigen der PDF-Seite ist ein Fehler aufgetreten (%s)."],"An unknown error occurred while rendering the PDF page.":["Beim Anzeigen der PDF-Seite ist ein ubekannter Fehler aufgetreten."],"Only PDF documents and OpenDocument files can be shared at this time.":["Es können nur Dokumente im PDF oder OpenDocument-Format als Präsentation verwendet werden."],"Failed to start screen sharing (%s).":["Die Bildschirmfreigabe konnte nicht gestartet werden (%s)."],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":["Die Berechtigung für die Bildschirmaufzeichnung wurde verweigert. Bitte stellen Sie sicher die Unterstützung für Bildschimaufzeichnung in Ihrem Browser aktiviert ist. Kopieren Sie dazu chrome://flags/#enable-usermedia-screen-capture und öffnen Sie diese Adresse in Ihrem Browser. Aktivieren Sie die oberste Einstellung und starten dann den Browser neu. Anschließend können Sie die Bildschirmfreigabe benutzen."],"Permission to start screen sharing was denied.":["Die Berechtigung den Bildschirm freizugeben wurde verweigert."],"Use browser language":["Browsereinstellung"],"Meet with me here:":["Meeting:"],"Room name":["Raum-Name"],"The request contains an invalid parameter value. Please check the URL of the video you want to share and try again.":["Die Anfrage enthält falsche Parameter. Bitte prüfen Sie die URL des Videos."],"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred. Please try again later.":["Dieser Inhalt kann nicht im HTML5-Player abgespielt werden oder ein anderer HTML5-Player-Fehler ist aufgetreten. Bitte versuchen Sie es später wieder."],"The video requested was not found. Please check the URL of the video you want to share and try again.":["Das Video wurde nicht gefunden. Bitte prüfen Sie die URL des Videos."],"The owner of the requested video does not allow it to be played in embedded players.":["Der Eigentümer des Videos hat das Video nicht für eingebettete Anzeige freigegeben."],"An unknown error occurred while playing back the video (%s). Please try again later.":["Beim Abspielen des Videos ist ein unbekannter Fehler aufgetreten (%s). Bitte versuchen Sie es später wieder."],"An unknown error occurred while playing back the video. Please try again later.":["Beim Abspielen des Videos ist ein unbekannter Fehler aufgetreten. Bitte versuchen Sie es später wieder."],"Unknown URL format. Please make sure to enter a valid YouTube URL.":["Unbekanntes URL-Format. Bitte geben Sie eine gültige YouTube URL ein."],"Error":["Fehler"],"Hint":["Hinweis"],"Please confirm":["Bitte bestätigen"],"More information required":["Weitere Informationen nötig"],"Ok":["OK"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":["Die Bildschrimfreigabe benötigt eine Browser-Erweiterung. Bitte fügen Sie die \"Spreed WebRTC screen sharing\" Erweiterung zu Chrome hinzu."],"Access code required":["Bitte Zugriffscode eingeben"],"Access denied":["Zugriff verweigert"],"Please provide a valid access code.":["Bitte geben Sie einen gültigen Zugriffscode ein."],"Failed to verify access code. Check your Internet connection and try again.":["Der Zugriffscode konnte nicht überprueft werden. Bitte prüfen Sie Ihre Internetverbindung."],"PIN for room %s is now '%s'.":["PIN für Raum %s ist jetzt '%s'."],"PIN lock has been removed from room %s.":["Raum %s ist nicht mehr PIN-geschützt."],"Enter the PIN for room %s":["Geben Sie die PIN für Raum %s ein"],"Please sign in to create rooms.":["Bitte melden Sie sich an um Räume zu erstellen."],"and %s":["und %s"],"and %d others":["und %d weiteren"],"User":["Teilnehmer"],"Someone":["Unbekannt"],"Me":["Ich"]}}} \ No newline at end of file diff --git a/static/translation/messages-ja.json b/static/translation/messages-ja.json index fc2a831e..d6d3d384 100644 --- a/static/translation/messages-ja.json +++ b/static/translation/messages-ja.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Your audio level":[null,"あなたの音量"],"Standard view":[null,""],"Large view":[null,""],"Kiosk view":[null,""],"Auditorium":[null,""],"Start chat":[null,"チャットを始める"],"Start video call":[null,"テレビ電話を始める"],"Start audio conference":[null,"音声会議を始める"],"No one else here":[null,""],"Take":[null,""],"Retake":[null,""],"Cancel":[null,"キャンセル"],"Set as Profile Picture":[null,""],"Take picture":[null,"写真を取る"],"Waiting for camera":[null,"カメラ待ち"],"The file couldn't be read.":[null,""],"The file is not an image.":[null,""],"The file is too large. Max. %d MB.":[null,""],"Select file":[null,""],"Chat sessions":[null,"チャットのセッション"],"Room chat":[null,"ルームチャット"],"Peer to peer":[null,"ピア・ツー・ピア"],"Close chat":[null,"チャットを終える"],"Share my location":[null,""],"is typing...":[null,"は入力中です..."],"has stopped typing...":[null,"は入力を止めました..."],"Type here to chat...":[null,"ここに入力してチャット開始します..."],"Send":[null,"送信"],"Accept":[null,""],"Reject":[null,"拒否"],"You have no contacts.":[null,""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[null,""],"Edit contact":[null,""],"Edit":[null,""],"Name":[null,"名前"],"Remove":[null,""],"Refresh":[null,""],"Save":[null,""],"Close":[null,"閉じる"],"File sharing":[null,"ファイル共有"],"File is no longer available":[null,"ファイルは有効ではありません"],"Download":[null,"ダウンロード"],"Open":[null,"開く"],"Unshare":[null,"共有取り消し"],"Retry":[null,"リトライ"],"Download failed.":[null,"ダウンロード失敗."],"Share a YouTube video":[null,""],"Share a file as presentation":[null,""],"Share your screen":[null,"画面を共有する."],"Chat":[null,"チャット"],"Contacts":[null,""],"Mute microphone":[null,"消音"],"Turn camera off":[null,"カメラをオフにする"],"Settings":[null,"設定"],"Loading presentation ...":[null,""],"Please upload a document":[null,""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[null,""],"Upload":[null,""],"You can drag files here too.":[null,""],"Presentation controls":[null,""],"Prev":[null,""],"Next":[null,""],"Change room":[null,"ルームチェンジ"],"Room":[null,"ルーム"],"Leave room":[null,"ルームを出る"],"Main":[null,"メイン"],"Current room":[null,"現在のルーム"],"Screen sharing options":[null,"画面共有オプション"],"Fit screen.":[null,"画面に合わせる"],"Profile":[null,""],"Your name":[null,"あなたの名前"],"Your picture":[null,"あなたの写真"],"Status message":[null,""],"What's on your mind?":[null,""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[null,""],"Your ID":[null,""],"Register":[null,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Sign in":[null,""],"Create an account":[null,""],"Sign out":[null,""],"Manage account":[null,""],"Media":[null,""],"Microphone":[null,"マイク"],"Camera":[null,"カメラ"],"Video quality":[null,"ビデオ画質"],"Low":[null,"低い"],"High":[null,"高い"],"HD":[null,"HD"],"Full HD":[null,""],"General":[null,""],"Language":[null,"言語"],"Language changes become active on reload.":[null,"言語の変更は再読み込み時に適用となります."],"Default room":[null,"デフォルト・ルーム"],"Set alternative room to join at start.":[null,"スタート時に別のルームに参加する."],"Desktop notification":[null,"デスクトップ通知"],"Enable":[null,"有効にする"],"Denied - check your browser settings":[null,"拒否 - ブラウザ設定を確認して下さい"],"Allowed":[null,"許可"],"Advanced settings":[null,"詳細設定"],"Play audio on same device as selected microphone":[null,""],"Experimental AEC":[null,""],"Experimental AGC":[null,""],"Experimental noise suppression":[null,""],"Max video frame rate":[null,"ビデオ最高フレームレート"],"auto":[null,"自動"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[null,""],"Detect CPU over use":[null,""],"Automatically reduces video quality as needed.":[null,""],"Optimize for high resolution video":[null,""],"Reduce video noise":[null,""],"Enable experiments":[null,""],"Show advanced settings":[null,"詳細設定を表示"],"Hide advanced settings":[null,"詳細設定を隠す"],"Remember settings":[null,"設定を保存"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Room link":[null,""],"Invite with Facebook":[null,""],"Invite with Twitter":[null,""],"Invite with Google Plus":[null,""],"Invite with XING":[null,""],"Initializing":[null,"初期化中"],"Online":[null,"オンライン"],"Calling":[null,"発信中"],"Hangup":[null,"切断"],"In call with":[null,"と会話中"],"Conference with":[null,"と会議中"],"Your are offline":[null,"オフラインです"],"Go online":[null,"オンラインにする"],"Connection interrupted":[null,"接続は中断されました"],"An error occured":[null,"エラーが発生しました"],"Incoming call":[null,"着信中"],"from":[null,"から"],"Accept call":[null,"通話"],"Waiting for camera/microphone access":[null,"カメラ・マイクの接続待ち."],"Checking camera and microphone access.":[null,"カメラ・マイクの接続確認中."],"Please allow access to your camera and microphone.":[null,"カメラとマイクの接続を許可してください."],"Camera / microphone access required.":[null,"カメラ・マイクの接続が必要です."],"Please check your browser settings and allow camera and microphone access for this site.":[null,"ブラウザ設定で、このサイトへのカメラ・マイクの接続を許可してください."],"Skip check":[null,"チェックをスキップ"],"Click here for help (Google Chrome).":[null,"ここをクリックしてヘルプ表示(Google Chrome)"],"Please set your user details and settings.":[null,"あなたのプロフィールとアプリの動作を設定してください."],"Enter a room name":[null,""],"Random room name":[null,""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[null,""],"Room history":[null,""],"Please sign in.":[null,""],"Videos play simultaneously for everyone in this call.":[null,""],"YouTube URL":[null,""],"Could not load YouTube player API, please check your network / firewall settings.":[null,""],"Currently playing":[null,""],"YouTube controls":[null,""],"YouTube video to share":[null,""],"Peer to peer chat active.":[null,"ピア・ツー・ピア・チャットがアクティブです."],"Peer to peer chat is now off.":[null,"ピア・ツー・ピア・チャットがオフです."]," is now offline.":[null,"は今オフラインです"]," is now online.":[null,"は今オンラインです"],"You share file:":[null,"あなたの共有ファイル:"],"Incoming file:":[null,"受信中ファイル:"],"You shared your location:":[null,""],"Location received:":[null,""],"You accepted the contact request.":[null,""],"You rejected the contact request.":[null,""],"You sent a contact request.":[null,""],"Your contact request was accepted.":[null,""],"Incoming contact request.":[null,""],"Your contact request was rejected.":[null,""],"Edit Contact":[null,""],"Close this window and disconnect?":[null,""],"Contacts Manager":[null,""],"Restart required to apply updates. Click ok to restart now.":[null,"アップデート適用のため再起動してください.ここをクリックして再起動する."],"Failed to access camera/microphone.":[null,"カメラ・マイクへの接続に失敗しました."],"Failed to establish peer connection.":[null,"ピアとの接続に失敗しました."],"We are sorry but something went wrong. Boo boo.":[null,"申し訳ないのですが、不具合が生じました。"],"Oops":[null,"しまった"],"Peer connection failed. Check your settings.":[null,"ピア接続に失敗しました.設定を確認してください."],"User hung up because of error.":[null,"エラーのため切断しました."]," is busy. Try again later.":[null,"は話中です.後で掛けなおしてください."]," rejected your call.":[null,"着信拒否されました."]," does not pick up.":[null,"は電話にでません."],"Your browser does not support WebRTC. No calls possible.":[null,"ブラウザがWebRTCをサポートしていない為通話はできません."],"Chat with":[null,"とチャットする"],"Message from ":[null,"からのメッセージ"],"You are now in room %s ...":[null,"あなたは%sのルームにいます..."],"Your browser does not support file transfer.":[null,"ブラウザがファイル転送をサポートしていません."],"Could not load PDF: Please make sure to select a PDF document.":[null,""],"Could not load PDF: Missing PDF file.":[null,""],"An error occurred while loading the PDF (%s).":[null,""],"An unknown error occurred while loading the PDF.":[null,""],"An error occurred while loading the PDF page (%s).":[null,""],"An unknown error occurred while loading the PDF page.":[null,""],"An error occurred while rendering the PDF page (%s).":[null,""],"An unknown error occurred while rendering the PDF page.":[null,""],"Only PDF documents and OpenDocument files can be shared at this time.":[null,""],"Failed to start screen sharing (%s).":[null,""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"画面共有は拒否されました.ブラウザの画面共有の設定を確認して下さい. Chromeのアドレスバーに chrome://flags/#enable-usermedia-screen-capture を入力して開き、スクリーンキャプチャのサポートを有効にしてください。その後ブラウザを再起動してください。"],"Permission to start screen sharing was denied.":[null,""],"Use browser language":[null,"ブラウザの言語を使用"],"Meet with me here:":[null,"ここで私と会う:"],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[null,""],"Error":[null,"エラー"],"Hint":[null,"ヒント"],"Please confirm":[null,"確認して下さい"],"More information required":[null,"さらなる情報が必要です"],"Ok":[null,"OK"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[null,""],"Access code required":[null,"アクセスコードが必要です"],"Access denied":[null,"アクセスが拒否されました"],"Please provide a valid access code.":[null,"有効なアクセスコードを入力してください."],"Failed to verify access code. Check your Internet connection and try again.":[null,"アクセスコードの確認に失敗しました.インターネット接続を確認してリトライしてください."],"PIN for room %s is now '%s'.":[null,""],"PIN lock has been removed from room %s.":[null,""],"Enter the PIN for room %s":[null,""],"Please sign in to create rooms.":[null,""],"and %d others":[null,""],"User":[null,"ユーザー"],"Someone":[null,"誰か"],"Me":[null,"私"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Standard view":[""],"Large view":[""],"Kiosk view":[""],"Auditorium":[""],"Start chat":["チャットを始める"],"Start video call":["テレビ電話を始める"],"Start audio conference":["音声会議を始める"],"No one else here":[""],"Take":[""],"Retake":[""],"Cancel":["キャンセル"],"Set as Profile Picture":[""],"Take picture":["写真を取る"],"Waiting for camera":["カメラ待ち"],"The file couldn't be read.":[""],"The file is not an image.":[""],"The file is too large. Max. %d MB.":[""],"Select file":[""],"Chat sessions":["チャットのセッション"],"Room chat":["ルームチャット"],"Peer to peer":["ピア・ツー・ピア"],"Close chat":["チャットを終える"],"Share my location":[""],"is typing...":["は入力中です..."],"has stopped typing...":["は入力を止めました..."],"Type here to chat...":["ここに入力してチャット開始します..."],"Send":["送信"],"Accept":[""],"Reject":["拒否"],"You have no contacts.":[""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[""],"Edit contact":[""],"Edit":[""],"Name":["名前"],"Remove":[""],"Refresh":[""],"Save":[""],"Close":["閉じる"],"File sharing":["ファイル共有"],"File is no longer available":["ファイルは有効ではありません"],"Download":["ダウンロード"],"Open":["開く"],"Unshare":["共有取り消し"],"Retry":["リトライ"],"Download failed.":["ダウンロード失敗."],"Share a YouTube video":[""],"Share a file as presentation":[""],"Share your screen":["画面を共有する."],"Chat":["チャット"],"Contacts":[""],"Mute microphone":["消音"],"Turn camera off":["カメラをオフにする"],"Settings":["設定"],"Loading presentation ...":[""],"Please upload a document":[""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[""],"Upload":[""],"You can drag files here too.":[""],"Presentation controls":[""],"Prev":[""],"Next":[""],"Change room":["ルームチェンジ"],"Room":["ルーム"],"Leave room":["ルームを出る"],"Main":["メイン"],"Current room":["現在のルーム"],"Screen sharing options":["画面共有オプション"],"Fit screen.":["画面に合わせる"],"Please select what to share.":[""],"Window":[""],"Application":[""],"Share the whole screen. Click share to select the screen.":[""],"Share a single window. Click share to select the window.":[""],"Share all windows of a application. This can leak content behind windows when windows get moved. Click share to select the application.":[""],"Profile":[""],"Your name":["あなたの名前"],"Your picture":["あなたの写真"],"Status message":[""],"What's on your mind?":[""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[""],"Your ID":[""],"Register":[""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[""],"Sign in":[""],"Create an account":[""],"Sign out":[""],"Manage account":[""],"Media":[""],"Microphone":["マイク"],"Camera":["カメラ"],"Video quality":["ビデオ画質"],"Low":["低い"],"High":["高い"],"HD":["HD"],"Full HD":[""],"General":[""],"Language":["言語"],"Language changes become active on reload.":["言語の変更は再読み込み時に適用となります."],"Default room":["デフォルト・ルーム"],"Set alternative room to join at start.":["スタート時に別のルームに参加する."],"Desktop notification":["デスクトップ通知"],"Enable":["有効にする"],"Denied - check your browser settings":["拒否 - ブラウザ設定を確認して下さい"],"Allowed":["許可"],"Advanced settings":["詳細設定"],"Play audio on same device as selected microphone":[""],"Experimental AEC":[""],"Experimental AGC":[""],"Experimental noise suppression":[""],"Max video frame rate":["ビデオ最高フレームレート"],"auto":["自動"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[""],"Detect CPU over use":[""],"Automatically reduces video quality as needed.":[""],"Optimize for high resolution video":[""],"Reduce video noise":[""],"Enable experiments":[""],"Show advanced settings":["詳細設定を表示"],"Hide advanced settings":["詳細設定を隠す"],"Remember settings":["設定を保存"],"Your ID will still be kept - press the log out button above to delete the ID.":[""],"Room link":[""],"Invite with Facebook":[""],"Invite with Twitter":[""],"Invite with Google Plus":[""],"Invite with XING":[""],"Initializing":["初期化中"],"Online":["オンライン"],"Calling":["発信中"],"Hangup":["切断"],"In call with":["と会話中"],"Conference with":["と会議中"],"Your are offline":["オフラインです"],"Go online":["オンラインにする"],"Connection interrupted":["接続は中断されました"],"An error occured":["エラーが発生しました"],"Incoming call":["着信中"],"from":["から"],"Accept call":["通話"],"Waiting for camera/microphone access":["カメラ・マイクの接続待ち."],"Your audio level":["あなたの音量"],"Checking camera and microphone access.":["カメラ・マイクの接続確認中."],"Please allow access to your camera and microphone.":["カメラとマイクの接続を許可してください."],"Camera / microphone access required.":["カメラ・マイクの接続が必要です."],"Please check your browser settings and allow camera and microphone access for this site.":["ブラウザ設定で、このサイトへのカメラ・マイクの接続を許可してください."],"Skip check":["チェックをスキップ"],"Click here for help (Google Chrome).":["ここをクリックしてヘルプ表示(Google Chrome)"],"Please set your user details and settings.":["あなたのプロフィールとアプリの動作を設定してください."],"Enter a room name":[""],"Random room name":[""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[""],"Room history":[""],"Please sign in.":[""],"Videos play simultaneously for everyone in this call.":[""],"YouTube URL":[""],"Could not load YouTube player API, please check your network / firewall settings.":[""],"Currently playing":[""],"YouTube controls":[""],"YouTube video to share":[""],"Peer to peer chat active.":["ピア・ツー・ピア・チャットがアクティブです."],"Peer to peer chat is now off.":["ピア・ツー・ピア・チャットがオフです."]," is now offline.":["は今オフラインです"]," is now online.":["は今オンラインです"],"You share file:":["あなたの共有ファイル:"],"Incoming file:":["受信中ファイル:"],"You shared your location:":[""],"Location received:":[""],"You accepted the contact request.":[""],"You rejected the contact request.":[""],"You sent a contact request.":[""],"Your contact request was accepted.":[""],"Incoming contact request.":[""],"Your contact request was rejected.":[""],"Edit Contact":[""],"Close this window and disconnect?":[""],"Contacts Manager":[""],"Restart required to apply updates. Click ok to restart now.":["アップデート適用のため再起動してください.ここをクリックして再起動する."],"Failed to access camera/microphone.":["カメラ・マイクへの接続に失敗しました."],"Failed to establish peer connection.":["ピアとの接続に失敗しました."],"We are sorry but something went wrong. Boo boo.":["申し訳ないのですが、不具合が生じました。"],"Oops":["しまった"],"Peer connection failed. Check your settings.":["ピア接続に失敗しました.設定を確認してください."],"User hung up because of error.":["エラーのため切断しました."]," is busy. Try again later.":["は話中です.後で掛けなおしてください."]," rejected your call.":["着信拒否されました."]," does not pick up.":["は電話にでません."],"Your browser does not support WebRTC. No calls possible.":["ブラウザがWebRTCをサポートしていない為通話はできません."],"Chat with":["とチャットする"],"Message from ":["からのメッセージ"],"You are now in room %s ...":["あなたは%sのルームにいます..."],"Your browser does not support file transfer.":["ブラウザがファイル転送をサポートしていません."],"Could not load PDF: Please make sure to select a PDF document.":[""],"Could not load PDF: Missing PDF file.":[""],"An error occurred while loading the PDF (%s).":[""],"An unknown error occurred while loading the PDF.":[""],"An error occurred while loading the PDF page (%s).":[""],"An unknown error occurred while loading the PDF page.":[""],"An error occurred while rendering the PDF page (%s).":[""],"An unknown error occurred while rendering the PDF page.":[""],"Only PDF documents and OpenDocument files can be shared at this time.":[""],"Failed to start screen sharing (%s).":[""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":["画面共有は拒否されました.ブラウザの画面共有の設定を確認して下さい. Chromeのアドレスバーに chrome://flags/#enable-usermedia-screen-capture を入力して開き、スクリーンキャプチャのサポートを有効にしてください。その後ブラウザを再起動してください。"],"Permission to start screen sharing was denied.":[""],"Use browser language":["ブラウザの言語を使用"],"Meet with me here:":["ここで私と会う:"],"The request contains an invalid parameter value. Please check the URL of the video you want to share and try again.":[""],"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred. Please try again later.":[""],"The video requested was not found. Please check the URL of the video you want to share and try again.":[""],"The owner of the requested video does not allow it to be played in embedded players.":[""],"An unknown error occurred while playing back the video (%s). Please try again later.":[""],"An unknown error occurred while playing back the video. Please try again later.":[""],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[""],"Error":["エラー"],"Hint":["ヒント"],"Please confirm":["確認して下さい"],"More information required":["さらなる情報が必要です"],"Ok":["OK"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[""],"Access code required":["アクセスコードが必要です"],"Access denied":["アクセスが拒否されました"],"Please provide a valid access code.":["有効なアクセスコードを入力してください."],"Failed to verify access code. Check your Internet connection and try again.":["アクセスコードの確認に失敗しました.インターネット接続を確認してリトライしてください."],"PIN for room %s is now '%s'.":[""],"PIN lock has been removed from room %s.":[""],"Enter the PIN for room %s":[""],"Please sign in to create rooms.":[""],"and %d others":[""],"User":["ユーザー"],"Someone":["誰か"],"Me":["私"]}}} \ No newline at end of file diff --git a/static/translation/messages-ko.json b/static/translation/messages-ko.json index 67823d8b..249c1086 100644 --- a/static/translation/messages-ko.json +++ b/static/translation/messages-ko.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Your audio level":[null,"음성크기"],"Standard view":[null,""],"Large view":[null,""],"Kiosk view":[null,""],"Auditorium":[null,""],"Start chat":[null,"대화시작"],"Start video call":[null,"화상회의 시작"],"Start audio conference":[null,"음성회의 시작"],"No one else here":[null,""],"Take":[null,""],"Retake":[null,""],"Cancel":[null,"취소"],"Set as Profile Picture":[null,""],"Take picture":[null,"사진 찍음"],"Waiting for camera":[null,"카메라 대기중"],"The file couldn't be read.":[null,""],"The file is not an image.":[null,""],"The file is too large. Max. %d MB.":[null,""],"Select file":[null,""],"Chat sessions":[null,"대화 세션"],"Room chat":[null,"대화 방"],"Peer to peer":[null,"일대일"],"Close chat":[null,"대화 종료"],"Share my location":[null,""],"is typing...":[null,"입력중"],"has stopped typing...":[null,"입력 종료"],"Type here to chat...":[null,"대화 입력"],"Send":[null,"전송"],"Accept":[null,""],"Reject":[null,"거부"],"You have no contacts.":[null,""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[null,""],"Edit contact":[null,""],"Edit":[null,""],"Name":[null,"이름"],"Remove":[null,""],"Refresh":[null,""],"Save":[null,""],"Close":[null,"닫음"],"File sharing":[null,"회일 공유"],"File is no longer available":[null,"화일이 유효하지 않습니다"],"Download":[null,"다운로드"],"Open":[null,"열기"],"Unshare":[null,"비공유"],"Retry":[null,"재시도"],"Download failed.":[null,"다운로드실패"],"Share a YouTube video":[null,""],"Share a file as presentation":[null,""],"Share your screen":[null,"화면 공유하기"],"Chat":[null,"대화"],"Contacts":[null,""],"Mute microphone":[null,"음성제거"],"Turn camera off":[null,"카메라꺼짐"],"Settings":[null,"설정"],"Loading presentation ...":[null,""],"Please upload a document":[null,""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[null,""],"Upload":[null,""],"You can drag files here too.":[null,""],"Presentation controls":[null,""],"Prev":[null,""],"Next":[null,""],"Change room":[null,"방 변경"],"Room":[null,"방"],"Leave room":[null,"방 이동"],"Main":[null,"메인"],"Current room":[null,"현재 방"],"Screen sharing options":[null,"화면 공유 옵션"],"Fit screen.":[null,"화면에 맟춤"],"Profile":[null,""],"Your name":[null,"사용자 이름"],"Your picture":[null,"사용자 사진"],"Status message":[null,""],"What's on your mind?":[null,""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[null,""],"Your ID":[null,""],"Register":[null,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Sign in":[null,""],"Create an account":[null,""],"Sign out":[null,""],"Manage account":[null,""],"Media":[null,""],"Microphone":[null,"마이크"],"Camera":[null,"카메라"],"Video quality":[null,"영상 수준"],"Low":[null,"낮음"],"High":[null,"높음"],"HD":[null,"고화질"],"Full HD":[null,""],"General":[null,""],"Language":[null,"언어"],"Language changes become active on reload.":[null,"언어 변경이 재로드 되고 있습니다"],"Default room":[null,"기본 방"],"Set alternative room to join at start.":[null,"시작시에 다른 방에 합류하도록 설정 되었습니다"],"Desktop notification":[null,"데스크탑에 통보"],"Enable":[null,"활성화"],"Denied - check your browser settings":[null,"거부됨 - 브라우저 설정을 확인하세요"],"Allowed":[null,"허락됨"],"Advanced settings":[null,"고급 설정"],"Play audio on same device as selected microphone":[null,""],"Experimental AEC":[null,""],"Experimental AGC":[null,""],"Experimental noise suppression":[null,""],"Max video frame rate":[null,"비디오프레임 비율 최대화"],"auto":[null,"자동"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[null,""],"Detect CPU over use":[null,""],"Automatically reduces video quality as needed.":[null,""],"Optimize for high resolution video":[null,""],"Reduce video noise":[null,""],"Enable experiments":[null,""],"Show advanced settings":[null,"고급 설정 보기"],"Hide advanced settings":[null,"고급 설정 감추기"],"Remember settings":[null,"설정 기억"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Room link":[null,""],"Invite with Facebook":[null,""],"Invite with Twitter":[null,""],"Invite with Google Plus":[null,""],"Invite with XING":[null,""],"Initializing":[null,"초기화"],"Online":[null,"온라인"],"Calling":[null,"전화걸기"],"Hangup":[null,"전화끊기"],"In call with":[null,"전화중"],"Conference with":[null,"회의중"],"Your are offline":[null,"오프라인 입니다"],"Go online":[null,"온라인에 연결합니다"],"Connection interrupted":[null,"연결이 중단"],"An error occured":[null,"에러 발생"],"Incoming call":[null,"전화 걸려옴"],"from":[null,"부터"],"Accept call":[null,"전화 받음"],"Waiting for camera/microphone access":[null,"카메라/마이크 사용을 기다림"],"Checking camera and microphone access.":[null,"카메라와 마이크의 사용을 확인 하세요"],"Please allow access to your camera and microphone.":[null,"카메라와 마이크의 사용을 허용 하세요"],"Camera / microphone access required.":[null,"카메라/마이크 사용이 필요합니다"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"이 사이트에 대하여 브라우저의 설정을 확인하고 카메라와 마이크의 사용을 허용 하세요"],"Skip check":[null,"확인 넘어가기"],"Click here for help (Google Chrome).":[null,"도움말을 원하면 여기를 클릭 하세요 (구글 크롬)"],"Please set your user details and settings.":[null,"사용자의 세부상세와 설정을 지정하세요 "],"Enter a room name":[null,""],"Random room name":[null,""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[null,""],"Room history":[null,""],"Please sign in.":[null,""],"Videos play simultaneously for everyone in this call.":[null,""],"YouTube URL":[null,""],"Could not load YouTube player API, please check your network / firewall settings.":[null,""],"Currently playing":[null,""],"YouTube controls":[null,""],"YouTube video to share":[null,""],"Peer to peer chat active.":[null,"일대일 대화 활성화"],"Peer to peer chat is now off.":[null,"일대일 대화 꺼짐"]," is now offline.":[null,"현재 오프라인 상태"]," is now online.":[null,"현재 온라인 상태"],"You share file:":[null,"공유 화일:"],"Incoming file:":[null,"도착하는 화일:"],"You shared your location:":[null,""],"Location received:":[null,""],"You accepted the contact request.":[null,""],"You rejected the contact request.":[null,""],"You sent a contact request.":[null,""],"Your contact request was accepted.":[null,""],"Incoming contact request.":[null,""],"Your contact request was rejected.":[null,""],"Edit Contact":[null,""],"Close this window and disconnect?":[null,""],"Contacts Manager":[null,""],"Restart required to apply updates. Click ok to restart now.":[null,"업데이트를 적용하려면 재시작이 필요 합니다. 지금 재시작 하려면 ok를 클릭 하십시오"],"Failed to access camera/microphone.":[null,"카메라/마이크 사용 실패"],"Failed to establish peer connection.":[null,"상대연결 설정이 실패 하였습니다"],"We are sorry but something went wrong. Boo boo.":[null,"죄송합니다만 현재 문제가 있습니다."],"Oops":[null,"이런"],"Peer connection failed. Check your settings.":[null,"상대연결이 실패 했습니다. 설정을 확인 하십시오"],"User hung up because of error.":[null,"오류로 인해 사용자 끊어짐"]," is busy. Try again later.":[null,"통화중. 다시 시도 하세요."]," rejected your call.":[null,"전화가 거부 되었습니다."]," does not pick up.":[null,"전화를 받지 않습니다."],"Your browser does not support WebRTC. No calls possible.":[null,"브라우저가 WebRTC를 지원하지 않습니다. 전화걸기가 불가능 합니다."],"Chat with":[null,"대화하기"],"Message from ":[null,"로 부터 메시지"],"You are now in room %s ...":[null,"당신은 현재 방%s ...에 있습니다"],"Your browser does not support file transfer.":[null,"당신의 브라우저가 회일전송을 지원하지 않습니다."],"Could not load PDF: Please make sure to select a PDF document.":[null,""],"Could not load PDF: Missing PDF file.":[null,""],"An error occurred while loading the PDF (%s).":[null,""],"An unknown error occurred while loading the PDF.":[null,""],"An error occurred while loading the PDF page (%s).":[null,""],"An unknown error occurred while loading the PDF page.":[null,""],"An error occurred while rendering the PDF page (%s).":[null,""],"An unknown error occurred while rendering the PDF page.":[null,""],"Only PDF documents and OpenDocument files can be shared at this time.":[null,""],"Failed to start screen sharing (%s).":[null,""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"화면공유가 거절되었습니다. 사용하시는 브라우저에서 화면공유를 가능하도록 하여 주십시오. chrome://flags/#enable-usermedia-screen-capture를 복사하여 브라우저에서 수행하시고 상단의 프래그를 가능으로 변경 하십시오. 브라우저를 다시 수행시키면 사용하실수 있습니다."],"Permission to start screen sharing was denied.":[null,""],"Use browser language":[null,"브라우저 언어 사용"],"Meet with me here:":[null,"나를 여기서 만납니다:"],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[null,""],"Error":[null,"오류"],"Hint":[null,"도움말"],"Please confirm":[null,"확인하십시오"],"More information required":[null,"더 많은 정보가 필요함"],"Ok":[null,"오케이"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[null,""],"Access code required":[null,"접속코드 필요함"],"Access denied":[null,"접속 거부"],"Please provide a valid access code.":[null,"유효한 접속코드가 필요합니다."],"Failed to verify access code. Check your Internet connection and try again.":[null,"접속코드 확인이 실패 했습니다. 인터넷 연결을 확인하고 다시 시도해 주십시오. "],"PIN for room %s is now '%s'.":[null,""],"PIN lock has been removed from room %s.":[null,""],"Enter the PIN for room %s":[null,""],"Please sign in to create rooms.":[null,""],"and %d others":[null,""],"User":[null,"사용자"],"Someone":[null,"어떤 사람"],"Me":[null,"나"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Standard view":[""],"Large view":[""],"Kiosk view":[""],"Auditorium":[""],"Start chat":["대화시작"],"Start video call":["화상회의 시작"],"Start audio conference":["음성회의 시작"],"No one else here":[""],"Take":[""],"Retake":[""],"Cancel":["취소"],"Set as Profile Picture":[""],"Take picture":["사진 찍음"],"Waiting for camera":["카메라 대기중"],"The file couldn't be read.":[""],"The file is not an image.":[""],"The file is too large. Max. %d MB.":[""],"Select file":[""],"Chat sessions":["대화 세션"],"Room chat":["대화 방"],"Peer to peer":["일대일"],"Close chat":["대화 종료"],"Share my location":[""],"is typing...":["입력중"],"has stopped typing...":["입력 종료"],"Type here to chat...":["대화 입력"],"Send":["전송"],"Accept":[""],"Reject":["거부"],"You have no contacts.":[""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[""],"Edit contact":[""],"Edit":[""],"Name":["이름"],"Remove":[""],"Refresh":[""],"Save":[""],"Close":["닫음"],"File sharing":["회일 공유"],"File is no longer available":["화일이 유효하지 않습니다"],"Download":["다운로드"],"Open":["열기"],"Unshare":["비공유"],"Retry":["재시도"],"Download failed.":["다운로드실패"],"Share a YouTube video":[""],"Share a file as presentation":[""],"Share your screen":["화면 공유하기"],"Chat":["대화"],"Contacts":[""],"Mute microphone":["음성제거"],"Turn camera off":["카메라꺼짐"],"Settings":["설정"],"Loading presentation ...":[""],"Please upload a document":[""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[""],"Upload":[""],"You can drag files here too.":[""],"Presentation controls":[""],"Prev":[""],"Next":[""],"Change room":["방 변경"],"Room":["방"],"Leave room":["방 이동"],"Main":["메인"],"Current room":["현재 방"],"Screen sharing options":["화면 공유 옵션"],"Fit screen.":["화면에 맟춤"],"Please select what to share.":[""],"Window":[""],"Application":[""],"Share the whole screen. Click share to select the screen.":[""],"Share a single window. Click share to select the window.":[""],"Share all windows of a application. This can leak content behind windows when windows get moved. Click share to select the application.":[""],"Profile":[""],"Your name":["사용자 이름"],"Your picture":["사용자 사진"],"Status message":[""],"What's on your mind?":[""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[""],"Your ID":[""],"Register":[""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[""],"Sign in":[""],"Create an account":[""],"Sign out":[""],"Manage account":[""],"Media":[""],"Microphone":["마이크"],"Camera":["카메라"],"Video quality":["영상 수준"],"Low":["낮음"],"High":["높음"],"HD":["고화질"],"Full HD":[""],"General":[""],"Language":["언어"],"Language changes become active on reload.":["언어 변경이 재로드 되고 있습니다"],"Default room":["기본 방"],"Set alternative room to join at start.":["시작시에 다른 방에 합류하도록 설정 되었습니다"],"Desktop notification":["데스크탑에 통보"],"Enable":["활성화"],"Denied - check your browser settings":["거부됨 - 브라우저 설정을 확인하세요"],"Allowed":["허락됨"],"Advanced settings":["고급 설정"],"Play audio on same device as selected microphone":[""],"Experimental AEC":[""],"Experimental AGC":[""],"Experimental noise suppression":[""],"Max video frame rate":["비디오프레임 비율 최대화"],"auto":["자동"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[""],"Detect CPU over use":[""],"Automatically reduces video quality as needed.":[""],"Optimize for high resolution video":[""],"Reduce video noise":[""],"Enable experiments":[""],"Show advanced settings":["고급 설정 보기"],"Hide advanced settings":["고급 설정 감추기"],"Remember settings":["설정 기억"],"Your ID will still be kept - press the log out button above to delete the ID.":[""],"Room link":[""],"Invite with Facebook":[""],"Invite with Twitter":[""],"Invite with Google Plus":[""],"Invite with XING":[""],"Initializing":["초기화"],"Online":["온라인"],"Calling":["전화걸기"],"Hangup":["전화끊기"],"In call with":["전화중"],"Conference with":["회의중"],"Your are offline":["오프라인 입니다"],"Go online":["온라인에 연결합니다"],"Connection interrupted":["연결이 중단"],"An error occured":["에러 발생"],"Incoming call":["전화 걸려옴"],"from":["부터"],"Accept call":["전화 받음"],"Waiting for camera/microphone access":["카메라/마이크 사용을 기다림"],"Your audio level":["음성크기"],"Checking camera and microphone access.":["카메라와 마이크의 사용을 확인 하세요"],"Please allow access to your camera and microphone.":["카메라와 마이크의 사용을 허용 하세요"],"Camera / microphone access required.":["카메라/마이크 사용이 필요합니다"],"Please check your browser settings and allow camera and microphone access for this site.":["이 사이트에 대하여 브라우저의 설정을 확인하고 카메라와 마이크의 사용을 허용 하세요"],"Skip check":["확인 넘어가기"],"Click here for help (Google Chrome).":["도움말을 원하면 여기를 클릭 하세요 (구글 크롬)"],"Please set your user details and settings.":["사용자의 세부상세와 설정을 지정하세요 "],"Enter a room name":[""],"Random room name":[""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[""],"Room history":[""],"Please sign in.":[""],"Videos play simultaneously for everyone in this call.":[""],"YouTube URL":[""],"Could not load YouTube player API, please check your network / firewall settings.":[""],"Currently playing":[""],"YouTube controls":[""],"YouTube video to share":[""],"Peer to peer chat active.":["일대일 대화 활성화"],"Peer to peer chat is now off.":["일대일 대화 꺼짐"]," is now offline.":["현재 오프라인 상태"]," is now online.":["현재 온라인 상태"],"You share file:":["공유 화일:"],"Incoming file:":["도착하는 화일:"],"You shared your location:":[""],"Location received:":[""],"You accepted the contact request.":[""],"You rejected the contact request.":[""],"You sent a contact request.":[""],"Your contact request was accepted.":[""],"Incoming contact request.":[""],"Your contact request was rejected.":[""],"Edit Contact":[""],"Close this window and disconnect?":[""],"Contacts Manager":[""],"Restart required to apply updates. Click ok to restart now.":["업데이트를 적용하려면 재시작이 필요 합니다. 지금 재시작 하려면 ok를 클릭 하십시오"],"Failed to access camera/microphone.":["카메라/마이크 사용 실패"],"Failed to establish peer connection.":["상대연결 설정이 실패 하였습니다"],"We are sorry but something went wrong. Boo boo.":["죄송합니다만 현재 문제가 있습니다."],"Oops":["이런"],"Peer connection failed. Check your settings.":["상대연결이 실패 했습니다. 설정을 확인 하십시오"],"User hung up because of error.":["오류로 인해 사용자 끊어짐"]," is busy. Try again later.":["통화중. 다시 시도 하세요."]," rejected your call.":["전화가 거부 되었습니다."]," does not pick up.":["전화를 받지 않습니다."],"Your browser does not support WebRTC. No calls possible.":["브라우저가 WebRTC를 지원하지 않습니다. 전화걸기가 불가능 합니다."],"Chat with":["대화하기"],"Message from ":["로 부터 메시지"],"You are now in room %s ...":["당신은 현재 방%s ...에 있습니다"],"Your browser does not support file transfer.":["당신의 브라우저가 회일전송을 지원하지 않습니다."],"Could not load PDF: Please make sure to select a PDF document.":[""],"Could not load PDF: Missing PDF file.":[""],"An error occurred while loading the PDF (%s).":[""],"An unknown error occurred while loading the PDF.":[""],"An error occurred while loading the PDF page (%s).":[""],"An unknown error occurred while loading the PDF page.":[""],"An error occurred while rendering the PDF page (%s).":[""],"An unknown error occurred while rendering the PDF page.":[""],"Only PDF documents and OpenDocument files can be shared at this time.":[""],"Failed to start screen sharing (%s).":[""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":["화면공유가 거절되었습니다. 사용하시는 브라우저에서 화면공유를 가능하도록 하여 주십시오. chrome://flags/#enable-usermedia-screen-capture를 복사하여 브라우저에서 수행하시고 상단의 프래그를 가능으로 변경 하십시오. 브라우저를 다시 수행시키면 사용하실수 있습니다."],"Permission to start screen sharing was denied.":[""],"Use browser language":["브라우저 언어 사용"],"Meet with me here:":["나를 여기서 만납니다:"],"The request contains an invalid parameter value. Please check the URL of the video you want to share and try again.":[""],"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred. Please try again later.":[""],"The video requested was not found. Please check the URL of the video you want to share and try again.":[""],"The owner of the requested video does not allow it to be played in embedded players.":[""],"An unknown error occurred while playing back the video (%s). Please try again later.":[""],"An unknown error occurred while playing back the video. Please try again later.":[""],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[""],"Error":["오류"],"Hint":["도움말"],"Please confirm":["확인하십시오"],"More information required":["더 많은 정보가 필요함"],"Ok":["오케이"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[""],"Access code required":["접속코드 필요함"],"Access denied":["접속 거부"],"Please provide a valid access code.":["유효한 접속코드가 필요합니다."],"Failed to verify access code. Check your Internet connection and try again.":["접속코드 확인이 실패 했습니다. 인터넷 연결을 확인하고 다시 시도해 주십시오. "],"PIN for room %s is now '%s'.":[""],"PIN lock has been removed from room %s.":[""],"Enter the PIN for room %s":[""],"Please sign in to create rooms.":[""],"and %d others":[""],"User":["사용자"],"Someone":["어떤 사람"],"Me":["나"]}}} \ No newline at end of file diff --git a/static/translation/messages-zh-cn.json b/static/translation/messages-zh-cn.json index 7cf1921d..b76e81e2 100644 --- a/static/translation/messages-zh-cn.json +++ b/static/translation/messages-zh-cn.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Your audio level":[null,"您的通话音量"],"Standard view":[null,""],"Large view":[null,""],"Kiosk view":[null,""],"Auditorium":[null,""],"Start chat":[null,"开始聊天"],"Start video call":[null,"开始视频通话"],"Start audio conference":[null,"开始语音会议"],"No one else here":[null,""],"Take":[null,""],"Retake":[null,""],"Cancel":[null,"取消"],"Set as Profile Picture":[null,""],"Take picture":[null,"拍照"],"Waiting for camera":[null,"等待启动摄像头"],"The file couldn't be read.":[null,""],"The file is not an image.":[null,""],"The file is too large. Max. %d MB.":[null,""],"Select file":[null,""],"Chat sessions":[null,"会话"],"Room chat":[null,"房间聊天"],"Peer to peer":[null,"P2P"],"Close chat":[null,"关闭聊天"],"Share my location":[null,""],"is typing...":[null,"正在输入..."],"has stopped typing...":[null,"停止输入..."],"Type here to chat...":[null,"在此输入开始聊天..."],"Send":[null,"发送"],"Accept":[null,""],"Reject":[null,"拒绝"],"You have no contacts.":[null,""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[null,""],"Edit contact":[null,""],"Edit":[null,""],"Name":[null,"名字"],"Remove":[null,""],"Refresh":[null,""],"Save":[null,""],"Close":[null,"关闭"],"File sharing":[null,"分享文件"],"File is no longer available":[null,"文件已不存在"],"Download":[null,"下载"],"Open":[null,"打开"],"Unshare":[null,"停止分享"],"Retry":[null,"重试"],"Download failed.":[null,"下载失败"],"Share a YouTube video":[null,""],"Share a file as presentation":[null,""],"Share your screen":[null,"共享您的屏幕"],"Chat":[null,"聊天"],"Contacts":[null,""],"Mute microphone":[null,"关闭麦克风"],"Turn camera off":[null,"关闭摄像头"],"Settings":[null,"系统设置"],"Loading presentation ...":[null,""],"Please upload a document":[null,""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[null,""],"Upload":[null,""],"You can drag files here too.":[null,""],"Presentation controls":[null,""],"Prev":[null,""],"Next":[null,""],"Change room":[null,"更换房间"],"Room":[null,"房间"],"Leave room":[null,"离开房间"],"Main":[null,"主房间"],"Current room":[null,"當前房间"],"Screen sharing options":[null,"屏幕共享设置"],"Fit screen.":[null,"适合屏幕"],"Profile":[null,""],"Your name":[null,"您的名字"],"Your picture":[null,"您的图片"],"Status message":[null,""],"What's on your mind?":[null,""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[null,""],"Your ID":[null,""],"Register":[null,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Sign in":[null,""],"Create an account":[null,""],"Sign out":[null,""],"Manage account":[null,""],"Media":[null,""],"Microphone":[null,"麦克风"],"Camera":[null,"摄像头"],"Video quality":[null,"视频质量"],"Low":[null,"低"],"High":[null,"高"],"HD":[null,"高清"],"Full HD":[null,""],"General":[null,""],"Language":[null,"语言"],"Language changes become active on reload.":[null,"转换语言需重启程序"],"Default room":[null,"系统默认房间"],"Set alternative room to join at start.":[null,"重设初始默认房间"],"Desktop notification":[null,"桌面提醒"],"Enable":[null,"开启"],"Denied - check your browser settings":[null,"被拒绝--请检查浏览器设置"],"Allowed":[null,"启用"],"Advanced settings":[null,"高级设置"],"Play audio on same device as selected microphone":[null,""],"Experimental AEC":[null,""],"Experimental AGC":[null,""],"Experimental noise suppression":[null,""],"Max video frame rate":[null,"最大视频帧速率"],"auto":[null,"自动"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[null,""],"Detect CPU over use":[null,""],"Automatically reduces video quality as needed.":[null,""],"Optimize for high resolution video":[null,""],"Reduce video noise":[null,""],"Enable experiments":[null,""],"Show advanced settings":[null,"展开高级设置"],"Hide advanced settings":[null,"隐藏高级设置"],"Remember settings":[null,"记住设置"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Room link":[null,""],"Invite with Facebook":[null,""],"Invite with Twitter":[null,""],"Invite with Google Plus":[null,""],"Invite with XING":[null,""],"Initializing":[null,"初始化"],"Online":[null,"在线"],"Calling":[null,"呼叫中"],"Hangup":[null,"挂断"],"In call with":[null,"正在和**通话"],"Conference with":[null,"和**会议通话"],"Your are offline":[null,"您不在线"],"Go online":[null,"上线"],"Connection interrupted":[null,"连接已中断"],"An error occured":[null,"出现错误"],"Incoming call":[null,"来电"],"from":[null,"来自"],"Accept call":[null,"接受通话"],"Waiting for camera/microphone access":[null,"等待摄像头/麦克风连接"],"Checking camera and microphone access.":[null,"正在检查摄像头及麦克风连接"],"Please allow access to your camera and microphone.":[null,"请允许连接您的摄像头及麦克风"],"Camera / microphone access required.":[null,"需连接摄像头/麦克风"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"请检查浏览器设置并允许摄像头及麦克风连接此网站"],"Skip check":[null,"越过检查"],"Click here for help (Google Chrome).":[null,"点击这里获取帮助 (Google Chrome)"],"Please set your user details and settings.":[null,"请设定您的用户信息及设置"],"Enter a room name":[null,""],"Random room name":[null,""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[null,""],"Room history":[null,""],"Please sign in.":[null,""],"Videos play simultaneously for everyone in this call.":[null,""],"YouTube URL":[null,""],"Could not load YouTube player API, please check your network / firewall settings.":[null,""],"Currently playing":[null,""],"YouTube controls":[null,""],"YouTube video to share":[null,""],"Peer to peer chat active.":[null,"P2P聊天已启动"],"Peer to peer chat is now off.":[null,"P2P现在未启动"]," is now offline.":[null," 不在线"]," is now online.":[null," 现在在线"],"You share file:":[null,"分享文件:"],"Incoming file:":[null,"发来文件:"],"You shared your location:":[null,""],"Location received:":[null,""],"You accepted the contact request.":[null,""],"You rejected the contact request.":[null,""],"You sent a contact request.":[null,""],"Your contact request was accepted.":[null,""],"Incoming contact request.":[null,""],"Your contact request was rejected.":[null,""],"Edit Contact":[null,""],"Close this window and disconnect?":[null,""],"Contacts Manager":[null,""],"Restart required to apply updates. Click ok to restart now.":[null,"适用更新需重启,现在点击Ok重新启动。"],"Failed to access camera/microphone.":[null,"摄像头/麦克风连接失败"],"Failed to establish peer connection.":[null,"对等连接建立失败"],"We are sorry but something went wrong. Boo boo.":[null,"很抱歉,有错误发生。"],"Oops":[null,"Oops"],"Peer connection failed. Check your settings.":[null,"对等连接失败,请检查设置。"],"User hung up because of error.":[null,"用户因错误挂断"]," is busy. Try again later.":[null," 正在通话,请稍后再试。"]," rejected your call.":[null," 拒绝了您的呼叫。"]," does not pick up.":[null," 无人接听。"],"Your browser does not support WebRTC. No calls possible.":[null,"您的浏览器不支持WebRTC。不能进行通话。"],"Chat with":[null,"与**聊天"],"Message from ":[null,"来自于**的信息"],"You are now in room %s ...":[null,"您在 %s 房间"],"Your browser does not support file transfer.":[null,"您的浏览器不支持文件传输"],"Could not load PDF: Please make sure to select a PDF document.":[null,""],"Could not load PDF: Missing PDF file.":[null,""],"An error occurred while loading the PDF (%s).":[null,""],"An unknown error occurred while loading the PDF.":[null,""],"An error occurred while loading the PDF page (%s).":[null,""],"An unknown error occurred while loading the PDF page.":[null,""],"An error occurred while rendering the PDF page (%s).":[null,""],"An unknown error occurred while rendering the PDF page.":[null,""],"Only PDF documents and OpenDocument files can be shared at this time.":[null,""],"Failed to start screen sharing (%s).":[null,""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"启动屏幕共享许可被拒绝。请确认您已开启浏览器屏幕共享连接。请复制chrome://flags/#enable-usermedia-screen-capture并用您的浏览器打开,启用最上端的功能。然后重启浏览器,操作完成。"],"Permission to start screen sharing was denied.":[null,""],"Use browser language":[null,"使用浏览器语言"],"Meet with me here:":[null,"我们这里见:"],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[null,""],"Error":[null,"错误"],"Hint":[null,"提示"],"Please confirm":[null,"请确认"],"More information required":[null,"需要更多信息"],"Ok":[null,"Ok"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[null,""],"Access code required":[null,"需要接入码"],"Access denied":[null,"连接被拒绝"],"Please provide a valid access code.":[null,"请提供有效接入码"],"Failed to verify access code. Check your Internet connection and try again.":[null,"接入码认证失败。请检查您的网络连接并重试。"],"PIN for room %s is now '%s'.":[null,""],"PIN lock has been removed from room %s.":[null,""],"Enter the PIN for room %s":[null,""],"Please sign in to create rooms.":[null,""],"and %s":[null,""],"and %d others":[null,""],"User":[null,"用户"],"Someone":[null,"某人"],"Me":[null,"我"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Standard view":[""],"Large view":[""],"Kiosk view":[""],"Auditorium":[""],"Start chat":["开始聊天"],"Start video call":["开始视频通话"],"Start audio conference":["开始语音会议"],"No one else here":[""],"Take":[""],"Retake":[""],"Cancel":["取消"],"Set as Profile Picture":[""],"Take picture":["拍照"],"Waiting for camera":["等待启动摄像头"],"The file couldn't be read.":[""],"The file is not an image.":[""],"The file is too large. Max. %d MB.":[""],"Select file":[""],"Chat sessions":["会话"],"Room chat":["房间聊天"],"Peer to peer":["P2P"],"Close chat":["关闭聊天"],"Share my location":[""],"is typing...":["正在输入..."],"has stopped typing...":["停止输入..."],"Type here to chat...":["在此输入开始聊天..."],"Send":["发送"],"Accept":[""],"Reject":["拒绝"],"You have no contacts.":[""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[""],"Edit contact":[""],"Edit":[""],"Name":["名字"],"Remove":[""],"Refresh":[""],"Save":[""],"Close":["关闭"],"File sharing":["分享文件"],"File is no longer available":["文件已不存在"],"Download":["下载"],"Open":["打开"],"Unshare":["停止分享"],"Retry":["重试"],"Download failed.":["下载失败"],"Share a YouTube video":[""],"Share a file as presentation":[""],"Share your screen":["共享您的屏幕"],"Chat":["聊天"],"Contacts":[""],"Mute microphone":["关闭麦克风"],"Turn camera off":["关闭摄像头"],"Settings":["系统设置"],"Loading presentation ...":[""],"Please upload a document":[""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[""],"Upload":[""],"You can drag files here too.":[""],"Presentation controls":[""],"Prev":[""],"Next":[""],"Change room":["更换房间"],"Room":["房间"],"Leave room":["离开房间"],"Main":["主房间"],"Current room":["當前房间"],"Screen sharing options":["屏幕共享设置"],"Fit screen.":["适合屏幕"],"Please select what to share.":[""],"Window":[""],"Application":[""],"Share the whole screen. Click share to select the screen.":[""],"Share a single window. Click share to select the window.":[""],"Share all windows of a application. This can leak content behind windows when windows get moved. Click share to select the application.":[""],"Profile":[""],"Your name":["您的名字"],"Your picture":["您的图片"],"Status message":[""],"What's on your mind?":[""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[""],"Your ID":[""],"Register":[""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[""],"Sign in":[""],"Create an account":[""],"Sign out":[""],"Manage account":[""],"Media":[""],"Microphone":["麦克风"],"Camera":["摄像头"],"Video quality":["视频质量"],"Low":["低"],"High":["高"],"HD":["高清"],"Full HD":[""],"General":[""],"Language":["语言"],"Language changes become active on reload.":["转换语言需重启程序"],"Default room":["系统默认房间"],"Set alternative room to join at start.":["重设初始默认房间"],"Desktop notification":["桌面提醒"],"Enable":["开启"],"Denied - check your browser settings":["被拒绝--请检查浏览器设置"],"Allowed":["启用"],"Advanced settings":["高级设置"],"Play audio on same device as selected microphone":[""],"Experimental AEC":[""],"Experimental AGC":[""],"Experimental noise suppression":[""],"Max video frame rate":["最大视频帧速率"],"auto":["自动"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[""],"Detect CPU over use":[""],"Automatically reduces video quality as needed.":[""],"Optimize for high resolution video":[""],"Reduce video noise":[""],"Enable experiments":[""],"Show advanced settings":["展开高级设置"],"Hide advanced settings":["隐藏高级设置"],"Remember settings":["记住设置"],"Your ID will still be kept - press the log out button above to delete the ID.":[""],"Room link":[""],"Invite with Facebook":[""],"Invite with Twitter":[""],"Invite with Google Plus":[""],"Invite with XING":[""],"Initializing":["初始化"],"Online":["在线"],"Calling":["呼叫中"],"Hangup":["挂断"],"In call with":["正在和**通话"],"Conference with":["和**会议通话"],"Your are offline":["您不在线"],"Go online":["上线"],"Connection interrupted":["连接已中断"],"An error occured":["出现错误"],"Incoming call":["来电"],"from":["来自"],"Accept call":["接受通话"],"Waiting for camera/microphone access":["等待摄像头/麦克风连接"],"Your audio level":["您的通话音量"],"Checking camera and microphone access.":["正在检查摄像头及麦克风连接"],"Please allow access to your camera and microphone.":["请允许连接您的摄像头及麦克风"],"Camera / microphone access required.":["需连接摄像头/麦克风"],"Please check your browser settings and allow camera and microphone access for this site.":["请检查浏览器设置并允许摄像头及麦克风连接此网站"],"Skip check":["越过检查"],"Click here for help (Google Chrome).":["点击这里获取帮助 (Google Chrome)"],"Please set your user details and settings.":["请设定您的用户信息及设置"],"Enter a room name":[""],"Random room name":[""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[""],"Room history":[""],"Please sign in.":[""],"Videos play simultaneously for everyone in this call.":[""],"YouTube URL":[""],"Could not load YouTube player API, please check your network / firewall settings.":[""],"Currently playing":[""],"YouTube controls":[""],"YouTube video to share":[""],"Peer to peer chat active.":["P2P聊天已启动"],"Peer to peer chat is now off.":["P2P现在未启动"]," is now offline.":[" 不在线"]," is now online.":[" 现在在线"],"You share file:":["分享文件:"],"Incoming file:":["发来文件:"],"You shared your location:":[""],"Location received:":[""],"You accepted the contact request.":[""],"You rejected the contact request.":[""],"You sent a contact request.":[""],"Your contact request was accepted.":[""],"Incoming contact request.":[""],"Your contact request was rejected.":[""],"Edit Contact":[""],"Close this window and disconnect?":[""],"Contacts Manager":[""],"Restart required to apply updates. Click ok to restart now.":["适用更新需重启,现在点击Ok重新启动。"],"Failed to access camera/microphone.":["摄像头/麦克风连接失败"],"Failed to establish peer connection.":["对等连接建立失败"],"We are sorry but something went wrong. Boo boo.":["很抱歉,有错误发生。"],"Oops":["Oops"],"Peer connection failed. Check your settings.":["对等连接失败,请检查设置。"],"User hung up because of error.":["用户因错误挂断"]," is busy. Try again later.":[" 正在通话,请稍后再试。"]," rejected your call.":[" 拒绝了您的呼叫。"]," does not pick up.":[" 无人接听。"],"Your browser does not support WebRTC. No calls possible.":["您的浏览器不支持WebRTC。不能进行通话。"],"Chat with":["与**聊天"],"Message from ":["来自于**的信息"],"You are now in room %s ...":["您在 %s 房间"],"Your browser does not support file transfer.":["您的浏览器不支持文件传输"],"Could not load PDF: Please make sure to select a PDF document.":[""],"Could not load PDF: Missing PDF file.":[""],"An error occurred while loading the PDF (%s).":[""],"An unknown error occurred while loading the PDF.":[""],"An error occurred while loading the PDF page (%s).":[""],"An unknown error occurred while loading the PDF page.":[""],"An error occurred while rendering the PDF page (%s).":[""],"An unknown error occurred while rendering the PDF page.":[""],"Only PDF documents and OpenDocument files can be shared at this time.":[""],"Failed to start screen sharing (%s).":[""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":["启动屏幕共享许可被拒绝。请确认您已开启浏览器屏幕共享连接。请复制chrome://flags/#enable-usermedia-screen-capture并用您的浏览器打开,启用最上端的功能。然后重启浏览器,操作完成。"],"Permission to start screen sharing was denied.":[""],"Use browser language":["使用浏览器语言"],"Meet with me here:":["我们这里见:"],"The request contains an invalid parameter value. Please check the URL of the video you want to share and try again.":[""],"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred. Please try again later.":[""],"The video requested was not found. Please check the URL of the video you want to share and try again.":[""],"The owner of the requested video does not allow it to be played in embedded players.":[""],"An unknown error occurred while playing back the video (%s). Please try again later.":[""],"An unknown error occurred while playing back the video. Please try again later.":[""],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[""],"Error":["错误"],"Hint":["提示"],"Please confirm":["请确认"],"More information required":["需要更多信息"],"Ok":["Ok"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[""],"Access code required":["需要接入码"],"Access denied":["连接被拒绝"],"Please provide a valid access code.":["请提供有效接入码"],"Failed to verify access code. Check your Internet connection and try again.":["接入码认证失败。请检查您的网络连接并重试。"],"PIN for room %s is now '%s'.":[""],"PIN lock has been removed from room %s.":[""],"Enter the PIN for room %s":[""],"Please sign in to create rooms.":[""],"and %s":[""],"and %d others":[""],"User":["用户"],"Someone":["某人"],"Me":["我"]}}} \ No newline at end of file diff --git a/static/translation/messages-zh-tw.json b/static/translation/messages-zh-tw.json index c329c070..84e6f2b7 100644 --- a/static/translation/messages-zh-tw.json +++ b/static/translation/messages-zh-tw.json @@ -1 +1 @@ -{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Your audio level":[null,"您的通話音量"],"Standard view":[null,""],"Large view":[null,""],"Kiosk view":[null,""],"Auditorium":[null,""],"Start chat":[null,"開始聊天"],"Start video call":[null,"開始視頻通話"],"Start audio conference":[null,"開始語音會議"],"No one else here":[null,""],"Take":[null,""],"Retake":[null,""],"Cancel":[null,"取消"],"Set as Profile Picture":[null,""],"Take picture":[null,"拍照"],"Waiting for camera":[null,"等待啟動攝像頭"],"The file couldn't be read.":[null,""],"The file is not an image.":[null,""],"The file is too large. Max. %d MB.":[null,""],"Select file":[null,""],"Chat sessions":[null,"會話"],"Room chat":[null,"房間聊天"],"Peer to peer":[null,"P2P"],"Close chat":[null,"關閉聊天"],"Share my location":[null,""],"is typing...":[null,"正在輸入..."],"has stopped typing...":[null,"停止輸入..."],"Type here to chat...":[null,"在此輸入開始聊天..."],"Send":[null,"發送"],"Accept":[null,""],"Reject":[null,"拒絕"],"You have no contacts.":[null,""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[null,""],"Edit contact":[null,""],"Edit":[null,""],"Name":[null,"名字"],"Remove":[null,""],"Refresh":[null,""],"Save":[null,""],"Close":[null,"關閉"],"File sharing":[null,"分享文件"],"File is no longer available":[null,"文件已不存在"],"Download":[null,"下載"],"Open":[null,"打開"],"Unshare":[null,"停止分享"],"Retry":[null,"重試"],"Download failed.":[null,"下載失敗"],"Share a YouTube video":[null,""],"Share a file as presentation":[null,""],"Share your screen":[null,"共享您的屏幕"],"Chat":[null,"聊天"],"Contacts":[null,""],"Mute microphone":[null,"關閉麥克風"],"Turn camera off":[null,"關閉攝像頭"],"Settings":[null,"系統設置"],"Loading presentation ...":[null,""],"Please upload a document":[null,""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[null,""],"Upload":[null,""],"You can drag files here too.":[null,""],"Presentation controls":[null,""],"Prev":[null,""],"Next":[null,""],"Change room":[null,"更換房間"],"Room":[null,"房間"],"Leave room":[null,"離開房間"],"Main":[null,"住房間"],"Current room":[null,"當前房間"],"Screen sharing options":[null,"屏幕共享設置"],"Fit screen.":[null,"適合屏幕"],"Profile":[null,""],"Your name":[null,"您的名字"],"Your picture":[null,"您的圖片"],"Status message":[null,""],"What's on your mind?":[null,""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[null,""],"Your ID":[null,""],"Register":[null,""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[null,""],"Sign in":[null,""],"Create an account":[null,""],"Sign out":[null,""],"Manage account":[null,""],"Media":[null,""],"Microphone":[null,"麥克風"],"Camera":[null,"攝像頭"],"Video quality":[null,"視頻質量"],"Low":[null,"低"],"High":[null,"高"],"HD":[null,"高清"],"Full HD":[null,""],"General":[null,""],"Language":[null,"語言"],"Language changes become active on reload.":[null,"轉換語言需要重啟程序"],"Default room":[null,"系統默認房間"],"Set alternative room to join at start.":[null,"重設初始默認房間"],"Desktop notification":[null,"桌面提醒"],"Enable":[null,"開啟"],"Denied - check your browser settings":[null,"被拒絕﹣請檢查瀏覽器設置"],"Allowed":[null,"啟用"],"Advanced settings":[null,"高級設置"],"Play audio on same device as selected microphone":[null,""],"Experimental AEC":[null,""],"Experimental AGC":[null,""],"Experimental noise suppression":[null,""],"Max video frame rate":[null,"最大視頻幀速率"],"auto":[null,"自動"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[null,""],"Detect CPU over use":[null,""],"Automatically reduces video quality as needed.":[null,""],"Optimize for high resolution video":[null,""],"Reduce video noise":[null,""],"Enable experiments":[null,""],"Show advanced settings":[null,"展開高級設置"],"Hide advanced settings":[null,"隐藏高级设置"],"Remember settings":[null,"記住設置"],"Your ID will still be kept - press the log out button above to delete the ID.":[null,""],"Room link":[null,""],"Invite with Facebook":[null,""],"Invite with Twitter":[null,""],"Invite with Google Plus":[null,""],"Invite with XING":[null,""],"Initializing":[null,"初始化"],"Online":[null,"在線"],"Calling":[null,"呼叫中"],"Hangup":[null,"掛斷"],"In call with":[null,"正在和**通電話"],"Conference with":[null,"和**會議通話"],"Your are offline":[null,"您不在線"],"Go online":[null,"上線"],"Connection interrupted":[null,"連接已終端"],"An error occured":[null,"出現錯誤"],"Incoming call":[null,"來電"],"from":[null,"來自"],"Accept call":[null,"接受通話"],"Waiting for camera/microphone access":[null,"等待攝像頭/麥克風連接"],"Checking camera and microphone access.":[null,"正在檢查攝像頭及麥克風連接"],"Please allow access to your camera and microphone.":[null,"請允許連接您的攝像頭及麥克風"],"Camera / microphone access required.":[null,"需連接攝像頭/麥克風"],"Please check your browser settings and allow camera and microphone access for this site.":[null,"請檢查瀏覽器設置並允許攝像頭及麥克風連接此網站"],"Skip check":[null,"越过检查"],"Click here for help (Google Chrome).":[null,"點擊這裡獲取幫助 (Google Chrome)"],"Please set your user details and settings.":[null,"請設定您的用戶信息及設置"],"Enter a room name":[null,""],"Random room name":[null,""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[null,""],"Room history":[null,""],"Please sign in.":[null,""],"Videos play simultaneously for everyone in this call.":[null,""],"YouTube URL":[null,""],"Could not load YouTube player API, please check your network / firewall settings.":[null,""],"Currently playing":[null,""],"YouTube controls":[null,""],"YouTube video to share":[null,""],"Peer to peer chat active.":[null,"P2P聊天啟動"],"Peer to peer chat is now off.":[null,"P2P現在未啟動"]," is now offline.":[null," 不在線"]," is now online.":[null," 現在在線"],"You share file:":[null,"分享文件:"],"Incoming file:":[null,"發來文件:"],"You shared your location:":[null,""],"Location received:":[null,""],"You accepted the contact request.":[null,""],"You rejected the contact request.":[null,""],"You sent a contact request.":[null,""],"Your contact request was accepted.":[null,""],"Incoming contact request.":[null,""],"Your contact request was rejected.":[null,""],"Edit Contact":[null,""],"Close this window and disconnect?":[null,""],"Contacts Manager":[null,""],"Restart required to apply updates. Click ok to restart now.":[null,"適用更新需重啟,現在點擊Ok重新啟動。"],"Failed to access camera/microphone.":[null,"攝像頭/麥克風連接失敗"],"Failed to establish peer connection.":[null,"對等連接建立失敗"],"We are sorry but something went wrong. Boo boo.":[null,"很抱歉,有序哦嗚發生......"],"Oops":[null,"Oops"],"Peer connection failed. Check your settings.":[null,"對等連接失敗,請檢查設置。"],"User hung up because of error.":[null,"用戶因錯誤掛斷"]," is busy. Try again later.":[null," 正在通話,請您稍後。"]," rejected your call.":[null," 拒絕了您的呼叫"]," does not pick up.":[null," 無人接聽。"],"Your browser does not support WebRTC. No calls possible.":[null,"您的遊覽器不支持WebRTC。不能進行通話。"],"Chat with":[null,"于**聊天"],"Message from ":[null,"來自於**的信息"],"You are now in room %s ...":[null,"您在 %s 房間"],"Your browser does not support file transfer.":[null,"您的遊覽器不支持文件傳輸"],"Could not load PDF: Please make sure to select a PDF document.":[null,""],"Could not load PDF: Missing PDF file.":[null,""],"An error occurred while loading the PDF (%s).":[null,""],"An unknown error occurred while loading the PDF.":[null,""],"An error occurred while loading the PDF page (%s).":[null,""],"An unknown error occurred while loading the PDF page.":[null,""],"An error occurred while rendering the PDF page (%s).":[null,""],"An unknown error occurred while rendering the PDF page.":[null,""],"Only PDF documents and OpenDocument files can be shared at this time.":[null,""],"Failed to start screen sharing (%s).":[null,""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":[null,"啟動屏幕共享許可被拒絕。請確認您已開啟瀏覽器屏幕共享連接。請復制chrome://flags/#enable-usermedia-screen-capture並用您的瀏覽器打開,啟用最上端的功能。然後重啟瀏覽器,操作完成。"],"Permission to start screen sharing was denied.":[null,""],"Use browser language":[null,"使用瀏覽器語言"],"Meet with me here:":[null,"我們這裡見:"],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[null,""],"Error":[null,"錯誤"],"Hint":[null,"提示"],"Please confirm":[null,"請確認"],"More information required":[null,"需要更多信息"],"Ok":[null,"Ok"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[null,""],"Access code required":[null,"需要接入碼"],"Access denied":[null,"連接被拒絕"],"Please provide a valid access code.":[null,"請提供有效接入碼"],"Failed to verify access code. Check your Internet connection and try again.":[null,"接入碼認證錯誤。請檢查您的網絡連接并重試。"],"PIN for room %s is now '%s'.":[null,""],"PIN lock has been removed from room %s.":[null,""],"Enter the PIN for room %s":[null,""],"Please sign in to create rooms.":[null,""],"and %s":[null,""],"and %d others":[null,""],"User":[null,"用戶"],"Someone":[null,"某人"],"Me":[null,"我"]}}} \ No newline at end of file +{"domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural_forms":"nplurals=1; plural=0"},"Standard view":[""],"Large view":[""],"Kiosk view":[""],"Auditorium":[""],"Start chat":["開始聊天"],"Start video call":["開始視頻通話"],"Start audio conference":["開始語音會議"],"No one else here":[""],"Take":[""],"Retake":[""],"Cancel":["取消"],"Set as Profile Picture":[""],"Take picture":["拍照"],"Waiting for camera":["等待啟動攝像頭"],"The file couldn't be read.":[""],"The file is not an image.":[""],"The file is too large. Max. %d MB.":[""],"Select file":[""],"Chat sessions":["會話"],"Room chat":["房間聊天"],"Peer to peer":["P2P"],"Close chat":["關閉聊天"],"Share my location":[""],"is typing...":["正在輸入..."],"has stopped typing...":["停止輸入..."],"Type here to chat...":["在此輸入開始聊天..."],"Send":["發送"],"Accept":[""],"Reject":["拒絕"],"You have no contacts.":[""],"To add new contacts, join a room and create a contact add request by clicking on the star icon next to a user entry.":[""],"Edit contact":[""],"Edit":[""],"Name":["名字"],"Remove":[""],"Refresh":[""],"Save":[""],"Close":["關閉"],"File sharing":["分享文件"],"File is no longer available":["文件已不存在"],"Download":["下載"],"Open":["打開"],"Unshare":["停止分享"],"Retry":["重試"],"Download failed.":["下載失敗"],"Share a YouTube video":[""],"Share a file as presentation":[""],"Share your screen":["共享您的屏幕"],"Chat":["聊天"],"Contacts":[""],"Mute microphone":["關閉麥克風"],"Turn camera off":["關閉攝像頭"],"Settings":["系統設置"],"Loading presentation ...":[""],"Please upload a document":[""],"Documents are shared with everyone in this call. The supported file types are PDF and OpenDocument files.":[""],"Upload":[""],"You can drag files here too.":[""],"Presentation controls":[""],"Prev":[""],"Next":[""],"Change room":["更換房間"],"Room":["房間"],"Leave room":["離開房間"],"Main":["住房間"],"Current room":["當前房間"],"Screen sharing options":["屏幕共享設置"],"Fit screen.":["適合屏幕"],"Please select what to share.":[""],"Window":[""],"Application":[""],"Share the whole screen. Click share to select the screen.":[""],"Share a single window. Click share to select the window.":[""],"Share all windows of a application. This can leak content behind windows when windows get moved. Click share to select the application.":[""],"Profile":[""],"Your name":["您的名字"],"Your picture":["您的圖片"],"Status message":[""],"What's on your mind?":[""],"Your picture, name and status message identify yourself in calls, chats and rooms.":[""],"Your ID":[""],"Register":[""],"Authenticated by certificate. To log out you have to remove your certificate from the browser.":[""],"Sign in":[""],"Create an account":[""],"Sign out":[""],"Manage account":[""],"Media":[""],"Microphone":["麥克風"],"Camera":["攝像頭"],"Video quality":["視頻質量"],"Low":["低"],"High":["高"],"HD":["高清"],"Full HD":[""],"General":[""],"Language":["語言"],"Language changes become active on reload.":["轉換語言需要重啟程序"],"Default room":["系統默認房間"],"Set alternative room to join at start.":["重設初始默認房間"],"Desktop notification":["桌面提醒"],"Enable":["開啟"],"Denied - check your browser settings":["被拒絕﹣請檢查瀏覽器設置"],"Allowed":["啟用"],"Advanced settings":["高級設置"],"Play audio on same device as selected microphone":[""],"Experimental AEC":[""],"Experimental AGC":[""],"Experimental noise suppression":[""],"Max video frame rate":["最大視頻幀速率"],"auto":["自動"],"Sending stereo audio disables echo cancellation. Enable only if you have stereo input.":[""],"Detect CPU over use":[""],"Automatically reduces video quality as needed.":[""],"Optimize for high resolution video":[""],"Reduce video noise":[""],"Enable experiments":[""],"Show advanced settings":["展開高級設置"],"Hide advanced settings":["隐藏高级设置"],"Remember settings":["記住設置"],"Your ID will still be kept - press the log out button above to delete the ID.":[""],"Room link":[""],"Invite with Facebook":[""],"Invite with Twitter":[""],"Invite with Google Plus":[""],"Invite with XING":[""],"Initializing":["初始化"],"Online":["在線"],"Calling":["呼叫中"],"Hangup":["掛斷"],"In call with":["正在和**通電話"],"Conference with":["和**會議通話"],"Your are offline":["您不在線"],"Go online":["上線"],"Connection interrupted":["連接已終端"],"An error occured":["出現錯誤"],"Incoming call":["來電"],"from":["來自"],"Accept call":["接受通話"],"Waiting for camera/microphone access":["等待攝像頭/麥克風連接"],"Your audio level":["您的通話音量"],"Checking camera and microphone access.":["正在檢查攝像頭及麥克風連接"],"Please allow access to your camera and microphone.":["請允許連接您的攝像頭及麥克風"],"Camera / microphone access required.":["需連接攝像頭/麥克風"],"Please check your browser settings and allow camera and microphone access for this site.":["請檢查瀏覽器設置並允許攝像頭及麥克風連接此網站"],"Skip check":["越过检查"],"Click here for help (Google Chrome).":["點擊這裡獲取幫助 (Google Chrome)"],"Please set your user details and settings.":["請設定您的用戶信息及設置"],"Enter a room name":[""],"Random room name":[""],"Enter the name of an existing room. You can create new rooms when you are signed in.":[""],"Room history":[""],"Please sign in.":[""],"Videos play simultaneously for everyone in this call.":[""],"YouTube URL":[""],"Could not load YouTube player API, please check your network / firewall settings.":[""],"Currently playing":[""],"YouTube controls":[""],"YouTube video to share":[""],"Peer to peer chat active.":["P2P聊天啟動"],"Peer to peer chat is now off.":["P2P現在未啟動"]," is now offline.":[" 不在線"]," is now online.":[" 現在在線"],"You share file:":["分享文件:"],"Incoming file:":["發來文件:"],"You shared your location:":[""],"Location received:":[""],"You accepted the contact request.":[""],"You rejected the contact request.":[""],"You sent a contact request.":[""],"Your contact request was accepted.":[""],"Incoming contact request.":[""],"Your contact request was rejected.":[""],"Edit Contact":[""],"Close this window and disconnect?":[""],"Contacts Manager":[""],"Restart required to apply updates. Click ok to restart now.":["適用更新需重啟,現在點擊Ok重新啟動。"],"Failed to access camera/microphone.":["攝像頭/麥克風連接失敗"],"Failed to establish peer connection.":["對等連接建立失敗"],"We are sorry but something went wrong. Boo boo.":["很抱歉,有序哦嗚發生......"],"Oops":["Oops"],"Peer connection failed. Check your settings.":["對等連接失敗,請檢查設置。"],"User hung up because of error.":["用戶因錯誤掛斷"]," is busy. Try again later.":[" 正在通話,請您稍後。"]," rejected your call.":[" 拒絕了您的呼叫"]," does not pick up.":[" 無人接聽。"],"Your browser does not support WebRTC. No calls possible.":["您的遊覽器不支持WebRTC。不能進行通話。"],"Chat with":["于**聊天"],"Message from ":["來自於**的信息"],"You are now in room %s ...":["您在 %s 房間"],"Your browser does not support file transfer.":["您的遊覽器不支持文件傳輸"],"Could not load PDF: Please make sure to select a PDF document.":[""],"Could not load PDF: Missing PDF file.":[""],"An error occurred while loading the PDF (%s).":[""],"An unknown error occurred while loading the PDF.":[""],"An error occurred while loading the PDF page (%s).":[""],"An unknown error occurred while loading the PDF page.":[""],"An error occurred while rendering the PDF page (%s).":[""],"An unknown error occurred while rendering the PDF page.":[""],"Only PDF documents and OpenDocument files can be shared at this time.":[""],"Failed to start screen sharing (%s).":[""],"Permission to start screen sharing was denied. Make sure to have enabled screen sharing access for your browser. Copy chrome://flags/#enable-usermedia-screen-capture and open it with your browser and enable the flag on top. Then restart the browser and you are ready to go.":["啟動屏幕共享許可被拒絕。請確認您已開啟瀏覽器屏幕共享連接。請復制chrome://flags/#enable-usermedia-screen-capture並用您的瀏覽器打開,啟用最上端的功能。然後重啟瀏覽器,操作完成。"],"Permission to start screen sharing was denied.":[""],"Use browser language":["使用瀏覽器語言"],"Meet with me here:":["我們這裡見:"],"The request contains an invalid parameter value. Please check the URL of the video you want to share and try again.":[""],"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred. Please try again later.":[""],"The video requested was not found. Please check the URL of the video you want to share and try again.":[""],"The owner of the requested video does not allow it to be played in embedded players.":[""],"An unknown error occurred while playing back the video (%s). Please try again later.":[""],"An unknown error occurred while playing back the video. Please try again later.":[""],"Unknown URL format. Please make sure to enter a valid YouTube URL.":[""],"Error":["錯誤"],"Hint":["提示"],"Please confirm":["請確認"],"More information required":["需要更多信息"],"Ok":["Ok"],"Screen sharing requires a browser extension. Please add the Spreed WebRTC screen sharing extension to Chrome and try again.":[""],"Access code required":["需要接入碼"],"Access denied":["連接被拒絕"],"Please provide a valid access code.":["請提供有效接入碼"],"Failed to verify access code. Check your Internet connection and try again.":["接入碼認證錯誤。請檢查您的網絡連接并重試。"],"PIN for room %s is now '%s'.":[""],"PIN lock has been removed from room %s.":[""],"Enter the PIN for room %s":[""],"Please sign in to create rooms.":[""],"and %s":[""],"and %d others":[""],"User":["用戶"],"Someone":["某人"],"Me":["我"]}}} \ No newline at end of file