Merge branch 'cloudrtc'

This commit is contained in:
balzack 2024-06-03 22:29:53 -07:00
commit 8a93d123ad
21 changed files with 300 additions and 97 deletions

View File

@ -160,6 +160,7 @@ const Strings = [
enableVideo: 'Enable Video Queue',
enableBinary: 'Enable Binary Files',
enableCalls: 'Enable WebRTC Calls',
iceService: 'Cloudflare Service',
relayUrl: 'Relay URL',
relayUsername: 'Relay Username',
relayPassword: 'Relay Password',
@ -370,6 +371,7 @@ const Strings = [
enableVideo: 'Activer les Fichiers Vidéo',
enableBinary: 'Activer les Fichiers Binaires',
enableCalls: 'Activer les Appels',
iceService: 'Service Cloudflare',
relayUrl: 'URL de Relais',
relayUsername: 'Nom d\'Utilisateur du Relais',
relayPassword: 'Mot de Passe du Relais',
@ -580,6 +582,7 @@ const Strings = [
enableVideo: 'Permitir Archivos de Vídeo',
enableBinary: 'Permitir Archivos Binarios',
enableCalls: 'Permitier Llamadas',
iceService: 'Servicio Cloudflare',
relayUrl: 'URL para Llamadas',
relayUsername: 'Nombre de Usuario para Llamadas',
relayPassword: 'Contraseña para Llamadas',
@ -791,6 +794,7 @@ const Strings = [
enableVideo: 'Videodateien aktivieren',
enableBinary: 'Binärdateien aktivieren',
enableCalls: 'Anrufe Ermöglichen',
iceService: 'Cloudflare-Dienst',
relayUrl: 'URL für Anrufe',
relayUsername: 'Benutzername für Anrufe',
relayPassword: 'Passwort für Anrufe',
@ -990,6 +994,7 @@ const Strings = [
enableVideo: 'Habilitar Fila de Vídeo',
enableBinary: 'Habilitar Fila Binários',
enableCalls: 'Habilitar Chamadas WebRTC',
iceService: 'Serviço Cloudflare',
relayUrl: 'URL do Relay',
relayUsername: 'Nome de Usuário do Relay',
relayPassword: 'Senha do Relay',
@ -1186,6 +1191,7 @@ const Strings = [
enableVideo: 'Включить очередь видео',
enableBinary: 'Включить двоичные файлы',
enableCalls: 'Включить звонки WebRTC',
iceService: 'Сервис Cloudflare',
relayUrl: 'URL релея',
relayUsername: 'Имя пользователя релея',
relayPassword: 'Пароль релея',

View File

@ -213,8 +213,9 @@ export function useAppContext() {
card.actions.setRevision(cardRev);
}
else if (activity.ring) {
const { cardId, callId, calleeToken, iceUrl, iceUsername, icePassword } = activity.ring;
ring.actions.ring(cardId, callId, calleeToken, iceUrl, iceUsername, icePassword);
const { cardId, callId, calleeToken, ice, iceUrl, iceUsername, icePassword } = activity.ring;
const config = ice ? ice : [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
ring.actions.ring(cardId, callId, calleeToken, config);
}
else {
const { profile: profileRev, account: accountRev, channel: channelRev, card: cardRev } = activity;

View File

@ -331,9 +331,9 @@ export function useRingContext() {
clearSession: () => {
access.current = null;
},
ring: (cardId, callId, calleeToken, iceUrl, iceUsername, icePassword) => {
ring: (cardId, callId, calleeToken, ice) => {
const key = `${cardId}:${callId}`
const call = ringing.current.get(key) || { cardId, calleeToken, callId, iceUrl, iceUsername, icePassword }
const call = ringing.current.get(key) || { cardId, calleeToken, callId, ice }
call.expires = Date.now() + EXPIRE;
ringing.current.set(key, call);
updateState({ ringing: ringing.current });
@ -365,7 +365,7 @@ export function useRingContext() {
}
}
},
accept: async (cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword) => {
accept: async (cardId, callId, contactNode, contactToken, calleeToken, ice) => {
if (calling.current) {
throw new Error("active session");
}
@ -378,7 +378,6 @@ export function useRingContext() {
updateState({ ringing: ringing.current, callStatus: "connecting", cardId });
calling.current = { callId, contactNode, contactToken, host: false };
const ice = [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
await connect('impolite', contactNode, calleeToken, () => {}, () => {}, ice);
}
},
@ -422,9 +421,9 @@ export function useRingContext() {
throw err;
}
const { id, keepAlive, callerToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { id, keepAlive, callerToken, calleeToken, ice, iceUrl, iceUsername, icePassword } = call;
try {
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, iceUrl, iceUsername, icePassword });
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, ice, iceUrl, iceUsername, icePassword });
}
catch (err) {
console.log(err);
@ -446,7 +445,7 @@ export function useRingContext() {
}
}
else {
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, iceUrl, iceUsername, icePassword });
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, ice, iceUrl, iceUsername, icePassword });
index += 1;
}
}
@ -457,8 +456,8 @@ export function useRingContext() {
updateState({ callStatus: "ringing" });
calling.current = { callId: id, host: true };
const ice = [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
await connect('polite', server, callerToken, () => clearInterval(ringInterval), () => clearInterval(aliveInterval), ice);
const iceLegacy = [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
await connect('polite', server, callerToken, () => clearInterval(ringInterval), () => clearInterval(aliveInterval), ice ? ice : iceLegacy);
},
enableVideo: async () => {
if (!videoTrack.current) {

View File

@ -283,33 +283,48 @@ export function Dashboard(props) {
onValueChange={actions.setEnableIce} trackColor={styles.track}/>
</TouchableOpacity>
<InputField style={styles.field}
label={state.strings.relayUrl}
value={state.iceUrl}
autoCapitalize={'none'}
spellCheck={false}
disabled={!state.enableIce}
onChangeText={actions.setIceUrl}
/>
{ state.enableIce && (
<>
{ state.iceServiceFlag != null && (
<TouchableOpacity style={styles.ice} activeOpacity={1}
onPress={() => actions.setIceServiceFlag(!state.iceServiceFlag)}>
<Text style={styles.modalLabel}>{ state.strings.iceService }</Text>
<Switch style={styles.switch} value={state.iceServiceFlag}
onValueChange={actions.setIceServiceFlag} trackColor={styles.track}/>
</TouchableOpacity>
)}
<InputField style={styles.field}
label={state.strings.relayUsername}
value={state.iceUsername}
autoCapitalize={'none'}
spellCheck={false}
disabled={!state.enableIce}
onChangeText={actions.setIceUsername}
/>
{ !state.iceServiceFlag && (
<InputField style={styles.field}
label={state.strings.relayUrl}
value={state.iceUrl}
autoCapitalize={'none'}
spellCheck={false}
disabled={!state.enableIce}
onChangeText={actions.setIceUrl}
/>
)}
<InputField style={styles.field}
label={state.iceServiceFlag ? 'TURN_KEY_ID' : state.strings.relayUsername}
value={state.iceUsername}
autoCapitalize={'none'}
spellCheck={false}
disabled={!state.enableIce}
onChangeText={actions.setIceUsername}
/>
<InputField style={styles.field}
label={state.iceServiceFlag ? 'TURN_KEY_API_TOKEN' : state.strings.relayPassword}
value={state.icePassword}
autoCapitalize={'none'}
spellCheck={false}
disabled={!state.enableIce}
onChangeText={actions.setIcePassword}
/>
</>
)}
<InputField style={styles.field}
label={state.strings.relayPassword}
value={state.icePassword}
autoCapitalize={'none'}
spellCheck={false}
disabled={!state.enableIce}
onChangeText={actions.setIcePassword}
/>
<View style={styles.pad} />
</ScrollView>

View File

@ -41,6 +41,7 @@ export function useDashboard(server, token, mfa) {
enableBinary: true,
createToken: null,
enableIce: false,
iceServiceFlag: false,
iceUrl: null,
iceUsername: null,
icePassword: null,
@ -78,9 +79,12 @@ export function useDashboard(server, token, mfa) {
const config = await getNodeConfig(server, token);
const nodeAccounts = await getNodeAccounts(server, token);
const accounts = nodeAccounts.map(setAccountItem);
const { keyType, accountStorage, domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceUrl, iceUsername, icePassword } = config || {};
const { keyType, accountStorage, domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceService, iceUrl, iceUsername, icePassword } = config || {};
const storage = Math.ceil(accountStorage / 1073741824);
updateState({ keyType, storage: storage.toString(), domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceUrl, iceUsername, icePassword, accounts, mfaEnabled });
const iceServiceFlag = iceService === 'cloudflare' ? true : iceService == null ? null : true;
console.log("ICE:", iceService, iceServiceFlag);
updateState({ keyType, storage: storage.toString(), domain, enableImage, enableAudio, enableVideo, enableBinary, transformSupported, allowUnsealed, pushSupported, enableIce, iceServiceFlag, iceUrl, iceUsername, icePassword, accounts, mfaEnabled });
}
const refreshAccounts = async () => {
@ -150,6 +154,9 @@ export function useDashboard(server, token, mfa) {
setEnableIce: (enableIce) => {
updateState({ enableIce });
},
setIceServiceFlag: (iceServiceFlag) => {
updateState({ iceServiceFlag });
},
setIceUrl: (iceUrl) => {
updateState({ iceUrl });
},
@ -160,9 +167,10 @@ export function useDashboard(server, token, mfa) {
updateState({ icePassword });
},
saveConfig: async () => {
const { storage, domain, keyType, enableImage, pushSupported, allowUnsealed, transformSupported, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword } = state;
const { storage, domain, keyType, enableImage, pushSupported, allowUnsealed, transformSupported, enableAudio, enableVideo, enableBinary, enableIce, iceServiceFlag, iceUrl, iceUsername, icePassword } = state;
const iceService = iceServiceFlag ? 'cloudflare' : '';
const accountStorage = Number(storage) * 1073741824;
const config = { accountStorage, domain, keyType, enableImage, pushSupported, allowUnsealed, transformSupported, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword };
const config = { accountStorage, domain, keyType, enableImage, pushSupported, allowUnsealed, transformSupported, enableAudio, enableVideo, enableBinary, enableIce, iceService, iceUrl, iceUsername, icePassword };
await setNodeConfig(server, token, config);
},
enableUser: async (accountId, enabled) => {

View File

@ -49,7 +49,7 @@ export function useSession() {
const expired = Date.now();
ring.state.ringing.forEach(call => {
if (call.expires > expired && !call.status) {
const { callId, cardId, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { callId, cardId, calleeToken, ice } = call;
const contact = card.state.cards.get(cardId);
if (contact) {
const { imageSet, name, handle, node, guid } = contact.card?.profile || {};
@ -57,7 +57,7 @@ export function useSession() {
const contactToken = `${guid}.${token}`;
const server = node ? node : profile.state.server;
const img = imageSet ? card.actions.getCardImageUrl(cardId) : null;
ringing.push({ cardId, img, name, handle, contactNode: server, callId, contactToken, calleeToken, iceUrl, iceUsername, icePassword });
ringing.push({ cardId, img, name, handle, contactNode: server, callId, contactToken, calleeToken, ice });
}
}
});
@ -124,8 +124,8 @@ export function useSession() {
await ring.actions.decline(cardId, contactNode, contactToken, callId);
},
accept: async (call) => {
const { cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
await ring.actions.accept(cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword);
const { cardId, callId, contactNode, contactToken, calleeToken, ice } = call;
await ring.actions.accept(cardId, callId, contactNode, contactToken, calleeToken, ice);
},
end: async () => {
await ring.actions.end();

View File

@ -4119,6 +4119,8 @@ components:
type: boolean
enableIce:
type: boolean
iceService:
type: string
iceUrl:
type: string
iceUsername:
@ -4130,6 +4132,7 @@ components:
openAccessLimit:
type: integer
format: int64
Seal:
type: object
@ -4899,12 +4902,28 @@ components:
keepAlive:
type: integer
format: int32
iceService:
type: string
iceUrl:
type: string
iceUsername:
type: string
icePassword:
type: string
IceUrl:
tyle: object
required:
- urls
- username
- credential
properties:
urls:
type: string
username:
type: string
credential:
type: string
Ring:
type: object
@ -4920,6 +4939,10 @@ components:
index:
type: integer
format: int32
ice:
type: array
items:
$ref: '#/components/schemas/IceUrl'
iceUrl:
type: string
iceUsername:

View File

@ -40,6 +40,17 @@ func AddCall(w http.ResponseWriter, r *http.Request) {
return
}
iceService := getStrConfigValue(CNFIceService, "");
iceURL := getStrConfigValue(CNFIceUrl, "")
iceUsername := getStrConfigValue(CNFIceUsername, "")
icePassword := getStrConfigValue(CNFIcePassword, "")
ice, err := getIce(iceService, iceURL, iceUsername, icePassword);
if err != nil || len(ice) == 0 {
ErrResponse(w, http.StatusServiceUnavailable, err)
return
}
// generate call params
callerBin, callerErr := securerandom.Bytes(APPTokenSize)
if callerErr != nil {
@ -51,31 +62,28 @@ func AddCall(w http.ResponseWriter, r *http.Request) {
ErrResponse(w, http.StatusInternalServerError, calleeErr)
return
}
//turnBin, turnErr := securerandom.Bytes(APPTokenSize)
//if turnErr != nil {
// ErrResponse(w, http.StatusInternalServerError, turnErr)
// return
//}
callId := uuid.New().String()
// allocate bridge
callerToken := hex.EncodeToString(callerBin);
calleeToken := hex.EncodeToString(calleeBin);
iceUrl := getStrConfigValue(CNFIceUrl, "")
iceUsername := getStrConfigValue(CNFIceUsername, "")
icePassword := getStrConfigValue(CNFIcePassword, "")
bridgeRelay.AddBridge(account.ID, callId, cardId, callerToken, calleeToken);
turn := getDefaultIce(ice);
// create response
call := Call{
Id: callId,
CardId: cardId,
CallerToken: callerToken,
CalleeToken: calleeToken,
IceUrl: iceUrl,
IceUsername: iceUsername,
IcePassword: icePassword,
Ice: ice,
IceService: iceService,
IceURL: turn.URLs,
IceUsername: turn.Username,
IcePassword: turn.Credential,
KeepAlive: BridgeKeepAlive,
}
WriteResponse(w, call);
}

View File

@ -25,7 +25,8 @@ func GetNodeConfig(w http.ResponseWriter, r *http.Request) {
config.KeyType = getStrConfigValue(CNFKeyType, APPRSA2048)
config.PushSupported = getBoolConfigValue(CNFPushSupported, true)
config.EnableIce = getBoolConfigValue(CNFEnableIce, false)
config.IceUrl = getStrConfigValue(CNFIceUrl, "")
config.IceService = getStrConfigValue(CNFIceService, "")
config.IceURL = getStrConfigValue(CNFIceUrl, "")
config.IceUsername = getStrConfigValue(CNFIceUsername, "")
config.IcePassword = getStrConfigValue(CNFIcePassword, "")
config.EnableOpenAccess = getBoolConfigValue(CNFEnableOpenAccess, false);

View File

@ -101,7 +101,7 @@ func SetNodeConfig(w http.ResponseWriter, r *http.Request) {
return res
}
// upsert push supported
// upsert ice supported
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
@ -109,11 +109,19 @@ func SetNodeConfig(w http.ResponseWriter, r *http.Request) {
return res
}
// upsert ice service name
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
}).Create(&store.Config{ConfigID: CNFIceService, StrValue: config.IceService}).Error; res != nil {
return res
}
// upsert key type
if res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "config_id"}},
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
}).Create(&store.Config{ConfigID: CNFIceUrl, StrValue: config.IceUrl}).Error; res != nil {
}).Create(&store.Config{ConfigID: CNFIceUrl, StrValue: config.IceURL}).Error; res != nil {
return res
}

View File

@ -155,7 +155,8 @@ func SetRing(card *store.Card, ring Ring) {
var phone Phone
phone.CallID = ring.CallID
phone.CalleeToken = ring.CalleeToken
phone.IceUrl = ring.IceUrl
phone.Ice = ring.Ice
phone.IceURL = ring.IceURL
phone.IceUsername = ring.IceUsername
phone.IcePassword = ring.IcePassword
phone.CardID = card.CardSlot.CardSlotID

View File

@ -54,6 +54,9 @@ const CNFKeyType = "key_type"
//CNFEnableIce specifies whether webrtc is enabled
const CNFEnableIce = "enable_ice"
//CNFIceMode specifies if turn service is used
const CNFIceService = "ice_service"
//CNFIceUrl specifies the ice candidate url
const CNFIceUrl = "ice_url"

View File

@ -0,0 +1,57 @@
package databag
import (
"encoding/json"
"net/http"
"errors"
"bytes"
"strings"
)
func getIce(service string, urls string, username string, credential string) ([]IceURL, error) {
if service != "" {
gen := "https://rtc.live.cloudflare.com/v1/turn/keys/" + username + "/credentials/generate"
req, err := http.NewRequest(http.MethodPost, gen, bytes.NewBuffer([]byte("{\"ttl\": 86400}")))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Authorization", "Bearer " + credential)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || resp == nil || resp.StatusCode != 201 {
return nil, errors.New("invalid ice service response")
}
var r IceService
err = json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
return nil, errors.New("invalid ice service response")
}
ice := []IceURL{}
for _, url := range r.Servers.URLs {
ice = append(ice, IceURL{ URLs: url, Username: r.Servers.Username, Credential: r.Servers.Credential });
}
return ice, nil
}
return []IceURL {
IceURL {
URLs: urls,
Username: username,
Credential: credential,
},
}, nil
}
func getDefaultIce(ice []IceURL) IceURL {
for _, url := range ice {
if strings.HasSuffix(url.URLs, "?transport=udp") {
return url
}
}
return ice[0];
}

View File

@ -375,7 +375,9 @@ type NodeConfig struct {
EnableIce bool `json:"enableIce"`
IceUrl string `json:"iceUrl"`
IceService string `json:"iceService"`
IceURL string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`
@ -458,7 +460,9 @@ type Phone struct {
CalleeToken string `json:"calleeToken"`
IceUrl string `json:"iceUrl"`
Ice []IceURL `json:"ice,omitEmpty"`
IceURL string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`
@ -563,13 +567,40 @@ type Call struct {
KeepAlive int32 `json:"keepAlive"`
IceUrl string `json:"iceUrl"`
IceService string `json:"iceService"`
Ice []IceURL `json:"ice,omitEmpty"`
IceURL string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`
IcePassword string `json:"icePassword"`
}
type IceServers struct {
URLs []string `json:"urls"`
Username string `json:"username"`
Credential string `json:"credential"`
}
type IceService struct {
Servers IceServers `json:"iceServers"`
}
type IceURL struct {
URLs string `json:"urls"`
Username string `json:"username"`
Credential string `json:"credential"`
}
type Ring struct {
CallID string `json:"callId"`
@ -578,7 +609,9 @@ type Ring struct {
Index int32 `json:"index"`
IceUrl string `json:"iceUrl"`
Ice []IceURL `json:"ice,omitEmpty"`
IceURL string `json:"iceUrl"`
IceUsername string `json:"iceUsername"`

View File

@ -144,6 +144,8 @@ export const en = {
binaryHint: 'Allow binary files to be posted in topics',
enableWeb: 'Enable WebRTC Calls',
webHint: 'Enable audio and video calls to contacts',
enableService: 'Cloudflare Service',
serviceHint: 'Enable Cloudflare Service',
serverUrl: 'WebRTC Server URL',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'WebRTC Username',
@ -350,6 +352,8 @@ export const fr = {
binaryHint: 'Autoriser la publication de fichiers binaires dans les sujets',
enableWeb: 'Activer les Appels WebRTC',
webHint: 'Autoriser les appels audio et vidéo aux contacts',
enableService: 'Service Cloudflare',
serviceHint: 'Activer le Service Cloudflare',
serverUrl: 'URL du Serveur WebRTC',
urlHint: 'turn:ip:port?transport=udp',
webUsername: "Nom d'Utilisateur WebRTC",
@ -557,6 +561,8 @@ export const sp = {
binaryHint: 'Permitir que se publiquen archivos binarios en temas',
enableWeb: 'Activar llamadas WebRTC',
webHint: 'Permitir llamadas de audio y video a contactos',
enableService: 'Servicio Cloudflare',
serviceHint: 'Habilitar el Servicio Cloudflare',
serverUrl: 'URL del servidor WebRTC',
urlHint: 'turn:ip:puerto?transporte=udp',
webUsername: 'Nombre de usuario WebRTC',
@ -763,6 +769,8 @@ export const pt = {
binaryHint: 'Permitir que arquivos binários sejam postados em tópicos',
enableWeb: 'Ativar chamadas WebRTC',
webHint: 'Permitir chamadas de áudio e vídeo para contatos',
enableService: 'Serviço Cloudflare',
serviceHint: 'Habilitar serviço Cloudflare',
serverUrl: 'URL do servidor WebRTC',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'Nome de usuário WebRTC',
@ -969,6 +977,8 @@ export const de = {
binaryHint: 'Erlauben Sie die Veröffentlichung von Binärdateien in Themen',
enableWeb: 'WebRTC-Anrufe aktivieren',
webHint: 'Audio- und Videoanrufe an Kontakte zulassen',
enableService: 'Cloudflare-Dienst',
serviceHint: 'Aktivieren Sie den Cloudflare-Dienst',
serverUrl: 'URL des WebRTC-Servers',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'WebRTC-Benutzername',
@ -1175,6 +1185,8 @@ export const ru = {
binaryHint: 'Разрешить публикацию двоичных файлов в темах',
enableWeb: 'Включить WebRTC-звонки',
webHint: 'Разрешить аудио- и видеозвонки контактам',
enableService: 'Сервис Cloudflare',
serviceHint: 'Включить службу Cloudflare',
serverUrl: 'URL сервера WebRTC',
urlHint: 'turn:ip:port?transport=udp',
webUsername: 'Имя пользователя WebRTC',

View File

@ -185,8 +185,9 @@ export function useAppContext(websocket) {
setAppRevision(activity.revision);
}
else if (activity.ring) {
const { cardId, callId, calleeToken, iceUrl, iceUsername, icePassword } = activity.ring;
ringContext.actions.ring(cardId, callId, calleeToken, iceUrl, iceUsername, icePassword);
const { cardId, callId, calleeToken, ice, iceUrl, iceUsername, icePassword } = activity.ring;
const config = ice ? ice : [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
ringContext.actions.ring(cardId, callId, calleeToken, config);
}
else {
setAppRevision(activity);

View File

@ -300,9 +300,9 @@ export function useRingContext() {
clearToken: () => {
access.current = null;
},
ring: (cardId, callId, calleeToken, iceUrl, iceUsername, icePassword) => {
ring: (cardId, callId, calleeToken, ice) => {
const key = `${cardId}:${callId}`
const call = ringing.current.get(key) || { cardId, calleeToken, callId, iceUrl, iceUsername, icePassword }
const call = ringing.current.get(key) || { cardId, calleeToken, callId, ice }
call.expires = Date.now() + EXPIRE;
ringing.current.set(key, call);
updateState({ ringing: ringing.current });
@ -334,13 +334,13 @@ export function useRingContext() {
}
}
},
accept: async (cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword, audioId) => {
accept: async (cardId, callId, contactNode, contactToken, calleeToken, ice, audioId) => {
console.log("ACCEPT", ice);
if (calling.current) {
throw new Error("active session");
}
const ice = [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
const key = `${cardId}:${callId}`
const call = ringing.current.get(key);
if (call) {
@ -398,9 +398,10 @@ export function useRingContext() {
}
let index = 0;
const { id, keepAlive, callerToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { id, keepAlive, callerToken, calleeToken, ice } = call;
try {
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, iceUrl, iceUsername, icePassword });
const turn = ice[ice.length - 1]; //backwards compatibility
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, ice, iceUrl: turn.urls, iceUsername: turn.username, icePassword: turn.credential });
}
catch (err) {
console.log(err);
@ -421,7 +422,8 @@ export function useRingContext() {
}
}
else {
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, iceUrl, iceUsername, icePassword });
const turn = ice[ice.length - 1];
await addContactRing(contactNode, contactToken, { index, callId: id, calleeToken, ice, iceUrl: turn.urls, iceUsername: turn.username, icePassword: turn.credential });
index += 1;
}
}
@ -432,7 +434,6 @@ export function useRingContext() {
updateState({ callStatus: "ringing" });
calling.current = { callId: id, host: true };
const ice = [{ urls: iceUrl, username: iceUsername, credential: icePassword }];
await connect('polite', audioId, window.location.host, callerToken, () => clearInterval(ringInterval), () => clearInterval(aliveInterval), ice);
},
enableVideo: async (videoId) => {

View File

@ -265,21 +265,34 @@ export function Dashboard() {
defaultChecked={false} checked={state.enableIce} />
</div>
</Tooltip>
<div className="field">
<div>{state.strings.serverUrl}</div>
<Input placeholder={state.strings.urlHint} onChange={(e) => actions.setIceUrl(e.target.value)}
disabled={!state.enableIce} value={state.iceUrl} />
</div>
<div className="field">
<div>{state.strings.webUsername}</div>
<Input placeholder={state.strings.username} onChange={(e) => actions.setIceUsername(e.target.value)}
disabled={!state.enableIce} value={state.iceUsername} />
</div>
<div className="field">
<div>{state.strings.webPassword}</div>
<Input placeholder={state.strings.password} onChange={(e) => actions.setIcePassword(e.target.value)}
disabled={!state.enableIce} value={state.icePassword} />
</div>
{ state.enableIce && (
<div className="iceInput">
<Tooltip placement="topLeft" title={state.strings.serviceHint}>
<div className="field">
<div>{state.strings.enableService}</div>
<Switch onChange={(e) => actions.setIceServiceFlag(e)} size="small"
defaultChecked={false} checked={state.iceServiceFlag} />
</div>
</Tooltip>
{ !state.iceServiceFlag && (
<div className="field">
<div>{state.strings.serverUrl}</div>
<Input placeholder={state.strings.urlHint} onChange={(e) => actions.setIceUrl(e.target.value)}
value={state.iceUrl} />
</div>
)}
<div className="field">
<div>{state.iceServiceFlag ? 'TURN_KEY_ID' : state.strings.webUsername}</div>
<Input placeholder={state.strings.username} onChange={(e) => actions.setIceUsername(e.target.value)}
value={state.iceUsername} />
</div>
<div className="field">
<div>{state.iceServiceFlag ? 'TURN_KEY_API_TOKEN' : state.strings.webPassword}</div>
<Input placeholder={state.strings.password} onChange={(e) => actions.setIcePassword(e.target.value)}
value={state.icePassword} />
</div>
</div>
)}
<div className="control">
<Button key="back" onClick={() => actions.setShowSettings(false)}>{state.strings.cancel}</Button>
<Button key="save" type="primary" onClick={() => actions.setSettings()} loading={state.busy}>{state.strings.save}</Button>

View File

@ -111,6 +111,12 @@ export const SettingsLayout = styled(Space)`
min-height: 32px;
}
.iceInput {
display: flex;
flex-direction: column;
gap: 8px;
}
.field {
white-space: nowrap;
display: flex;

View File

@ -27,6 +27,7 @@ export function useDashboard(token) {
enableVideo: null,
enableBinary: null,
enableIce: null,
iceServiceFlag: null,
iceUrl: null,
iceUsername: null,
icePassword: null,
@ -128,6 +129,10 @@ export function useDashboard(token) {
setEnableIce: (enableIce) => {
updateState({ enableIce });
},
setIceServiceFlag: (iceServiceFlag) => {
const iceUrl = iceServiceFlag ? 'https://rtc.live.cloudflare.com/v1/turn/keys/%%TURN_KEY_ID%%/credentials/generate' : '';
updateState({ iceServiceFlag, iceUrl });
},
setIceUrl: (iceUrl) => {
updateState({ iceUrl });
},
@ -181,9 +186,10 @@ export function useDashboard(token) {
if (!state.busy) {
updateState({ busy: true });
try {
const { domain, keyType, accountStorage, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = state;
const { domain, keyType, accountStorage, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceServiceFlag, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = state;
const storage = accountStorage * 1073741824;
const config = { domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit };
const iceService = iceServiceFlag ? 'cloudflare' : '';
const config = { domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceService, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit };
await setNodeConfig(app.state.adminToken, config);
updateState({ busy: false, showSettings: false });
}
@ -200,9 +206,10 @@ export function useDashboard(token) {
try {
const enabled = await getAdminMFAuth(app.state.adminToken);
const config = await getNodeConfig(app.state.adminToken);
const { accountStorage, domain, keyType, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = config;
const { accountStorage, domain, keyType, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceService, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = config;
const iceServiceFlag = iceService === 'cloudflare';
const storage = Math.ceil(accountStorage / 1073741824);
updateState({ mfAuthSet: true, mfaAuthEnabled: enabled, configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit });
updateState({ mfAuthSet: true, mfaAuthEnabled: enabled, configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceServiceFlag, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit });
}
catch(err) {
console.log(err);

View File

@ -58,14 +58,14 @@ export function useSession() {
const expired = Date.now();
ring.state.ringing.forEach(call => {
if (call.expires > expired && !call.status) {
const { callId, cardId, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { callId, cardId, calleeToken, ice } = call;
const contact = card.state.cards.get(cardId);
if (contact) {
const { imageSet, name, handle, node, guid } = contact.data.cardProfile || {};
const { token } = contact.data.cardDetail;
const contactToken = `${guid}.${token}`;
const img = imageSet ? card.actions.getCardImageUrl(cardId) : null;
ringing.push({ cardId, img, name, handle, contactNode: node, callId, contactToken, calleeToken, iceUrl, iceUsername, icePassword });
ringing.push({ cardId, img, name, handle, contactNode: node, callId, contactToken, calleeToken, ice });
}
}
});
@ -180,9 +180,9 @@ export function useSession() {
await ring.actions.decline(cardId, node, contactToken, callId);
},
accept: async (call) => {
const { cardId, callId, contactNode, contactToken, calleeToken, iceUrl, iceUsername, icePassword } = call;
const { cardId, callId, contactNode, contactToken, calleeToken, ice } = call;
const node = contactNode ? contactNode : window.location.host;
await ring.actions.accept(cardId, callId, node, contactToken, calleeToken, iceUrl, iceUsername, icePassword, state.audioId);
await ring.actions.accept(cardId, callId, node, contactToken, calleeToken, ice, state.audioId);
},
end: async () => {
await ring.actions.end();