mirror of
https://github.com/tinode/chat.git
synced 2025-03-14 10:05:07 +00:00
production-related changes for video calls
This commit is contained in:
10
INSTALL.md
10
INSTALL.md
@ -179,6 +179,16 @@ A bash script [run-cluster.sh](./server/run-cluster.sh) may be found useful.
|
||||
|
||||
Follow [instructions](./docs/faq.md#q-how-to-setup-push-notifications-with-google-fcm).
|
||||
|
||||
|
||||
### Enabling Video Calls
|
||||
|
||||
Video calls use [WebRTC](https://en.wikipedia.org/wiki/WebRTC). WebRTC is a peer to peer protocol: once the call is established, the client applications exchange data directly. Direct data exchange is efficient but creates a problem when the parties are not accessible from the internet. WebRTC solves it by means of [ICE](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) servers which implement protocols [TURN(S)](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) and [STUN](https://en.wikipedia.org/wiki/STUN) as fallback.
|
||||
|
||||
Tinode does not provide ICE servers out of the box. You must install and configure (or purchase) your own servers otherwise video and voice calling will not be available.
|
||||
|
||||
Once you obtain the ICE TURN/STUN configuration from your service provider, add it to `tinode.conf` section `"webrtc"` - `"ice_servers"` (or `"ice_servers_file"`). Also change `"webrtc"` - `"enabled"` to `true`. An example configuration is provided in the `tinode.conf` for illustration only. IT WILL NOT FUNCTION because it uses dummy values instead of actual server addresses.
|
||||
|
||||
|
||||
### Note on Running the Server in Background
|
||||
|
||||
There is [no clean way](https://github.com/golang/go/issues/227) to daemonize a Go process internally. One must use external tools such as shell `&` operator, `systemd`, `launchd`, `SMF`, `daemon tools`, `runit`, etc. to run the process in the background.
|
||||
|
@ -83,6 +83,7 @@ When you register a new account you are asked for an email address to send valid
|
||||
* Scriptable [command line](tn-cli/) (Python)
|
||||
* User features:
|
||||
* One-on-one and group messaging. Voice messages.
|
||||
* Video and audio calling.
|
||||
* Channels with unlimited number of read-only subscribers.
|
||||
* Granular access control with permissions for various actions.
|
||||
* User search/discovery.
|
||||
@ -113,7 +114,6 @@ When you register a new account you are asked for an email address to send valid
|
||||
### Planned
|
||||
|
||||
* [Federation](https://en.wikipedia.org/wiki/Federation_(information_technology)).
|
||||
* Video or audio calling.
|
||||
* Location sharing.
|
||||
* Previews of attached videos, documents, links.
|
||||
* Hot standby.
|
||||
|
@ -143,13 +143,14 @@ You can specify the following environment variables when issuing `docker run` co
|
||||
| `DEBUG_EMAIL_VERIFICATION_CODE` | string | | Enable dummy email verification code, e.g. `123456`. Disabled by default (empty string). |
|
||||
| `EXT_CONFIG` | string | | Path to external config file to use instead of the built-in one. If this parameter is used all other variables except `RESET_DB`, `FCM_SENDER_ID`, `FCM_VAPID_KEY` are ignored. |
|
||||
| `EXT_STATIC_DIR` | string | | Path to external directory containing static data (e.g. Tinode Webapp files) |
|
||||
| `FCM_CRED_FILE` | string | | Path to json file with FCM server-side service account credentials which will be used to send push notifications. |
|
||||
| `FCM_CRED_FILE` | string | | Path to JSON file with FCM server-side service account credentials which will be used to send push notifications. |
|
||||
| `FCM_API_KEY` | string | | Firebase API key; required for receiving push notifications in the web client |
|
||||
| `FCM_APP_ID` | string | | Firebase web app ID; required for receiving push notifications in the web client |
|
||||
| `FCM_PROJECT_ID` | string | | Firebase project ID; required for receiving push notifications in the web client |
|
||||
| `FCM_SENDER_ID` | string | | Firebase FCM sender ID; required for receiving push notifications in the web client |
|
||||
| `FCM_VAPID_KEY` | string | | Also called 'Web Client certificate' in the FCM console; required by the web client to receive push notifications. |
|
||||
| `FCM_INCLUDE_ANDROID_NOTIFICATION` | boolean | true | If true, pushes a data + notification message, otherwise a data-only message. [More info](https://firebase.google.com/docs/cloud-messaging/concept-options). |
|
||||
| `ICE_SERVERS_FILE` | string | | Path to JSON file with configuration of ICE servers to be used for video calls. |
|
||||
| `MEDIA_HANDLER` | string | `fs` | Handler of large files, either `fs` or `s3` |
|
||||
| `MYSQL_DSN` | string | `'root@tcp(mysql)/tinode'` | MySQL [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name). |
|
||||
| `PLUGIN_PYTHON_CHAT_BOT_ENABLED` | bool | `false` | Enable calling into the plugin provided by Python chatbot |
|
||||
|
@ -102,6 +102,10 @@ ENV TNPG_AUTH_TOKEN=
|
||||
# Tinode Push Gateway organization name as registered at console.tinode.co
|
||||
ENV TNPG_ORG=
|
||||
|
||||
# Video calls configuration.
|
||||
ENV WEBRTC_ENABLED=false
|
||||
ENV ICE_SERVERS_FILE=
|
||||
|
||||
# Use the target db by default.
|
||||
# When TARGET_DB is "alldbs", it is the user's responsibility
|
||||
# to set STORE_USE_ADAPTER to the desired db adapter correctly.
|
||||
|
@ -141,10 +141,15 @@
|
||||
}
|
||||
],
|
||||
|
||||
"webrtc": {
|
||||
"enabled": $WEBRTC_ENABLED,
|
||||
"call_establishment_timeout": 30,
|
||||
"ice_servers_file": "$ICE_SERVERS_FILE"
|
||||
},
|
||||
|
||||
"cluster_config": {
|
||||
"self": "",
|
||||
"nodes": [
|
||||
// Name and TCP address of each node.
|
||||
{"name": "tinode-0", "addr": "tinode-0:12000"},
|
||||
{"name": "tinode-1", "addr": "tinode-1:12001"},
|
||||
{"name": "tinode-2", "addr": "tinode-2:12002"}
|
||||
@ -171,5 +176,4 @@
|
||||
"service_addr": "tcp://localhost:40051"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ else
|
||||
TNPG_PUSH_ENABLED=true
|
||||
fi
|
||||
|
||||
if [ ! -z "$ICE_SERVERS_FILE" ] ; then
|
||||
WEBRTC_ENABLED=true
|
||||
fi
|
||||
|
||||
# Generate a new 'working.config' from template and environment
|
||||
while IFS='' read -r line || [[ -n $line ]] ; do
|
||||
while [[ "$line" =~ (\$[A-Z_][A-Z_0-9]*) ]] ; do
|
||||
|
@ -64,7 +64,7 @@ If key is provided, it's a 0-based index into the `ent` array which contains ext
|
||||
* `LN`: link (URL) [https://api.tinode.co](https://api.tinode.co).
|
||||
* `MN`: mention such as [@tinode](#).
|
||||
* `RW`: logical grouping of formats, a row; may also be represented as a basic decoration.
|
||||
* `VC`: reserved for video and audio calls (not implemented yet).
|
||||
* `VC`: video (and audio) calls.
|
||||
|
||||
Examples:
|
||||
* `{ "at":8, "len":4, "tp":"ST"}`: apply formatting `ST` (strong/bold) to 4 characters starting at offset 8 into `txt`.
|
||||
@ -291,3 +291,27 @@ Hashtag `data` contains a single `val` field with the hashtag value which the cl
|
||||
```js
|
||||
{ "tp":"HT", "data":{ "val":"tinode" } }
|
||||
```
|
||||
|
||||
#### `VC`: video call control message
|
||||
Video call `data` contains current state of the call and its duration:
|
||||
```js
|
||||
{
|
||||
"tp": "VC",
|
||||
"data": {
|
||||
"duration": 10000,
|
||||
"state": "disconnected",
|
||||
"incoming": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `duration`: call duration in milliseconds.
|
||||
* `state`: surrent call state; supported states:
|
||||
* `accepted`: a call is established (ongoing).
|
||||
* `finished`: a previously establied call has successfully finished.
|
||||
* `disconnected`: the call is dropped for example because of an error.
|
||||
* `missed`: the call is missed, i.e. the callee didn't pick up the phone.
|
||||
* `declined`: the call is declined, i.e. the callee hung up before picking up.
|
||||
* `incoming`: if the call is incoming or outgoing.
|
||||
|
||||
The `VC` may also be represented as a format `"fmt": [{"len": 1, "tp": "VC"}]` with no entity. In such a case all call information is contained in the `head` fields of the enclosing message.
|
||||
|
@ -7,11 +7,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/logs"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
jcr "github.com/tinode/jsonco"
|
||||
)
|
||||
|
||||
// Video call constants.
|
||||
@ -44,6 +49,25 @@ const (
|
||||
constCallMsgDeclined = "declined"
|
||||
)
|
||||
|
||||
type callConfig struct {
|
||||
// Enable video/voice calls.
|
||||
Enabled bool `json:"enabled"`
|
||||
// Timeout in seconds before a call is dropped if not answered.
|
||||
CallEstablishmentTimeout int `json:"call_establishment_timeout"`
|
||||
// ICE servers.
|
||||
ICEServers []iceServer `json:"ice_servers"`
|
||||
// Alternative config as an external file.
|
||||
ICEServersFile string `json:"ice_servers_file"`
|
||||
}
|
||||
|
||||
// ICE server config.
|
||||
type iceServer struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
CredentialType string `json:"credential_type,omitempty"`
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
}
|
||||
|
||||
// videoCall describes video call that's being established or in progress.
|
||||
type videoCall struct {
|
||||
// Call participants.
|
||||
@ -58,6 +82,62 @@ type videoCall struct {
|
||||
acceptedAt time.Time
|
||||
}
|
||||
|
||||
func initVideoCalls(jsconfig json.RawMessage) error {
|
||||
var config callConfig
|
||||
|
||||
if len(jsconfig) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsconfig), &config); err != nil {
|
||||
return fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
if !config.Enabled {
|
||||
logs.Info.Println("Video calls disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(config.ICEServers) > 0 {
|
||||
globals.iceServers = config.ICEServers
|
||||
} else if config.ICEServersFile != "" {
|
||||
var iceConfig []iceServer
|
||||
if file, err := os.Open(config.ICEServersFile); err != nil {
|
||||
return fmt.Errorf("failed to read ICE config: %w", err)
|
||||
} else {
|
||||
jr := jcr.New(file)
|
||||
if err = json.NewDecoder(jr).Decode(&iceConfig); err != nil {
|
||||
switch jerr := err.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
|
||||
return fmt.Errorf("unmarshall error in ICE config in %s at %d:%d (offset %d bytes): %w",
|
||||
jerr.Field, lnum, cnum, jerr.Offset, jerr)
|
||||
case *json.SyntaxError:
|
||||
lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
|
||||
return fmt.Errorf("syntax error in config file at %d:%d (offset %d bytes): %w",
|
||||
lnum, cnum, jerr.Offset, jerr)
|
||||
default:
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
globals.iceServers = iceConfig
|
||||
}
|
||||
|
||||
if len(globals.iceServers) == 0 {
|
||||
return errors.New("no valid ICE cervers found")
|
||||
}
|
||||
|
||||
globals.callEstablishmentTimeout = config.CallEstablishmentTimeout
|
||||
if globals.callEstablishmentTimeout <= 0 {
|
||||
globals.callEstablishmentTimeout = defaultCallEstablishmentTimeout
|
||||
}
|
||||
|
||||
logs.Info.Println("Video calls enabled with ", len(globals.iceServers), "ICE servers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (call *videoCall) messageHead(newState string, duration int) map[string]interface{} {
|
||||
head := map[string]interface{}{
|
||||
"replace": ":" + strconv.Itoa(call.seq),
|
||||
|
@ -1620,6 +1620,11 @@ func ErrNotImplemented(id, topic string, serverTs, incomingReqTs time.Time) *Ser
|
||||
}
|
||||
}
|
||||
|
||||
// ErrNotImplementedReply feature not implemented error in response to a client request (501).
|
||||
func ErrNotImplementedReply(msg *ClientComMessage, ts time.Time) *ServerComMessage {
|
||||
return ErrNotImplemented(msg.Id, msg.Original, ts, msg.Timestamp)
|
||||
}
|
||||
|
||||
// ErrClusterUnreachable in-cluster communication has failed (502).
|
||||
func ErrClusterUnreachable(id, topic string, ts time.Time) *ServerComMessage {
|
||||
return &ServerComMessage{
|
||||
|
@ -126,14 +126,6 @@ type credValidator struct {
|
||||
addToTags bool
|
||||
}
|
||||
|
||||
// ICE server config (video calling).
|
||||
type iceServer struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
CredentialType string `json:"credential_type,omitempty"`
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
}
|
||||
|
||||
var globals struct {
|
||||
// Topics cache and processing.
|
||||
hub *Hub
|
||||
@ -264,19 +256,17 @@ type configType struct {
|
||||
// when the country isn't specified by the client explicitly and
|
||||
// it's impossible to infer it.
|
||||
DefaultCountryCode string `json:"default_country_code"`
|
||||
// Timeout in seconds before a call is dropped if not answered.
|
||||
CallEstablishmentTimeout int `json:"call_establishment_timeout"`
|
||||
|
||||
// Configs for subsystems
|
||||
Cluster json.RawMessage `json:"cluster_config"`
|
||||
Plugin json.RawMessage `json:"plugins"`
|
||||
Store json.RawMessage `json:"store_config"`
|
||||
Push json.RawMessage `json:"push"`
|
||||
TLS json.RawMessage `json:"tls"`
|
||||
Auth map[string]json.RawMessage `json:"auth_config"`
|
||||
Validator map[string]*validatorConfig `json:"acc_validation"`
|
||||
Media *mediaConfig `json:"media"`
|
||||
ICEServers []iceServer `json:"ice_servers"`
|
||||
Cluster json.RawMessage `json:"cluster_config"`
|
||||
Plugin json.RawMessage `json:"plugins"`
|
||||
Store json.RawMessage `json:"store_config"`
|
||||
Push json.RawMessage `json:"push"`
|
||||
TLS json.RawMessage `json:"tls"`
|
||||
Auth map[string]json.RawMessage `json:"auth_config"`
|
||||
Validator map[string]*validatorConfig `json:"acc_validation"`
|
||||
Media *mediaConfig `json:"media"`
|
||||
WebRTC json.RawMessage `json:"webrtc"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@ -530,11 +520,6 @@ func main() {
|
||||
globals.defaultCountryCode = defaultCountryCode
|
||||
}
|
||||
|
||||
globals.callEstablishmentTimeout = config.CallEstablishmentTimeout
|
||||
if globals.callEstablishmentTimeout <= 0 {
|
||||
globals.callEstablishmentTimeout = defaultCallEstablishmentTimeout
|
||||
}
|
||||
|
||||
if config.Media != nil {
|
||||
if config.Media.UseHandler == "" {
|
||||
config.Media = nil
|
||||
@ -569,7 +554,9 @@ func main() {
|
||||
logs.Info.Println("Stopped push notifications")
|
||||
}()
|
||||
|
||||
globals.iceServers = config.ICEServers
|
||||
if err = initVideoCalls(config.WebRTC); err != nil {
|
||||
logs.Err.Fatal("Failed to init video calls: %w", err)
|
||||
}
|
||||
|
||||
// Keep inactive LP sessions for 15 seconds
|
||||
globals.sessionStore = NewSessionStore(idleSessionTimeout + 15*time.Second)
|
||||
|
@ -754,8 +754,12 @@ func (s *Session) hello(msg *ClientComMessage) {
|
||||
"maxTagLength": maxTagLength,
|
||||
"maxTagCount": globals.maxTagCount,
|
||||
"maxFileUploadSize": globals.maxFileUploadSize,
|
||||
"iceServers": globals.iceServers,
|
||||
"callTimeout": globals.callEstablishmentTimeout,
|
||||
}
|
||||
if len(globals.iceServers) > 0 {
|
||||
params["iceServers"] = globals.iceServers
|
||||
}
|
||||
if globals.callEstablishmentTimeout > 0 {
|
||||
params["callTimeout"] = globals.callEstablishmentTimeout
|
||||
}
|
||||
|
||||
// Set ua & platform in the beginning of the session.
|
||||
|
@ -61,9 +61,6 @@
|
||||
// If missing, the server will default to "US".
|
||||
"default_country_code": "",
|
||||
|
||||
// Timeout in seconds before a video/voice call is dropped if not answered.
|
||||
"call_establishment_timeout": 30,
|
||||
|
||||
// Large media/blob handlers: large files/images included in messages.
|
||||
"media": {
|
||||
// The name of the media handler to use.
|
||||
@ -490,28 +487,37 @@
|
||||
}
|
||||
],
|
||||
|
||||
// Interactive Communication Establishment (ICE) STUN and TURN server configuration for video calls.
|
||||
// You need to configure your own servers. https://xirsys.com/ offers a free tier for developers.
|
||||
// Video calls will not work if both parties are behind NAT and no ICE servers are configured.
|
||||
"ice_servers": [
|
||||
{
|
||||
"urls": [
|
||||
"stun:stun.example.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "user-name-to-use-for-authentication-with-the-server",
|
||||
"credential": "your-password",
|
||||
"urls": [
|
||||
"turn:turn.example.com:80?transport=udp",
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:turn.example.com:80?transport=tcp",
|
||||
"turn:turn.example.com:3478?transport=tcp",
|
||||
"turns:turn.example.com:443?transport=tcp",
|
||||
"turns:turn.example.com:5349?transport=tcp"
|
||||
]
|
||||
}
|
||||
],
|
||||
// Configuration for voice and video calls.
|
||||
"webrtc": {
|
||||
// Disabled. Won't work without functioning ice_servers (see below).
|
||||
"enabled": false,
|
||||
// Timeout in seconds before a video/voice call is dropped if not answered.
|
||||
"call_establishment_timeout": 30,
|
||||
// Interactive Communication Establishment (ICE) STUN and TURN server configuration for video calls.
|
||||
// You need to configure your own servers. https://xirsys.com/ offers a free tier for developers.
|
||||
// Video calls will not work if both parties are behind NAT and no ICE servers are configured.
|
||||
"ice_servers": [
|
||||
{
|
||||
"urls": [
|
||||
"stun:stun.example.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "user-name-to-use-for-authentication-with-the-server",
|
||||
"credential": "your-password",
|
||||
"urls": [
|
||||
"turn:turn.example.com:80?transport=udp",
|
||||
"turn:turn.example.com:3478?transport=udp",
|
||||
"turn:turn.example.com:80?transport=tcp",
|
||||
"turn:turn.example.com:3478?transport=tcp",
|
||||
"turns:turn.example.com:443?transport=tcp",
|
||||
"turns:turn.example.com:5349?transport=tcp"
|
||||
]
|
||||
}
|
||||
],
|
||||
// An alternative way to provide STUN/TURN configuration.
|
||||
"ice_servers_file": "/path/to/ice-servers-config.json",
|
||||
},
|
||||
|
||||
// Cluster-mode configuration.
|
||||
"cluster_config": {
|
||||
|
@ -1030,6 +1030,10 @@ func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
||||
|
||||
isCall := msg.Pub.Head != nil && msg.Pub.Head["webrtc"] != nil
|
||||
if isCall {
|
||||
if len(globals.iceServers) == 0 {
|
||||
msg.sess.queueOut(ErrNotImplementedReply(msg, types.TimeNow()))
|
||||
return
|
||||
}
|
||||
if t.currentCall != nil {
|
||||
msg.sess.queueOut(ErrCallBusyReply(msg, types.TimeNow()))
|
||||
return
|
||||
@ -1046,6 +1050,7 @@ func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
||||
logs.Err.Printf("topic[%s]: failed to save messagge - %s", t.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if isCall {
|
||||
t.handleCallInvite(msg, asUid)
|
||||
}
|
||||
|
Reference in New Issue
Block a user