Compare commits

..

8 Commits

Author SHA1 Message Date
Vadim A. Misbakh-Soloviov
a08da498dc (feat) OpenResty Support
Signed-off-by: Vadim A. Misbakh-Soloviov <git@mva.name>
2022-11-07 22:28:44 +07:00
Hristo Terezov
a995b33753 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1528.0.0+23644901...v1530.0.0+f2af389e
2022-11-03 07:28:05 +01:00
Nils Ohlmeier
bfb15a2523 chore(deps) @jitsi/rtcstats 9.4.0 2022-11-03 07:26:14 +01:00
TTG
1d59c8122d fix(lang) update Chinese translations (#12344)
* Update Simplified Chinese translation

* Update Traditional Chinese translation

* Update translations

* Updated translation for #12391

* Updated translation for 12371
2022-11-02 16:41:26 -05:00
Hristo Terezov
31766c891b Fix get rooms info (#12492)
* Include local participant; filter out hidden participants for getRoomsInfo

* Review fixes: include ts changes and types

Co-authored-by: Bogdan Duduman <bogdan.duduman@8x8.com>
2022-11-02 12:06:45 -05:00
Robert Pintilii
7a3b8d6ac4 fix(recording-dialog) Fix broken dialog content (#12490) 2022-11-02 12:49:30 +02:00
Saúl Ibarra Corretgé
edf5e1c094 fix(ts) fix mysterious linting errors
We have a rule that should apply here, but somehow it doesn't...
2022-11-02 09:03:14 +01:00
Saúl Ibarra Corretgé
7cd39b7983 feat(ts) make tsc happy 2022-11-02 09:03:14 +01:00
73 changed files with 1163 additions and 897 deletions

View File

@@ -62,12 +62,14 @@ import {
import {
checkAndNotifyForNewDevice,
getAvailableDevices,
getDefaultDeviceId,
notifyCameraError,
notifyMicError,
setAudioOutputDeviceId,
updateDeviceList
} from './react/features/base/devices';
} from './react/features/base/devices/actions.web';
import {
getDefaultDeviceId,
setAudioOutputDeviceId
} from './react/features/base/devices/functions.web';
import {
JitsiConferenceErrors,
JitsiConferenceEvents,

2
debian/control vendored
View File

@@ -20,7 +20,7 @@ Description: WebRTC JavaScript video conferences
Package: jitsi-meet-web-config
Architecture: all
Depends: openssl, nginx | nginx-full | nginx-extras | apache2, curl
Depends: openssl, nginx | nginx-full | nginx-extras | openresty | apache2, curl
Description: Configuration for web serving of Jitsi Meet
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
Videobridge to provide high quality, scalable video conferences.

View File

@@ -57,6 +57,10 @@ case "$1" in
|| [ "$NGINX_EXTRAS_INSTALL_CHECK" = "unpacked" ] ; then
FORCE_NGINX="true"
fi
OPENRESTY_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'openresty' 2>/dev/null | awk '{print $3}' || true)"
if [ "$OPENRESTY_INSTALL_CHECK" = "installed" ] || [ "$OPENRESTY_INSTALL_CHECK" = "unpacked" ] ; then
FORCE_OPENRESTY="true"
fi
APACHE_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'apache2' 2>/dev/null | awk '{print $3}' || true)"
if [ "$APACHE_INSTALL_CHECK" = "installed" ] || [ "$APACHE_INSTALL_CHECK" = "unpacked" ] ; then
FORCE_APACHE="true"
@@ -182,21 +186,41 @@ case "$1" in
echo "config.flags.receiveMultipleVideoStreams = true;" >> $JITSI_MEET_CONFIG
fi
if [[ "$FORCE_NGINX" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
if [[ "$FORCE_OPENRESTY" = "true" ]]; then
NGX_COMMON_CONF_PATH="/usr/local/openresty/nginx/conf/$JVB_HOSTNAME.conf"
NGX_SVC_NAME=openresty
OPENRESTY_NGX_CONF="/usr/local/openresty/nginx/conf/nginx.conf"
else
NGX_COMMON_CONF_PATH="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
NGX_SVC_NAME=nginx
fi
if [[ ( "$FORCE_NGINX" = "true" || "$FORCE_OPENRESTY" = "true" ) && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
# this is a reconfigure, lets just delete old links
if [ "$RECONFIGURING" = "true" ] ; then
rm -f /etc/nginx/sites-enabled/$JVB_HOSTNAME_OLD.conf
rm -f /etc/jitsi/meet/$JVB_HOSTNAME_OLD-config.js
if [[ "$FORCE_OPENRESTY" = "true" ]]; then
sed -i "/include.*$JVB_HOSTNAME_OLD/d" "$OPENRESTY_NGX_CONF"
fi
fi
# nginx conf
if [ ! -f /etc/nginx/sites-available/$JVB_HOSTNAME.conf ] ; then
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf
if [ ! -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf ] ; then
ln -s /etc/nginx/sites-available/$JVB_HOSTNAME.conf /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
if [ ! -f "$NGX_COMMON_CONF_PATH" ] ; then
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example "$NGX_COMMON_CONF_PATH"
if [ ! -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf ] && ! [[ "$FORCE_OPENRESTY" = "true" ]] ; then
ln -s "$NGX_COMMON_CONF_PATH" /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
fi
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" "$NGX_COMMON_CONF_PATH"
if [[ "$FORCE_OPENRESTY" = "true" ]]; then
OPENRESTY_NGX_CONF_MD5_ORIG=$(dpkg-query -s openresty | sed -n '/\/nginx\.conf /{s@.* @@;p}')
OPENRESTY_NGX_CONF_MD5_USERS=$(md5sum "$OPENRESTY_NGX_CONF" | sed 's@ .*@@')
if [[ "$OPENRESTY_NGX_CONF_MD5_USERS" = "$OPENRESTY_NGX_CONF_MD5_ORIG" ]]; then
sed -i "/^http \x7b/,/^\x7d/s@^\x7d@\tinclude $NGX_COMMON_CONF_PATH;\n\x7d@" "$OPENRESTY_NGX_CONF"
fi
fi
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/nginx/sites-available/$JVB_HOSTNAME.conf
fi
if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
@@ -204,14 +228,14 @@ case "$1" in
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
sed -i "s/ssl_certificate_key\ \/etc\/jitsi\/meet\/.*key/ssl_certificate_key\ $CERT_KEY_ESC/g" \
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
"$NGX_COMMON_CONF_PATH"
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
sed -i "s/ssl_certificate\ \/etc\/jitsi\/meet\/.*crt/ssl_certificate\ $CERT_CRT_ESC/g" \
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
"$NGX_COMMON_CONF_PATH"
fi
invoke-rc.d nginx reload || true
invoke-rc.d $NGX_SVC_NAME reload || true
elif [[ "$FORCE_APACHE" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
# this is a reconfigure, lets just delete old links

View File

@@ -24,6 +24,9 @@ set -e
case "$1" in
remove)
if [ -x "/etc/init.d/openresty" ]; then
invoke-rc.d openresty reload || true
fi
if [ -x "/etc/init.d/nginx" ]; then
invoke-rc.d nginx reload || true
fi
@@ -38,6 +41,7 @@ case "$1" in
rm -f /etc/jitsi/meet/$JVB_HOSTNAME-config.js
rm -f /etc/nginx/sites-available/$JVB_HOSTNAME.conf
rm -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
rm -f /usr/local/openresty/nginx/conf/$JVB_HOSTNAME.conf
rm -f /etc/apache2/sites-available/$JVB_HOSTNAME.conf
rm -f /etc/apache2/sites-enabled/$JVB_HOSTNAME.conf
rm -f /etc/jitsi/meet/$JVB_HOSTNAME.key

14
globals.native.d.ts vendored
View File

@@ -12,8 +12,21 @@ interface IWindow {
JITSI_MEET_LITE_SDK: boolean;
JitsiMeetJS: any;
config: IConfig;
document: any;
innerHeight: number;
innerWidth: number;
interfaceConfig: any;
location: ILocation;
self: any;
top: any;
onerror: (event: string, source: any, lineno: any, colno: any, e: Error) => void;
onunhandledrejection: (event: any) => void;
setTimeout: typeof setTimeout;
clearTimeout: typeof clearTimeout;
setImmediate: typeof setImmediate;
clearImmediate: typeof clearImmediate;
}
interface INavigator {
@@ -22,6 +35,7 @@ interface INavigator {
declare global {
const APP: any;
const document: any;
const interfaceConfig: any;
const navigator: INavigator;
const window: IWindow;

View File

@@ -82,7 +82,7 @@
},
"labels": {
"buttonLabel": "驾驶模式",
"title": "安全驾驶模式",
"title": "驾驶模式",
"videoStopped": "你的视频已停止"
}
},
@@ -104,6 +104,7 @@
},
"noMessagesMessage": "会议中还没有消息,在这里开始谈话吧!",
"privateNotice": "与{{recipient}}的私聊",
"sendButton": "发送",
"smileysPanel": "表情符号面板",
"tabs": {
"chat": "聊天",
@@ -270,6 +271,7 @@
"gracefulShutdown": "我们目前正在维护中,请稍后再试。",
"grantModeratorDialog": "你确定要授予{{participantName}}主持人权限吗?",
"grantModeratorTitle": "授予主持人权限",
"hide": "隐藏",
"hideShareAudioHelper": "不要再显示",
"incorrectPassword": "错误的用户名或者密码",
"incorrectRoomLockPassword": "密码错误",
@@ -387,6 +389,7 @@
"shareYourScreenDisabled": "共享屏幕已禁用。",
"sharedVideoDialogError": "错误:网址无效",
"sharedVideoLinkPlaceholder": "YouTube或视频链接",
"show": "显示",
"start": "开始",
"startLiveStreaming": "开始直播",
"startRecording": "开始录制",
@@ -435,7 +438,7 @@
"search": "搜索GIPHY"
},
"helpView": {
"header": "帮助中心"
"title": "帮助中心"
},
"incomingCall": {
"answer": "接听",
@@ -553,6 +556,7 @@
"signedInAs": "你当前登录为:",
"start": "开始直播",
"streamIdHelp": "这是什么?",
"title": "直播",
"unavailableTitle": "直播不可用",
"youtubeTerms": "YouTube服务条款"
},
@@ -595,6 +599,7 @@
"passwordJoinButton": "加入",
"reject": "拒绝",
"rejectAll": "拒绝全部",
"title": "大厅",
"toggleLabel": "开启大厅模式"
},
"localRecording": {
@@ -621,6 +626,7 @@
"no": "否",
"participant": "参会者",
"participantStats": "参会者状态",
"selectTabTitle": "🎥请选择此标签页进行录制",
"sessionToken": "会话Token",
"start": "开始录制",
"stop": "停止录制",
@@ -737,13 +743,13 @@
"videoModeration": "开启视频"
},
"close": "关闭",
"header": "参会者",
"headings": {
"lobby": "大厅(({{count}}人)",
"participantsList": "会议参会者({{count}}人)",
"waitingLobby": "在大厅等待({{count}}人)"
},
"search": "搜索参会者"
"search": "搜索参会者",
"title": "参会者"
},
"passwordDigitsOnly": "最多{{number}}位数字",
"passwordSetRemotely": "由其他参会者设置",
@@ -853,7 +859,7 @@
"ringing": "响铃中……"
},
"privacyView": {
"header": "隐私"
"title": "隐私"
},
"profile": {
"avatar": "头像",
@@ -925,6 +931,7 @@
"signIn": "登录",
"signOut": "注销",
"surfaceError": "请选择当前标签页",
"title": "录制中",
"unavailable": "{{serviceName}}目前无法使用,我们正在努力解决这个问题,请稍后再试。",
"unavailableTitle": "录制不可用",
"uploadToCloud": "上传至云端"
@@ -936,8 +943,8 @@
"security": {
"about": "你可以为会议添加一个$t(lockRoomPassword),参会者需要输入$t(lockRoomPassword)才能加入会议。",
"aboutReadOnly": "主持人可以为会议添加一个$t(lockRoomPassword)),参会者需要输入$t(lockRoomPassword)才能加入会议。",
"header": "安全选项",
"insecureRoomNameWarning": "会议室名称过于简单,任何人都可以加入此会议,请考虑使用安全选项以确保你的会议安全。"
"insecureRoomNameWarning": "会议室名称过于简单,任何人都可以加入此会议,请考虑使用安全选项以确保你的会议安全。",
"title": "安全选项"
},
"settings": {
"buttonLabel": "设置",
@@ -953,7 +960,7 @@
"desktopShareWarning": "你需要重新启动共享屏幕以使新设置生效。",
"devices": "设备",
"followMe": "所有人跟随",
"framesPerSecond": "帧",
"framesPerSecond": "帧",
"incomingMessage": "新消息",
"language": "语言",
"loggedIn": "以{{name}}登录",
@@ -1004,6 +1011,7 @@
"profileSection": "简介",
"serverURL": "服务器网址",
"showAdvanced": "显示高级设置",
"startCarModeInLowBandwidthMode": "同时开启驾驶模式和省流模式",
"startWithAudioMuted": "关闭音频并启动",
"startWithVideoMuted": "关闭视频并启动",
"terms": "条款",
@@ -1042,7 +1050,7 @@
"title": "由于你的电脑进入休眠模式,视频通话已经中断。"
},
"termsView": {
"header": "条款"
"title": "条款"
},
"toggleTopPanelLabel": "打开/关闭顶部面板",
"toolbar": {
@@ -1218,6 +1226,8 @@
"labelToolTip": "会议正在转录中",
"off": "转录已停止",
"pending": "准备转录会议中……",
"sourceLanguageDesc": "当前会议语言设置为<b>{{sourceLanguage}}</b><br/>你可以在这里",
"sourceLanguageHere": "更改",
"start": "开启显示字幕",
"stop": "停止显示字幕",
"subtitles": "字幕",
@@ -1337,12 +1347,12 @@
"jitsiOnMobile": "手机版Jitsi 下载我们的APP随时随地都能开始会议",
"join": "创建/加入",
"logo": {
"calendar": "日历logo",
"calendar": "日历图标",
"desktopPreviewThumbnail": "桌面预览缩略图",
"googleLogo": "谷歌logo",
"logoDeepLinking": "Jitsi meet logo",
"microsoftLogo": "微软logo",
"policyLogo": "政策logo"
"googleLogo": "谷歌图标",
"logoDeepLinking": "Jitsi Meet图标",
"microsoftLogo": "微软图标",
"policyLogo": "政策图标"
},
"mobileDownLoadLinkAndroid": "从Google Play下载安卓版手机APP",
"mobileDownLoadLinkFDroid": "从F-Droid下载安卓版手机APP",
@@ -1353,7 +1363,7 @@
"recentListDelete": "删除",
"recentListEmpty": "近期会议为空,与你的团队参会者聊天后,历史会议记录会出现在这里。",
"reducedUIText": "欢迎使用{{app}}",
"roomNameAllowedChars": "会议室名称不应包含以下任何字符:? & : ' \" % #",
"roomNameAllowedChars": "会议室名称不应包含以下字符:? & : ' \" % #",
"roomname": "请输入会议室名称",
"roomnameHint": "输入你想加入的会议室的名称或网址,你也可以使用不同的名称创建会议室,其他人只需输入相同的名称即可加入。",
"sendFeedback": "发送反馈",

View File

@@ -60,7 +60,7 @@
},
"calendarSync": {
"addMeetingURL": "增加會議連結",
"confirmAddLink": "您要為此活動加入 Jitsi 連結嗎?",
"confirmAddLink": "您要為此活動加入Jitsi連結嗎",
"error": {
"appConfiguration": "行事曆整合尚未正確設定。",
"generic": "發生錯誤,請檢查行事曆設定,或是重新整理行事曆。",
@@ -82,7 +82,7 @@
},
"labels": {
"buttonLabel": "駕駛模式",
"title": "安全駕駛模式",
"title": "駕駛模式",
"videoStopped": "您的視訊已停用"
}
},
@@ -104,6 +104,7 @@
},
"noMessagesMessage": "此會議尚無訊息,在此開始對話聊天!",
"privateNotice": "傳送私人訊息至{{recipient}}",
"sendButton": "傳送",
"smileysPanel": "Emoji 面板",
"tabs": {
"chat": "聊天",
@@ -118,7 +119,7 @@
"buttonTextEdge": "安裝 Edge 外掛程式",
"close": "關閉",
"dontShowAgain": "不要再問了",
"installExtensionText": "安裝適用於 Google 行事曆及 Office 365 整合的擴充功能"
"installExtensionText": "安裝適用於Google行事曆及Office 365整合的擴充功能"
},
"connectingOverlay": {
"joiningRoom": "正在將您連接至您的會議……"
@@ -140,7 +141,7 @@
},
"connectionindicator": {
"address": "位址:",
"audio_ssrc": "音訊 SSRC",
"audio_ssrc": "音訊SSRC",
"bandwidth": "估計頻寬:",
"bitrate": "連線速率:",
"bridgeCount": "伺服器數量:",
@@ -155,7 +156,7 @@
"maxEnabledResolution": "最大傳輸",
"more": "顯示更多",
"packetloss": "丟包率:",
"participant_id": "與會者 ID",
"participant_id": "與會者ID",
"quality": {
"good": "很好",
"inactive": "未啟用",
@@ -172,7 +173,7 @@
"status": "連接:",
"transport": "傳輸協定:",
"transport_plural": "傳輸:",
"video_ssrc": "視訊 SSRC"
"video_ssrc": "視訊SSRC"
},
"dateUtils": {
"earlier": "稍早",
@@ -183,10 +184,10 @@
"appNotInstalled": "您需要在手機上安裝{{app}}行動應用程式才能加入這場會議。",
"description": "甚麼事情都沒發生?我們已嘗試在您的{{app}}桌面應用程式開啟會議。請再試一次,或是在{{app}}網路應用程式開啟會議。",
"descriptionWithoutWeb": "甚麼事情都沒發生?我們已試著將您的會議在桌面應用程式{{app}}中啟動。",
"downloadApp": "下載 App",
"ifDoNotHaveApp": "如果您尚未安裝 App",
"ifHaveApp": "如果您已經App",
"joinInApp": "使用 App 加入會議",
"downloadApp": "下載App",
"ifDoNotHaveApp": "如果您尚未安裝App",
"ifHaveApp": "如果您已經App",
"joinInApp": "使用App加入會議",
"launchWebButton": "在瀏覽器開啟",
"title": "正在{{app}}發起您的會議……",
"tryAgainButton": "在桌面上再試一次",
@@ -270,6 +271,7 @@
"gracefulShutdown": "我們目前正在維護中,請稍後再試。",
"grantModeratorDialog": "您確定要授予{{participantName}}主持人權限嗎?",
"grantModeratorTitle": "授予主持人權限",
"hide": "隱藏",
"hideShareAudioHelper": "不再顯示",
"incorrectPassword": "錯誤的用戶名稱或密碼",
"incorrectRoomLockPassword": "密碼不符",
@@ -387,6 +389,7 @@
"shareYourScreenDisabled": "畫面分享已停用。",
"sharedVideoDialogError": "錯誤:網址無效",
"sharedVideoLinkPlaceholder": "YouTube或影片網址",
"show": "顯示",
"start": "開始",
"startLiveStreaming": "啟動直播串流",
"startRecording": "啟動錄製作業",
@@ -435,7 +438,7 @@
"search": "搜尋 GIPHY"
},
"helpView": {
"header": "說明中心"
"title": "說明中心"
},
"incomingCall": {
"answer": "接通",
@@ -553,6 +556,7 @@
"signedInAs": "您目前登入名稱為:",
"start": "啟動直播串流",
"streamIdHelp": "這是什麼?",
"title": "直播串流",
"unavailableTitle": "直播串流無法使用",
"youtubeTerms": "YouTube服務條款"
},
@@ -595,6 +599,7 @@
"passwordJoinButton": "加入",
"reject": "拒絕",
"rejectAll": "拒絕所有人",
"title": "大廳",
"toggleLabel": "啟用大廳模式"
},
"localRecording": {
@@ -621,6 +626,7 @@
"no": "否",
"participant": "與會者",
"participantStats": "與會者狀態",
"selectTabTitle": "🎥請選擇此分頁進行錄製",
"sessionToken": "工作階段Token",
"start": "啟動錄製",
"stop": "停用錄製",
@@ -737,13 +743,13 @@
"videoModeration": "開啟視訊"
},
"close": "關閉",
"header": "與會者",
"headings": {
"lobby": "大廳({{count}}人)",
"participantsList": "會議與會者({{count}}人)",
"waitingLobby": "於大廳等候({{count}}人)"
},
"search": "搜尋與會者"
"search": "搜尋與會者",
"title": "與會者"
},
"passwordDigitsOnly": "上限為{{number}}位數",
"passwordSetRemotely": "由其他與會者設定",
@@ -824,7 +830,7 @@
"initiated": "通話已初始化",
"joinAudioByPhone": "使用手機音訊裝置加入",
"joinMeeting": "加入會議",
"joinMeetingInLowBandwidthMode": "以低寬模式加入",
"joinMeetingInLowBandwidthMode": "以低寬模式加入",
"joinWithoutAudio": "無音訊情況下加入",
"keyboardShortcuts": "啟用鍵盤快捷鍵",
"linkCopied": "連結已複製到剪貼簿",
@@ -853,7 +859,7 @@
"ringing": "鈴鈴鈴……"
},
"privacyView": {
"header": "隱私權"
"title": "隱私權"
},
"profile": {
"avatar": "頭像",
@@ -925,6 +931,7 @@
"signIn": "登入",
"signOut": "登出",
"surfaceError": "請選擇當前分頁",
"title": "錄製中",
"unavailable": "喔哦!{{serviceName}}目前無法使用,我們正在解決此問題,請稍後再試。",
"unavailableTitle": "錄製無法使用",
"uploadToCloud": "上傳至雲端"
@@ -936,8 +943,8 @@
"security": {
"about": "您可以添加$t(lockRoomPassword)至您的會議,與會者在加入會議前必須先輸入$t(lockRoomPassword)。",
"aboutReadOnly": "主持人可以添加$t(lockRoomPassword)至會議,與會者在加入會議前必須先輸入$t(lockRoomPassword)。",
"header": "安全性選項",
"insecureRoomNameWarning": "會議室名稱過於簡單,任何人都可以加入此會議,請考慮使用安全性選項以保護您的會議安全。"
"insecureRoomNameWarning": "會議室名稱過於簡單,任何人都可以加入此會議,請考慮使用安全性選項以保護您的會議安全。",
"title": "安全性選項"
},
"settings": {
"buttonLabel": "設定",
@@ -953,7 +960,7 @@
"desktopShareWarning": "您必須重新啟動桌面畫面分享以套用新的設定。",
"devices": "裝置",
"followMe": "全部人跟隨我",
"framesPerSecond": "影格率",
"framesPerSecond": "fps",
"incomingMessage": "新訊息",
"language": "語言",
"loggedIn": "以{{name}}登入",
@@ -1004,6 +1011,7 @@
"profileSection": "簡介",
"serverURL": "伺服器網址",
"showAdvanced": "顯示進階設定",
"startCarModeInLowBandwidthMode": "同時啟用駕駛模式與低頻寬模式",
"startWithAudioMuted": "啟動並靜音",
"startWithVideoMuted": "啟動並關閉影像",
"terms": "條款",
@@ -1042,7 +1050,7 @@
"title": "由於電腦進入休眠,您的視訊通話已經中斷。"
},
"termsView": {
"header": "條款"
"title": "條款"
},
"toggleTopPanelLabel": "啟用/停用頂部面板",
"toolbar": {
@@ -1218,6 +1226,8 @@
"labelToolTip": "此會議正在轉錄",
"off": "轉錄已停用",
"pending": "準備轉錄會議……",
"sourceLanguageDesc": "會議語言當前設定為<b>{{sourceLanguage}}</b><br/>您可以在這裡",
"sourceLanguageHere": "修改",
"start": "開始顯示字幕",
"stop": "停用顯示字幕",
"subtitles": "字幕",
@@ -1338,10 +1348,10 @@
"join": "建立/加入",
"logo": {
"calendar": "行事曆圖示",
"desktopPreviewThumbnail": "桌面畫面分享縮圖",
"googleLogo": "Google 商標",
"logoDeepLinking": "Jitsi meet 商標",
"microsoftLogo": "Microsoft 商標",
"desktopPreviewThumbnail": "桌面預覽縮圖",
"googleLogo": "Google圖示",
"logoDeepLinking": "Jitsi Meet圖示",
"microsoftLogo": "Microsoft圖示",
"policyLogo": "政策圖示"
},
"mobileDownLoadLinkAndroid": "下載 Android 版本的手機應用程式",
@@ -1353,7 +1363,7 @@
"recentListDelete": "刪除",
"recentListEmpty": "目前最近使用是空白的,與您的團隊成員聊天,即會在此處找到最近使用過的會議。",
"reducedUIText": "歡迎使用{{app}}",
"roomNameAllowedChars": "會議室名稱不應包含以下字元:? & : ' % #",
"roomNameAllowedChars": "會議室名稱不應包含以下字元:? & : ' \" % #",
"roomname": "輸入會議室名稱",
"roomnameHint": "請輸入您想加入的會議室名稱或網址,您可以用個名稱來建立會議室,只要其他人輸入相同的名稱就能加入會議室喔。",
"sendFeedback": "傳送回饋",

View File

@@ -1,10 +1,12 @@
/* global APP, JitsiMeetJS */
import {
getAudioOutputDeviceId,
notifyCameraError,
notifyMicError
} from '../../react/features/base/devices';
} from '../../react/features/base/devices/actions.web';
import {
getAudioOutputDeviceId
} from '../../react/features/base/devices/functions.web';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId,

24
package-lock.json generated
View File

@@ -31,7 +31,7 @@
"@jitsi/js-utils": "2.0.4",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.3.0",
"@jitsi/rtcstats": "9.4.0",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",
"@mui/material": "5.10.2",
@@ -74,7 +74,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1528.0.0+23644901/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1530.0.0+f2af389e/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -3780,9 +3780,9 @@
"integrity": "sha512-JujivPbOUvdRYa2xqByHYKfKGNGa7ZPyNLaNuh8hEp9XsiNfjaJAHdboq6M1VY9TP+765nyxC0LjpAw1VkikOQ=="
},
"node_modules/@jitsi/rtcstats": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.3.0.tgz",
"integrity": "sha512-aipr1Tt/vfouMmgISCSu64Np3pD1u51y/2SztYNDt5bd6f79Qrieceu0JFqZWxC9KQRsamoJL7Mb9qxo2KkULg==",
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.4.0.tgz",
"integrity": "sha512-NZXgJUAX6Mvexes7zAnHOiU+F2O7NIdyRUcir1YUD85mvBV0DMjuwUnIL5XaYkCzDuE3rTcV2FX9B80BTRlnLQ==",
"dependencies": {
"@jitsi/js-utils": "^2.0.0",
"sdp": "^3.0.3",
@@ -13497,8 +13497,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1528.0.0+23644901/lib-jitsi-meet.tgz",
"integrity": "sha512-pE11TMMGVAkuFncsh2Z7SaYt6EetVv8AlU/ZfT5+W5vKqw1R0aKrTm2Lvz2cGtzJ4ooCkghsZwQYHQo6MuuZ9Q==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1530.0.0+f2af389e/lib-jitsi-meet.tgz",
"integrity": "sha512-gqsNJblQ5wgYZJzhbkI7iBbg5Ddn9/EyfiCOwYdB9lHe07yDYco7H/vUH/TxTFTurEHtyV8LKb5KMEhJIKVhpw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -23208,9 +23208,9 @@
"integrity": "sha512-JujivPbOUvdRYa2xqByHYKfKGNGa7ZPyNLaNuh8hEp9XsiNfjaJAHdboq6M1VY9TP+765nyxC0LjpAw1VkikOQ=="
},
"@jitsi/rtcstats": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.3.0.tgz",
"integrity": "sha512-aipr1Tt/vfouMmgISCSu64Np3pD1u51y/2SztYNDt5bd6f79Qrieceu0JFqZWxC9KQRsamoJL7Mb9qxo2KkULg==",
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.4.0.tgz",
"integrity": "sha512-NZXgJUAX6Mvexes7zAnHOiU+F2O7NIdyRUcir1YUD85mvBV0DMjuwUnIL5XaYkCzDuE3rTcV2FX9B80BTRlnLQ==",
"requires": {
"@jitsi/js-utils": "^2.0.0",
"sdp": "^3.0.3",
@@ -30510,8 +30510,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1528.0.0+23644901/lib-jitsi-meet.tgz",
"integrity": "sha512-pE11TMMGVAkuFncsh2Z7SaYt6EetVv8AlU/ZfT5+W5vKqw1R0aKrTm2Lvz2cGtzJ4ooCkghsZwQYHQo6MuuZ9Q==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1530.0.0+f2af389e/lib-jitsi-meet.tgz",
"integrity": "sha512-gqsNJblQ5wgYZJzhbkI7iBbg5Ddn9/EyfiCOwYdB9lHe07yDYco7H/vUH/TxTFTurEHtyV8LKb5KMEhJIKVhpw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@@ -36,7 +36,7 @@
"@jitsi/js-utils": "2.0.4",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.3.0",
"@jitsi/rtcstats": "9.4.0",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",
"@mui/material": "5.10.2",
@@ -79,7 +79,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1528.0.0+23644901/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1530.0.0+f2af389e/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -199,7 +199,7 @@
"tsc:web": "tsc --noEmit --project tsconfig.web.json",
"tsc:native": "tsc --noEmit --project tsconfig.native.json",
"tsc:ci": "npm run tsc:web && npm run tsc:native",
"lint:ci": "eslint --ext .js,.ts,.tsx --max-warnings 0 . && npm run tsc:web",
"lint:ci": "eslint --ext .js,.ts,.tsx --max-warnings 0 . && npm run tsc:ci",
"lang-sort": "./resources/lang-sort.sh",
"lint-fix": "eslint --ext .js,.ts,.tsx --max-warnings 0 --fix .",
"postinstall": "patch-package --error-on-fail && jetify",

View File

@@ -3,8 +3,8 @@ import { API_ID } from '../../../modules/API/constants';
import { getName as getAppName } from '../app/functions';
import { IStore } from '../app/types';
import { getAnalyticsRoomName } from '../base/conference/functions';
import checkChromeExtensionsInstalled from '../base/environment/checkChromeExtensionsInstalled';
import {
checkChromeExtensionsInstalled,
isMobileBrowser
} from '../base/environment/utils';
import JitsiMeetJS, {

View File

@@ -1,3 +1,5 @@
/* eslint-disable lines-around-comment */
import logger from '../logger';
import AbstractHandler, { IEvent } from './AbstractHandler';
@@ -63,7 +65,7 @@ export default class AmplitudeHandler extends AbstractHandler {
* @param {Object} userProps - The user portperties.
* @returns {void}
*/
setUserProperties(userProps: Object) {
setUserProperties(userProps: any) {
if (this._enabled) {
amplitude.getInstance().setUserProperties(userProps);
}
@@ -82,6 +84,7 @@ export default class AmplitudeHandler extends AbstractHandler {
return;
}
// @ts-ignore
amplitude.getInstance().logEvent(this._extractName(event) ?? '', event);
}
@@ -100,7 +103,9 @@ export default class AmplitudeHandler extends AbstractHandler {
return {
sessionId: amplitude.getInstance().getSessionId(),
// @ts-ignore
deviceId: amplitude.getInstance().options.deviceId,
// @ts-ignore
userId: amplitude.getInstance().options.userId
};
}

View File

@@ -162,14 +162,10 @@ export function appNavigate(uri?: string) {
* If we have a close page enabled, redirect to it without
* showing any other dialog.
*
* @param {Object} _options - Used to decide which particular close page to show
* or if close page is disabled, whether we should show the thankyou dialog.
* @param {boolean} options.showThankYou - Whether we should
* show thank you dialog.
* @param {boolean} options.feedbackSubmitted - Whether feedback was submitted.
* @param {Object} options - Ignored.
* @returns {Function}
*/
export function maybeRedirectToWelcomePage(_options: { feedbackSubmitted?: boolean; showThankYou?: boolean; } = {}) {
export function maybeRedirectToWelcomePage(options: any) { // eslint-disable-line @typescript-eslint/no-unused-vars
// Dummy.
}

View File

@@ -9,7 +9,7 @@ import { IAudioOnlyState } from '../base/audio-only/reducer';
import { IConferenceState } from '../base/conference/reducer';
import { IConfigState } from '../base/config/reducer';
import { IConnectionState } from '../base/connection/reducer';
import { IDevicesState } from '../base/devices/reducer';
import { IDevicesState } from '../base/devices/types';
import { IDialogState } from '../base/dialog/reducer';
import { IFlagsState } from '../base/flags/reducer';
import { IJwtState } from '../base/jwt/reducer';

View File

@@ -52,6 +52,7 @@ export interface IJitsiConference {
getLocalTracks: Function;
getMeetingUniqueId: Function;
getParticipantById: Function;
getParticipants: Function;
grantOwner: Function;
isAVModerationSupported: Function;
isCallstatsEnabled: Function;
@@ -68,6 +69,7 @@ export interface IJitsiConference {
on: Function;
removeTrack: Function;
replaceTrack: Function;
room: IJitsiConferenceRoom;
sendCommand: Function;
sendCommandOnce: Function;
sendEndpointMessage: Function;
@@ -111,6 +113,11 @@ export interface IConferenceState {
subject?: string;
}
export interface IJitsiConferenceRoom {
myroomjid: string;
roomjid: string;
}
/**
* Listen for actions that contain the conference object, so that it can be
* stored for use by other action creators.

View File

@@ -1,7 +1,7 @@
import { IStore } from '../../app/types';
import JitsiMeetJS from '../lib-jitsi-meet';
import { updateSettings } from '../settings/actions';
import { getUserSelectedOutputDeviceId } from '../settings/functions.any';
import { getUserSelectedOutputDeviceId } from '../settings/functions.web';
import {
ADD_PENDING_DEVICE_REQUEST,

View File

@@ -0,0 +1,19 @@
import { IReduxState } from '../../app/types';
/**
* Returns true if there are devices of a specific type or on native platform.
*
* @param {Object} state - The state of the application.
* @param {string} type - The type of device: VideoOutput | audioOutput | audioInput.
*
* @returns {boolean}
*/
export function hasAvailableDevices(state: IReduxState, type: string) {
if (state['features/base/devices'] === undefined) {
return true;
}
const availableDevices = state['features/base/devices'].availableDevices;
return Number(availableDevices[type as keyof typeof availableDevices]?.length) > 0;
}

View File

@@ -0,0 +1 @@
export * from './functions.any';

View File

@@ -5,10 +5,9 @@ import { ISettingsState } from '../settings/reducer';
import { parseURLParams } from '../util/parseURLParams';
import logger from './logger';
import { IDevicesState } from './reducer';
import { IDevicesState } from './types';
declare const APP: any;
export * from './functions.any';
const webrtcKindToJitsiKindTranslator = {
audioinput: 'audioInput',
@@ -240,24 +239,6 @@ export function getVideoDeviceIds(state: IReduxState) {
return state['features/base/devices'].availableDevices.videoInput?.map(({ deviceId }) => deviceId);
}
/**
* Returns true if there are devices of a specific type or on native platform.
*
* @param {Object} state - The state of the application.
* @param {string} type - The type of device: VideoOutput | audioOutput | audioInput.
*
* @returns {boolean}
*/
export function hasAvailableDevices(state: IReduxState, type: string) {
if (state['features/base/devices'] === undefined) {
return true;
}
const availableDevices = state['features/base/devices'].availableDevices;
return Number(availableDevices[type as keyof typeof availableDevices]?.length) > 0;
}
/**
* Set device id of the audio output device which is currently in use.
* Empty string stands for default device.

View File

@@ -1,3 +0,0 @@
export * from './actions';
export * from './actionTypes';
export * from './functions';

View File

@@ -35,7 +35,7 @@ import {
setAudioOutputDeviceId
} from './functions';
import logger from './logger';
import { IDevicesState } from './reducer';
import { IDevicesState } from './types';
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
microphone: {

View File

@@ -8,8 +8,9 @@ import {
SET_VIDEO_INPUT_DEVICE,
UPDATE_DEVICE_LIST
} from './actionTypes';
import { groupDevicesByKind } from './functions';
import { groupDevicesByKind } from './functions.web';
import logger from './logger';
import { IDevicesState } from './types';
const DEFAULT_STATE: IDevicesState = {
@@ -25,19 +26,6 @@ const DEFAULT_STATE: IDevicesState = {
}
};
export interface IDevicesState {
availableDevices: {
audioInput?: MediaDeviceInfo[];
audioOutput?: MediaDeviceInfo[];
videoInput?: MediaDeviceInfo[];
};
pendingRequests: Object[];
permissions: {
audio: boolean;
video: boolean;
};
}
/**
* Listen for actions which changes the state of known and used devices.
*

View File

@@ -0,0 +1,17 @@
/* eslint-disable lines-around-comment */
export interface IDevicesState {
availableDevices: {
// @ts-ignore
audioInput?: MediaDeviceInfo[];
// @ts-ignore
audioOutput?: MediaDeviceInfo[];
// @ts-ignore
videoInput?: MediaDeviceInfo[];
};
pendingRequests: any[];
permissions: {
audio: boolean;
video: boolean;
};
}

View File

@@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
* Checks whether the chrome extensions defined in the config file are installed or not.
*
* @param {Object} config - Objects containing info about the configured extensions.
*
* @returns {Promise[]}
*/
export default function checkChromeExtensionsInstalled(config: any = {}) {
return Promise.resolve([]);
}

View File

@@ -0,0 +1,26 @@
/**
* Checks whether the chrome extensions defined in the config file are installed or not.
*
* @param {Object} config - Objects containing info about the configured extensions.
*
* @returns {Promise[]}
*/
export default function checkChromeExtensionsInstalled(config: any = {}) {
const isExtensionInstalled = (info: any) => new Promise(resolve => {
const img = new Image();
img.src = `chrome-extension://${info.id}/${info.path}`;
img.setAttribute('aria-hidden', 'true');
img.onload = function() {
resolve(true);
};
img.onerror = function() {
resolve(false);
};
});
const extensionInstalledFunction = (info: any) => isExtensionInstalled(info);
return Promise.all(
(config.chromeExtensionsInfo || []).map((info: any) => extensionInstalledFunction(info))
);
}

View File

@@ -18,30 +18,3 @@ export function isMobileBrowser() {
export function isIosMobileBrowser() {
return Platform.OS === 'ios';
}
/**
* Checks whether the chrome extensions defined in the config file are installed or not.
*
* @param {Object} config - Objects containing info about the configured extensions.
*
* @returns {Promise[]}
*/
export function checkChromeExtensionsInstalled(config: any = {}) {
const isExtensionInstalled = (info: any) => new Promise(resolve => {
const img = new Image();
img.src = `chrome-extension://${info.id}/${info.path}`;
img.setAttribute('aria-hidden', 'true');
img.onload = function() {
resolve(true);
};
img.onerror = function() {
resolve(false);
};
});
const extensionInstalledFunction = (info: any) => isExtensionInstalled(info);
return Promise.all(
(config.chromeExtensionsInfo || []).map((info: any) => extensionInstalledFunction(info))
);
}

View File

@@ -16,7 +16,7 @@ import { MEET_FEATURES } from './constants';
* @returns {string} The JSON Web Token (JWT), if any, defined by the specified
* {@code url}; otherwise, {@code undefined}.
*/
export function parseJWTFromURLParams(url: URL | Location = window.location) {
export function parseJWTFromURLParams(url: URL | typeof window.location = window.location) {
// @ts-ignore
return parseURLParams(url, true, 'search').jwt;
}

View File

@@ -51,5 +51,9 @@ export interface ILocalParticipant extends IParticipant {
}
export interface IJitsiParticipant {
getDisplayName: () => string;
getId: () => string;
getJid: () => string;
getRole: () => string;
isHidden: () => boolean;
}

View File

@@ -103,160 +103,6 @@ export function getServerURL(stateful: IStateful) {
return state['features/base/settings'].serverURL || DEFAULT_SERVER_URL;
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred cameraDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedCameraDeviceId(stateful: IStateful) {
const state = toState(stateful);
const {
userSelectedCameraDeviceId,
userSelectedCameraDeviceLabel
} = state['features/base/settings'];
const { videoInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: videoInput,
// Operating systems may append " #{number}" somewhere in the label so
// find and strip that bit.
matchRegex: /\s#\d*(?!.*\s#\d*)/,
userSelectedDeviceId: userSelectedCameraDeviceId,
userSelectedDeviceLabel: userSelectedCameraDeviceLabel,
replacement: ''
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred micDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedMicDeviceId(stateful: IStateful) {
const state = toState(stateful);
const {
userSelectedMicDeviceId,
userSelectedMicDeviceLabel
} = state['features/base/settings'];
const { audioInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioInput,
// Operating systems may append " ({number}-" somewhere in the label so
// find and strip that bit.
matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/,
userSelectedDeviceId: userSelectedMicDeviceId,
userSelectedDeviceLabel: userSelectedMicDeviceLabel,
replacement: ' ('
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred audioOutputDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedOutputDeviceId(stateful: IStateful) {
const state = toState(stateful);
const {
userSelectedAudioOutputDeviceId,
userSelectedAudioOutputDeviceLabel
} = state['features/base/settings'];
const { audioOutput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioOutput,
matchRegex: undefined,
userSelectedDeviceId: userSelectedAudioOutputDeviceId,
userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel,
replacement: undefined
});
}
/**
* A helper function to abstract the logic for choosing which device ID to
* use. Falls back to fuzzy matching on label if a device ID match is not found.
*
* @param {Object} options - The arguments used to match find the preferred
* device ID from available devices.
* @param {Array<string>} options.availableDevices - The array of currently
* available devices to match against.
* @param {Object} options.matchRegex - The regex to use to find strings
* appended to the label by the operating system. The matches will be replaced
* with options.replacement, with the intent of matching the same device that
* might have a modified label.
* @param {string} options.userSelectedDeviceId - The device ID the participant
* prefers to use.
* @param {string} options.userSelectedDeviceLabel - The label associated with the
* device ID the participant prefers to use.
* @param {string} options.replacement - The string to use with
* options.matchRegex to remove identifies added to the label by the operating
* system.
* @private
* @returns {string} The preferred device ID to use for media.
*/
function _getUserSelectedDeviceId(options: {
availableDevices: MediaDeviceInfo[] | undefined;
matchRegex?: RegExp;
replacement?: string;
userSelectedDeviceId?: string;
userSelectedDeviceLabel?: string;
}) {
const {
availableDevices,
matchRegex = '',
userSelectedDeviceId,
userSelectedDeviceLabel,
replacement = ''
} = options;
// If there is no label at all, there is no need to fall back to checking
// the label for a fuzzy match.
if (!userSelectedDeviceLabel || !userSelectedDeviceId) {
return userSelectedDeviceId;
}
const foundMatchingBasedonDeviceId = availableDevices?.find(
candidate => candidate.deviceId === userSelectedDeviceId);
// Prioritize matching the deviceId
if (foundMatchingBasedonDeviceId) {
return userSelectedDeviceId;
}
const strippedDeviceLabel
= matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement)
: userSelectedDeviceLabel;
const foundMatchBasedOnLabel = availableDevices?.find(candidate => {
const { label } = candidate;
if (!label) {
return false;
} else if (strippedDeviceLabel === label) {
return true;
}
const strippedCandidateLabel
= label.replace(matchRegex, replacement);
return strippedDeviceLabel === strippedCandidateLabel;
});
return foundMatchBasedOnLabel
? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId;
}
/**
* Should we hide the helper dialog when a user tries to do audio only screen sharing.
*

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { IReduxState } from '../../app/types';
import { IStateful } from '../app/types';
import { toState } from '../redux/functions';
export * from './functions.any';
@@ -58,3 +59,157 @@ function getDeviceIdByType(state: IReduxState, isType: string) {
export function getDisplayName(state: IReduxState): string {
return state['features/base/settings'].displayName || '';
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred cameraDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedCameraDeviceId(stateful: IStateful) {
const state = toState(stateful);
const {
userSelectedCameraDeviceId,
userSelectedCameraDeviceLabel
} = state['features/base/settings'];
const { videoInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: videoInput,
// Operating systems may append " #{number}" somewhere in the label so
// find and strip that bit.
matchRegex: /\s#\d*(?!.*\s#\d*)/,
userSelectedDeviceId: userSelectedCameraDeviceId,
userSelectedDeviceLabel: userSelectedCameraDeviceLabel,
replacement: ''
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred micDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedMicDeviceId(stateful: IStateful) {
const state = toState(stateful);
const {
userSelectedMicDeviceId,
userSelectedMicDeviceLabel
} = state['features/base/settings'];
const { audioInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioInput,
// Operating systems may append " ({number}-" somewhere in the label so
// find and strip that bit.
matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/,
userSelectedDeviceId: userSelectedMicDeviceId,
userSelectedDeviceLabel: userSelectedMicDeviceLabel,
replacement: ' ('
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred audioOutputDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedOutputDeviceId(stateful: IStateful) {
const state = toState(stateful);
const {
userSelectedAudioOutputDeviceId,
userSelectedAudioOutputDeviceLabel
} = state['features/base/settings'];
const { audioOutput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioOutput,
matchRegex: undefined,
userSelectedDeviceId: userSelectedAudioOutputDeviceId,
userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel,
replacement: undefined
});
}
/**
* A helper function to abstract the logic for choosing which device ID to
* use. Falls back to fuzzy matching on label if a device ID match is not found.
*
* @param {Object} options - The arguments used to match find the preferred
* device ID from available devices.
* @param {Array<string>} options.availableDevices - The array of currently
* available devices to match against.
* @param {Object} options.matchRegex - The regex to use to find strings
* appended to the label by the operating system. The matches will be replaced
* with options.replacement, with the intent of matching the same device that
* might have a modified label.
* @param {string} options.userSelectedDeviceId - The device ID the participant
* prefers to use.
* @param {string} options.userSelectedDeviceLabel - The label associated with the
* device ID the participant prefers to use.
* @param {string} options.replacement - The string to use with
* options.matchRegex to remove identifies added to the label by the operating
* system.
* @private
* @returns {string} The preferred device ID to use for media.
*/
function _getUserSelectedDeviceId(options: {
availableDevices: MediaDeviceInfo[] | undefined;
matchRegex?: RegExp;
replacement?: string;
userSelectedDeviceId?: string;
userSelectedDeviceLabel?: string;
}) {
const {
availableDevices,
matchRegex = '',
userSelectedDeviceId,
userSelectedDeviceLabel,
replacement = ''
} = options;
// If there is no label at all, there is no need to fall back to checking
// the label for a fuzzy match.
if (!userSelectedDeviceLabel || !userSelectedDeviceId) {
return userSelectedDeviceId;
}
const foundMatchingBasedonDeviceId = availableDevices?.find(
candidate => candidate.deviceId === userSelectedDeviceId);
// Prioritize matching the deviceId
if (foundMatchingBasedonDeviceId) {
return userSelectedDeviceId;
}
const strippedDeviceLabel
= matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement)
: userSelectedDeviceLabel;
const foundMatchBasedOnLabel = availableDevices?.find(candidate => {
const { label } = candidate;
if (!label) {
return false;
} else if (strippedDeviceLabel === label) {
return true;
}
const strippedCandidateLabel
= label.replace(matchRegex, replacement);
return strippedDeviceLabel === strippedCandidateLabel;
});
return foundMatchBasedOnLabel
? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId;
}

View File

@@ -1,4 +1,5 @@
/* eslint-disable lines-around-comment */
import { IReduxState, IStore } from '../../app/types';
// @ts-ignore
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
@@ -11,6 +12,8 @@ import { getLocalVideoTrack, isLocalVideoTrackDesktop } from './functions';
export * from './actions.any';
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
* Signals that the local participant is ending screensharing or beginning the screensharing flow.
*
@@ -37,6 +40,8 @@ export function toggleScreensharing(enabled: boolean, _ignore1?: boolean, _ignor
};
}
/* eslint-enable @typescript-eslint/no-unused-vars */
/**
* Creates desktop track and replaces the local one.
*

View File

@@ -1,29 +1,18 @@
import { IReduxState, IStore } from '../../app/types';
import { IStateful } from '../app/types';
import { IReduxState } from '../../app/types';
import {
getMultipleVideoSendingSupportFeatureFlag,
getMultipleVideoSupportFeatureFlag
} from '../config/functions.any';
import { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { setAudioMuted } from '../media/actions';
import { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import {
getVirtualScreenshareParticipantOwnerId,
isScreenShareParticipant
} from '../participants/functions';
import { IParticipant } from '../participants/types';
import { toState } from '../redux/functions';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
} from '../settings/functions.any';
// @ts-ignore
import loadEffects from './loadEffects';
import logger from './logger';
import { ITrack } from './reducer';
import { ITrackOptions } from './types';
import { ITrack } from './types';
/**
* Returns root tracks state.
@@ -79,223 +68,6 @@ export function isParticipantVideoMuted(participant: IParticipant, state: IRedux
return isParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO, state);
}
/**
* Creates a local video track for presenter. The constraints are computed based
* on the height of the desktop that is being shared.
*
* @param {Object} options - The options with which the local presenter track
* is to be created.
* @param {string|null} [options.cameraDeviceId] - Camera device id or
* {@code undefined} to use app's settings.
* @param {number} desktopHeight - The height of the desktop that is being
* shared.
* @returns {Promise<JitsiLocalTrack>}
*/
export async function createLocalPresenterTrack(options: ITrackOptions, desktopHeight: number) {
const { cameraDeviceId } = options;
// compute the constraints of the camera track based on the resolution
// of the desktop screen that is being shared.
const cameraHeights = [ 180, 270, 360, 540, 720 ];
const proportion = 5;
const result = cameraHeights.find(
height => (desktopHeight / proportion) < height);
const constraints = {
video: {
aspectRatio: 4 / 3,
height: {
ideal: result
}
}
};
const [ videoTrack ] = await JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
constraints,
devices: [ 'video' ]
});
videoTrack.type = MEDIA_TYPE.PRESENTER;
return videoTrack;
}
/**
* Create local tracks of specific types.
*
* @param {Object} options - The options with which the local tracks are to be
* created.
* @param {string|null} [options.cameraDeviceId] - Camera device id or
* {@code undefined} to use app's settings.
* @param {string[]} options.devices - Required track types such as 'audio'
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event.
* @param {IStore} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}
*/
export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore) {
let { cameraDeviceId, micDeviceId } = options;
const {
desktopSharingSourceDevice,
desktopSharingSources,
firePermissionPromptIsShownEvent,
timeout
} = options;
if (typeof APP !== 'undefined') {
// TODO The app's settings should go in the redux store and then the
// reliance on the global variable APP will go away.
if (!store) {
store = APP.store; // eslint-disable-line no-param-reassign
}
const state = store.getState();
if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
cameraDeviceId = getUserSelectedCameraDeviceId(state);
}
if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
micDeviceId = getUserSelectedMicDeviceId(state);
}
}
// @ts-ignore
const state = store.getState();
const {
desktopSharingFrameRate,
firefox_fake_device, // eslint-disable-line camelcase
resolution
} = state['features/base/config'];
const constraints = options.constraints ?? state['features/base/config'].constraints;
return (
loadEffects(store).then((effectsArray: Object[]) => {
// Filter any undefined values returned by Promise.resolve().
const effects = effectsArray.filter(effect => Boolean(effect));
return JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
constraints,
desktopSharingFrameRate,
desktopSharingSourceDevice,
desktopSharingSources,
// Copy array to avoid mutations inside library.
devices: options.devices?.slice(0),
effects,
firefox_fake_device, // eslint-disable-line camelcase
firePermissionPromptIsShownEvent,
micDeviceId,
resolution,
timeout
})
.catch((err: Error) => {
logger.error('Failed to create local tracks', options.devices, err);
return Promise.reject(err);
});
}));
}
/**
* Returns an object containing a promise which resolves with the created tracks &
* the errors resulting from that process.
*
* @returns {Promise<JitsiLocalTrack>}
*
* @todo Refactor to not use APP.
*/
export function createPrejoinTracks() {
const errors: any = {};
const initialDevices = [ 'audio' ];
const requestedAudio = true;
let requestedVideo = false;
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
// Always get a handle on the audio input device so that we have statistics even if the user joins the
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
// only after that point.
if (startWithAudioMuted) {
APP.store.dispatch(setAudioMuted(true));
}
if (!startWithVideoMuted && !startAudioOnly) {
initialDevices.push('video');
requestedVideo = true;
}
let tryCreateLocalTracks;
if (!requestedAudio && !requestedVideo) {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
}, APP.store)
.catch((err: Error) => {
if (requestedAudio && requestedVideo) {
// Try audio only...
errors.audioAndVideoError = err;
return (
createLocalTracksF({
devices: [ 'audio' ],
firePermissionPromptIsShownEvent: true
}));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
return [];
} else if (requestedVideo && !requestedAudio) {
errors.videoOnlyError = err;
return [];
}
logger.error('Should never happen');
})
.catch((err: Error) => {
// Log this just in case...
if (!requestedAudio) {
logger.error('The impossible just happened', err);
}
errors.audioOnlyError = err;
// Try video only...
return requestedVideo
? createLocalTracksF({
devices: [ 'video' ],
firePermissionPromptIsShownEvent: true
})
: [];
})
.catch((err: Error) => {
// Log this just in case...
if (!requestedVideo) {
logger.error('The impossible just happened', err);
}
errors.videoOnlyError = err;
return [];
});
}
return {
tryCreateLocalTracks,
errors
};
}
/**
* Returns local audio track.
*
@@ -667,16 +439,3 @@ export function setTrackMuted(track: any, muted: boolean, state: IReduxState) {
}
});
}
/**
* Determines whether toggle camera should be enabled or not.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {boolean} - Whether toggle camera should be enabled.
*/
export function isToggleCameraEnabled(stateful: IStateful) {
const state = toState(stateful);
const { videoInput } = state['features/base/devices'].availableDevices;
return isMobileBrowser() && Number(videoInput?.length) > 1;
}

View File

@@ -0,0 +1,45 @@
import { IStore } from '../../app/types';
import JitsiMeetJS from '../lib-jitsi-meet';
import { ITrackOptions } from './types';
export * from './functions.any';
/**
* Create local tracks of specific types.
*
* @param {Object} options - The options with which the local tracks are to be
* created.
* @param {string|null} [options.cameraDeviceId] - Camera device id or
* {@code undefined} to use app's settings.
* @param {string[]} options.devices - Required track types such as 'audio'
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event.
* @param {IStore} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}
*/
export function createLocalTracksF(options: ITrackOptions = {}, store: IStore) {
const { cameraDeviceId, micDeviceId } = options;
const state = store.getState();
const {
resolution
} = state['features/base/config'];
const constraints = options.constraints ?? state['features/base/config'].constraints;
return JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
constraints,
// Copy array to avoid mutations inside library.
devices: options.devices?.slice(0),
micDeviceId,
resolution
});
}

View File

@@ -0,0 +1,242 @@
import { IStore } from '../../app/types';
import { IStateful } from '../app/types';
import { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS from '../lib-jitsi-meet';
import { setAudioMuted } from '../media/actions';
import { MEDIA_TYPE } from '../media/constants';
import { toState } from '../redux/functions';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
} from '../settings/functions.web';
// @ts-ignore
import loadEffects from './loadEffects';
import logger from './logger';
import { ITrackOptions } from './types';
export * from './functions.any';
/**
* Create local tracks of specific types.
*
* @param {Object} options - The options with which the local tracks are to be
* created.
* @param {string|null} [options.cameraDeviceId] - Camera device id or
* {@code undefined} to use app's settings.
* @param {string[]} options.devices - Required track types such as 'audio'
* and/or 'video'.
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event.
* @param {IStore} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}
*/
export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore) {
let { cameraDeviceId, micDeviceId } = options;
const {
desktopSharingSourceDevice,
desktopSharingSources,
firePermissionPromptIsShownEvent,
timeout
} = options;
// TODO The app's settings should go in the redux store and then the
// reliance on the global variable APP will go away.
store = store || APP.store; // eslint-disable-line no-param-reassign
const state = store.getState();
if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
cameraDeviceId = getUserSelectedCameraDeviceId(state);
}
if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
micDeviceId = getUserSelectedMicDeviceId(state);
}
const {
desktopSharingFrameRate,
firefox_fake_device, // eslint-disable-line camelcase
resolution
} = state['features/base/config'];
const constraints = options.constraints ?? state['features/base/config'].constraints;
return (
loadEffects(store).then((effectsArray: Object[]) => {
// Filter any undefined values returned by Promise.resolve().
const effects = effectsArray.filter(effect => Boolean(effect));
return JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
constraints,
desktopSharingFrameRate,
desktopSharingSourceDevice,
desktopSharingSources,
// Copy array to avoid mutations inside library.
devices: options.devices?.slice(0),
effects,
firefox_fake_device, // eslint-disable-line camelcase
firePermissionPromptIsShownEvent,
micDeviceId,
resolution,
timeout
})
.catch((err: Error) => {
logger.error('Failed to create local tracks', options.devices, err);
return Promise.reject(err);
});
}));
}
/**
* Creates a local video track for presenter. The constraints are computed based
* on the height of the desktop that is being shared.
*
* @param {Object} options - The options with which the local presenter track
* is to be created.
* @param {string|null} [options.cameraDeviceId] - Camera device id or
* {@code undefined} to use app's settings.
* @param {number} desktopHeight - The height of the desktop that is being
* shared.
* @returns {Promise<JitsiLocalTrack>}
*/
export async function createLocalPresenterTrack(options: ITrackOptions, desktopHeight: number) {
const { cameraDeviceId } = options;
// compute the constraints of the camera track based on the resolution
// of the desktop screen that is being shared.
const cameraHeights = [ 180, 270, 360, 540, 720 ];
const proportion = 5;
const result = cameraHeights.find(
height => (desktopHeight / proportion) < height);
const constraints = {
video: {
aspectRatio: 4 / 3,
height: {
ideal: result
}
}
};
const [ videoTrack ] = await JitsiMeetJS.createLocalTracks(
{
cameraDeviceId,
constraints,
devices: [ 'video' ]
});
videoTrack.type = MEDIA_TYPE.PRESENTER;
return videoTrack;
}
/**
* Returns an object containing a promise which resolves with the created tracks &
* the errors resulting from that process.
*
* @returns {Promise<JitsiLocalTrack>}
*
* @todo Refactor to not use APP.
*/
export function createPrejoinTracks() {
const errors: any = {};
const initialDevices = [ 'audio' ];
const requestedAudio = true;
let requestedVideo = false;
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
// Always get a handle on the audio input device so that we have statistics even if the user joins the
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
// only after that point.
if (startWithAudioMuted) {
APP.store.dispatch(setAudioMuted(true));
}
if (!startWithVideoMuted && !startAudioOnly) {
initialDevices.push('video');
requestedVideo = true;
}
let tryCreateLocalTracks;
if (!requestedAudio && !requestedVideo) {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
}, APP.store)
.catch((err: Error) => {
if (requestedAudio && requestedVideo) {
// Try audio only...
errors.audioAndVideoError = err;
return (
createLocalTracksF({
devices: [ 'audio' ],
firePermissionPromptIsShownEvent: true
}));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
return [];
} else if (requestedVideo && !requestedAudio) {
errors.videoOnlyError = err;
return [];
}
logger.error('Should never happen');
})
.catch((err: Error) => {
// Log this just in case...
if (!requestedAudio) {
logger.error('The impossible just happened', err);
}
errors.audioOnlyError = err;
// Try video only...
return requestedVideo
? createLocalTracksF({
devices: [ 'video' ],
firePermissionPromptIsShownEvent: true
})
: [];
})
.catch((err: Error) => {
// Log this just in case...
if (!requestedVideo) {
logger.error('The impossible just happened', err);
}
errors.videoOnlyError = err;
return [];
});
}
return {
tryCreateLocalTracks,
errors
};
}
/**
* Determines whether toggle camera should be enabled or not.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {boolean} - Whether toggle camera should be enabled.
*/
export function isToggleCameraEnabled(stateful: IStateful) {
const state = toState(stateful);
const { videoInput } = state['features/base/devices'].availableDevices;
return isMobileBrowser() && Number(videoInput?.length) > 1;
}

View File

@@ -2,11 +2,9 @@ import { batch } from 'react-redux';
import { IStore } from '../../app/types';
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { hideNotification } from '../../notifications/actions';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any';
import { getAvailableDevices } from '../devices/actions';
import {
SET_AUDIO_MUTED,
SET_CAMERA_FACING_MODE,
@@ -14,43 +12,31 @@ import {
SET_VIDEO_MUTED,
TOGGLE_CAMERA_FACING_MODE
} from '../media/actionTypes';
import { setScreenshareMuted, toggleCameraFacingMode } from '../media/actions';
import { toggleCameraFacingMode } from '../media/actions';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
MediaType,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY,
VIDEO_TYPE
VIDEO_MUTISM_AUTHORITY
} from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import {
TRACK_ADDED,
TRACK_MUTE_UNMUTE_FAILED,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED,
TRACK_STOPPED,
TRACK_UPDATED
} from './actionTypes';
import {
createLocalTracksA,
destroyLocalTracks,
showNoDataFromSourceVideoError,
toggleScreensharing,
trackMuteUnmuteFailed,
trackNoDataFromSourceNotificationInfoChanged,
trackRemoved
} from './actions';
import {
getLocalTrack,
getTrackByJitsiTrack,
isUserInteractionRequiredForUnmute,
setTrackMuted
} from './functions';
import { ITrack } from './reducer';
import './subscriber';
/**
@@ -63,29 +49,6 @@ import './subscriber';
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case TRACK_ADDED: {
const { local } = action.track;
// The devices list needs to be refreshed when no initial video permissions
// were granted and a local video track is added by umuting the video.
if (local) {
store.dispatch(getAvailableDevices());
}
break;
}
case TRACK_NO_DATA_FROM_SOURCE: {
const result = next(action);
_handleNoDataFromSourceErrors(store, action);
return result;
}
case TRACK_REMOVED: {
_removeNoDataFromSourceNotification(store, action.track);
break;
}
case SET_AUDIO_MUTED:
if (!action.muted
&& isUserInteractionRequiredForUnmute(store.getState())) {
@@ -153,82 +116,6 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
}
case TRACK_MUTE_UNMUTE_FAILED: {
const { jitsiTrack } = action.track;
const muted = action.wasMuted;
const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO;
if (typeof APP !== 'undefined') {
if (isVideoTrack && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP
&& getMultipleVideoSendingSupportFeatureFlag(store.getState())) {
store.dispatch(setScreenshareMuted(!muted));
} else if (isVideoTrack) {
APP.conference.setVideoMuteStatus();
} else {
APP.conference.setAudioMuteStatus(!muted);
}
}
break;
}
case TRACK_STOPPED: {
const { jitsiTrack } = action.track;
if (typeof APP !== 'undefined'
&& getMultipleVideoSendingSupportFeatureFlag(store.getState())
&& jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
store.dispatch(toggleScreensharing(false));
}
break;
}
case TRACK_UPDATED: {
// TODO Remove the following calls to APP.UI once components interested
// in track mute changes are moved into React and/or redux.
if (typeof APP !== 'undefined') {
const result = next(action);
const state = store.getState();
if (isPrejoinPageVisible(state)) {
return result;
}
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
if (isVideoTrack) {
// Do not change the video mute state for local presenter tracks.
if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
APP.conference.mutePresenter(muted);
} else if (jitsiTrack.isLocal() && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) {
APP.conference.setVideoMuteStatus();
} else if (jitsiTrack.isLocal() && muted && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
!getMultipleVideoSendingSupportFeatureFlag(state)
&& store.dispatch(toggleScreensharing(false, false, true));
} else {
APP.UI.setVideoMuted(participantID);
}
} else if (jitsiTrack.isLocal()) {
APP.conference.setAudioMuteStatus(muted);
} else {
APP.UI.setAudioMuted(participantID, muted);
}
return result;
}
// Mobile.
const { jitsiTrack, local } = action.track;
if (local && jitsiTrack.isMuted()
&& jitsiTrack.type === MEDIA_TYPE.VIDEO && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
store.dispatch(toggleScreensharing(false));
}
break;
}
}
return next(action);
@@ -259,53 +146,6 @@ StateListenerRegistry.register(
}
});
/**
* Handles no data from source errors.
*
* @param {Store} store - The redux store in which the specified action is
* dispatched.
* @param {Action} action - The redux action dispatched in the specified store.
* @private
* @returns {void}
*/
function _handleNoDataFromSourceErrors(store: IStore, action: any) {
const { getState, dispatch } = store;
const track = getTrackByJitsiTrack(getState()['features/base/tracks'], action.track.jitsiTrack);
if (!track || !track.local) {
return;
}
const { jitsiTrack } = track;
if (track.mediaType === MEDIA_TYPE.AUDIO && track.isReceivingData) {
_removeNoDataFromSourceNotification(store, action.track);
}
if (track.mediaType === MEDIA_TYPE.VIDEO) {
const { noDataFromSourceNotificationInfo = {} } = track;
if (track.isReceivingData) {
if (noDataFromSourceNotificationInfo.timeout) {
clearTimeout(noDataFromSourceNotificationInfo.timeout);
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
}
// try to remove the notification if there is one.
_removeNoDataFromSourceNotification(store, action.track);
} else {
if (noDataFromSourceNotificationInfo.timeout) {
return;
}
const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(jitsiTrack)), 5000);
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, { timeout }));
}
}
}
/**
* Gets the local track associated with a specific {@code MEDIA_TYPE} in a
* specific redux store.
@@ -334,23 +174,6 @@ function _getLocalTrack(
includePending));
}
/**
* Removes the no data from source notification associated with the JitsiTrack if displayed.
*
* @param {Store} store - The redux store.
* @param {Track} track - The redux action dispatched in the specified store.
* @returns {void}
*/
function _removeNoDataFromSourceNotification({ getState, dispatch }: IStore, track: ITrack) {
const t = getTrackByJitsiTrack(getState()['features/base/tracks'], track.jitsiTrack);
const { jitsiTrack, noDataFromSourceNotificationInfo = {} } = t || {};
if (noDataFromSourceNotificationInfo?.uid) {
dispatch(hideNotification(noDataFromSourceNotificationInfo.uid));
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
}
}
/**
* Mutes or unmutes a local track with a specific media type.
*

View File

@@ -0,0 +1,38 @@
import {
MEDIA_TYPE,
VIDEO_TYPE
} from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import {
TRACK_UPDATED
} from './actionTypes';
import {
toggleScreensharing
} from './actions.native';
import './middleware.any';
/**
* Middleware that captures LIB_DID_DISPOSE and LIB_DID_INIT actions and,
* respectively, creates/destroys local media tracks. Also listens to
* media-related actions and performs corresponding operations with tracks.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case TRACK_UPDATED: {
const { jitsiTrack, local } = action.track;
if (local && jitsiTrack.isMuted()
&& jitsiTrack.type === MEDIA_TYPE.VIDEO && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
store.dispatch(toggleScreensharing(false));
}
break;
}
}
return next(action);
});

View File

@@ -0,0 +1,198 @@
import { IStore } from '../../app/types';
import { hideNotification } from '../../notifications/actions';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any';
import { getAvailableDevices } from '../devices/actions.web';
import { setScreenshareMuted } from '../media/actions';
import {
MEDIA_TYPE,
VIDEO_TYPE
} from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import {
TRACK_ADDED,
TRACK_MUTE_UNMUTE_FAILED,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED,
TRACK_STOPPED,
TRACK_UPDATED
} from './actionTypes';
import {
showNoDataFromSourceVideoError,
toggleScreensharing,
trackNoDataFromSourceNotificationInfoChanged
} from './actions.web';
import {
getTrackByJitsiTrack
} from './functions.web';
import { ITrack } from './types';
import './middleware.any';
/**
* Middleware that captures LIB_DID_DISPOSE and LIB_DID_INIT actions and,
* respectively, creates/destroys local media tracks. Also listens to
* media-related actions and performs corresponding operations with tracks.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case TRACK_ADDED: {
const { local } = action.track;
// The devices list needs to be refreshed when no initial video permissions
// were granted and a local video track is added by umuting the video.
if (local) {
store.dispatch(getAvailableDevices());
}
break;
}
case TRACK_NO_DATA_FROM_SOURCE: {
const result = next(action);
_handleNoDataFromSourceErrors(store, action);
return result;
}
case TRACK_REMOVED: {
_removeNoDataFromSourceNotification(store, action.track);
break;
}
case TRACK_MUTE_UNMUTE_FAILED: {
const { jitsiTrack } = action.track;
const muted = action.wasMuted;
const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO;
if (isVideoTrack && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP
&& getMultipleVideoSendingSupportFeatureFlag(store.getState())) {
store.dispatch(setScreenshareMuted(!muted));
} else if (isVideoTrack) {
APP.conference.setVideoMuteStatus();
} else {
APP.conference.setAudioMuteStatus(!muted);
}
break;
}
case TRACK_STOPPED: {
const { jitsiTrack } = action.track;
if (getMultipleVideoSendingSupportFeatureFlag(store.getState())
&& jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
store.dispatch(toggleScreensharing(false));
}
break;
}
case TRACK_UPDATED: {
// TODO Remove the following calls to APP.UI once components interested
// in track mute changes are moved into React and/or redux.
const result = next(action);
const state = store.getState();
if (isPrejoinPageVisible(state)) {
return result;
}
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
if (isVideoTrack) {
// Do not change the video mute state for local presenter tracks.
if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) {
APP.conference.mutePresenter(muted);
} else if (jitsiTrack.isLocal() && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) {
APP.conference.setVideoMuteStatus();
} else if (jitsiTrack.isLocal() && muted && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) {
!getMultipleVideoSendingSupportFeatureFlag(state)
&& store.dispatch(toggleScreensharing(false, false, true));
} else {
APP.UI.setVideoMuted(participantID);
}
} else if (jitsiTrack.isLocal()) {
APP.conference.setAudioMuteStatus(muted);
} else {
APP.UI.setAudioMuted(participantID, muted);
}
return result;
}
}
return next(action);
});
/**
* Handles no data from source errors.
*
* @param {Store} store - The redux store in which the specified action is
* dispatched.
* @param {Action} action - The redux action dispatched in the specified store.
* @private
* @returns {void}
*/
function _handleNoDataFromSourceErrors(store: IStore, action: any) {
const { getState, dispatch } = store;
const track = getTrackByJitsiTrack(getState()['features/base/tracks'], action.track.jitsiTrack);
if (!track || !track.local) {
return;
}
const { jitsiTrack } = track;
if (track.mediaType === MEDIA_TYPE.AUDIO && track.isReceivingData) {
_removeNoDataFromSourceNotification(store, action.track);
}
if (track.mediaType === MEDIA_TYPE.VIDEO) {
const { noDataFromSourceNotificationInfo = {} } = track;
if (track.isReceivingData) {
if (noDataFromSourceNotificationInfo.timeout) {
clearTimeout(noDataFromSourceNotificationInfo.timeout);
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
}
// try to remove the notification if there is one.
_removeNoDataFromSourceNotification(store, action.track);
} else {
if (noDataFromSourceNotificationInfo.timeout) {
return;
}
const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(jitsiTrack)), 5000);
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, { timeout }));
}
}
}
/**
* Removes the no data from source notification associated with the JitsiTrack if displayed.
*
* @param {Store} store - The redux store.
* @param {Track} track - The redux action dispatched in the specified store.
* @returns {void}
*/
function _removeNoDataFromSourceNotification({ getState, dispatch }: IStore, track: ITrack) {
const t = getTrackByJitsiTrack(getState()['features/base/tracks'], track.jitsiTrack);
const { jitsiTrack, noDataFromSourceNotificationInfo = {} } = t || {};
if (noDataFromSourceNotificationInfo?.uid) {
dispatch(hideNotification(noDataFromSourceNotificationInfo.uid));
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
}
}

View File

@@ -1,4 +1,3 @@
import { MediaType } from '../media/constants';
import { PARTICIPANT_ID_CHANGED } from '../participants/actionTypes';
import ReducerRegistry from '../redux/ReducerRegistry';
import { set } from '../redux/functions';
@@ -14,48 +13,7 @@ import {
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
TRACK_WILL_CREATE
} from './actionTypes';
export interface ITrack {
isReceivingData: boolean;
jitsiTrack: any;
lastMediaEvent?: string;
local: boolean;
mediaType: MediaType;
mirror: boolean;
muted: boolean;
noDataFromSourceNotificationInfo?: {
timeout?: number;
uid?: string;
};
participantId: string;
streamingStatus?: string;
videoStarted: boolean;
videoType?: string | null;
}
/**
* Track type.
*
* @typedef {object} Track
* @property {JitsiLocalTrack|JitsiRemoteTrack} jitsiTrack - The associated
* {@code JitsiTrack} instance. Optional for local tracks if those are still
* being created (ie {@code getUserMedia} is still in progress).
* @property {Promise} [gumProcess] - If a local track is still being created,
* it will have no {@code JitsiTrack}, but a {@code gumProcess} set to a
* {@code Promise} with and extra {@code cancel()}.
* @property {boolean} local=false - If the track is local.
* @property {MEDIA_TYPE} mediaType=false - The media type of the track.
* @property {boolean} mirror=false - The indicator which determines whether the
* display/rendering of the track should be mirrored. It only makes sense in the
* context of video (at least at the time of this writing).
* @property {boolean} muted=false - If the track is muted.
* @property {(string|undefined)} participantId - The ID of the participant whom
* the track belongs to.
* @property {boolean} videoStarted=false - If the video track has already
* started to play.
* @property {(VIDEO_TYPE|undefined)} videoType - The type of video track if
* any.
*/
import { ITrack } from './types';
/**
* Reducer function for a single track.

View File

@@ -1,3 +1,5 @@
import { MediaType } from '../media/constants';
export interface ITrackOptions {
cameraDeviceId?: string | null;
constraints?: {
@@ -18,6 +20,47 @@ export interface ITrackOptions {
timeout?: number;
}
/**
* Track type.
*
* @typedef {object} Track
* @property {JitsiLocalTrack|JitsiRemoteTrack} jitsiTrack - The associated
* {@code JitsiTrack} instance. Optional for local tracks if those are still
* being created (ie {@code getUserMedia} is still in progress).
* @property {Promise} [gumProcess] - If a local track is still being created,
* it will have no {@code JitsiTrack}, but a {@code gumProcess} set to a
* {@code Promise} with and extra {@code cancel()}.
* @property {boolean} local=false - If the track is local.
* @property {MEDIA_TYPE} mediaType=false - The media type of the track.
* @property {boolean} mirror=false - The indicator which determines whether the
* display/rendering of the track should be mirrored. It only makes sense in the
* context of video (at least at the time of this writing).
* @property {boolean} muted=false - If the track is muted.
* @property {(string|undefined)} participantId - The ID of the participant whom
* the track belongs to.
* @property {boolean} videoStarted=false - If the video track has already
* started to play.
* @property {(VIDEO_TYPE|undefined)} videoType - The type of video track if
* any.
*/
export interface ITrack {
isReceivingData: boolean;
jitsiTrack: any;
lastMediaEvent?: string;
local: boolean;
mediaType: MediaType;
mirror: boolean;
muted: boolean;
noDataFromSourceNotificationInfo?: {
timeout?: number;
uid?: string;
};
participantId: string;
streamingStatus?: string;
videoStarted: boolean;
videoType?: string | null;
}
export interface IToggleScreenSharingOptions {
audioOnly: boolean;
enabled?: boolean;

View File

@@ -1 +0,0 @@
export * from './native';

View File

@@ -1 +0,0 @@
export * from './web';

View File

@@ -1,7 +1,9 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { TouchableRipple } from 'react-native-paper';
import Icon from '../../../icons/components/Icon';
// @ts-ignore
import styles from '../../../react/components/native/styles';
import { IIconButtonProps } from '../../../react/types';
import { BUTTON_TYPES } from '../../constants';

View File

@@ -7,6 +7,8 @@ import { ISwitchProps } from '../types';
interface IProps extends ISwitchProps {
className?: string;
/**
* Id of the toggle.
*/
@@ -78,7 +80,7 @@ const useStyles = makeStyles()((theme: Theme) => {
};
});
const Switch = ({ id, checked, disabled, onChange }: IProps) => {
const Switch = ({ className, id, checked, disabled, onChange }: IProps) => {
const { classes: styles, cx } = useStyles();
const isMobile = isMobileBrowser();
@@ -89,7 +91,7 @@ const Switch = ({ id, checked, disabled, onChange }: IProps) => {
return (
<label
className = { cx('toggle-container', styles.container, checked && styles.containerOn,
isMobile && 'is-mobile', disabled && 'disabled') }>
isMobile && 'is-mobile', disabled && 'disabled', className) }>
<input
type = 'checkbox'
{ ...(id ? { id } : {}) }

View File

@@ -1 +0,0 @@
export { default as Button } from './Button';

View File

@@ -63,7 +63,7 @@ export function escapeRegexp(s: string) {
* @param {Object} w - Window object to use instead of the built in one.
* @returns {string}
*/
export function getBaseUrl(w: Window = window) {
export function getBaseUrl(w: typeof window = window) {
const doc = w.document;
const base = doc.querySelector('base');

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Linking } from 'react-native';
import logger from './logger';

View File

@@ -2,11 +2,18 @@ import _ from 'lodash';
import { IStateful } from '../base/app/types';
import { getCurrentConference } from '../base/conference/functions';
import { getParticipantById, getParticipantCount, isLocalParticipantModerator } from '../base/participants/functions';
import { IJitsiConference } from '../base/conference/reducer';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount,
isLocalParticipantModerator
} from '../base/participants/functions';
import { IJitsiParticipant } from '../base/participants/types';
import { toState } from '../base/redux/functions';
import { FEATURE_KEY } from './constants';
import { IRoom, IRooms } from './types';
import { IRoom, IRoomInfo, IRoomInfoParticipant, IRooms, IRoomsInfo } from './types';
/**
* Returns the rooms object for breakout rooms.
@@ -30,9 +37,16 @@ export const getMainRoom = (stateful: IStateful) => {
return _.find(rooms, room => Boolean(room.isMainRoom));
};
/**
* Returns the rooms info.
*
* @param {IStateful} stateful - The redux store, the redux.
* @returns {IRoomsInfo} The rooms info.
*/
export const getRoomsInfo = (stateful: IStateful) => {
const breakoutRooms = getBreakoutRooms(stateful);
const conference = getCurrentConference(stateful);
const conference: IJitsiConference = getCurrentConference(stateful);
const initialRoomsInfo = {
rooms: []
@@ -40,27 +54,45 @@ export const getRoomsInfo = (stateful: IStateful) => {
// only main roomn
if (!breakoutRooms || Object.keys(breakoutRooms).length === 0) {
// filter out hidden participants
const conferenceParticipants = conference?.getParticipants()
.filter((participant: IJitsiParticipant) => !participant.isHidden());
const localParticipant = getLocalParticipant(stateful);
let localParticipantInfo;
if (localParticipant) {
localParticipantInfo = {
role: localParticipant.role,
displayName: localParticipant.name,
avatarUrl: localParticipant.loadableAvatarUrl,
id: localParticipant.id
};
}
return {
...initialRoomsInfo,
rooms: [ {
isMainRoom: true,
id: conference?.room?.roomjid,
jid: conference?.room?.myroomjid,
participants: conference?.participants && Object.keys(conference.participants).length
? Object.keys(conference.participants).map(participantId => {
const participantItem = conference?.participants[participantId];
const storeParticipant = getParticipantById(stateful, participantItem._id);
participants: conferenceParticipants?.length > 0
? [
localParticipantInfo,
...conferenceParticipants.map((participantItem: IJitsiParticipant) => {
const storeParticipant = getParticipantById(stateful, participantItem.getId());
return {
jid: participantItem._jid,
role: participantItem._role,
displayName: participantItem._displayName,
avatarUrl: storeParticipant?.loadableAvatarUrl,
id: participantItem._id
};
}) : []
} ]
};
return {
jid: participantItem.getJid(),
role: participantItem.getRole(),
displayName: participantItem.getDisplayName(),
avatarUrl: storeParticipant?.loadableAvatarUrl,
id: participantItem.getId()
} as IRoomInfoParticipant;
}) ]
: [ localParticipantInfo ]
} as IRoomInfo ]
} as IRoomsInfo;
}
return {
@@ -86,11 +118,11 @@ export const getRoomsInfo = (stateful: IStateful) => {
avatarUrl: storeParticipant?.loadableAvatarUrl,
id: storeParticipant ? storeParticipant.id
: participantLongId
};
} as IRoomInfoParticipant;
}) : []
};
} as IRoomInfo;
})
};
} as IRoomsInfo;
};
/**

View File

@@ -15,3 +15,22 @@ export interface IRoom {
export interface IRooms {
[jid: string]: IRoom;
}
export interface IRoomInfo {
id: string;
isMainRoom: boolean;
jid: string;
participants: IRoomInfoParticipant[];
}
export interface IRoomsInfo {
rooms: IRoomInfo[];
}
export interface IRoomInfoParticipant {
avatarUrl: string;
displayName: string;
id: string;
jid: string;
role: string;
}

View File

@@ -5,7 +5,7 @@ import { IStore } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { connect } from '../../../base/redux/functions';
import { updateSettings } from '../../../base/settings/actions';
import { Button } from '../../../base/ui/components/web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
// @ts-ignore

View File

@@ -8,8 +8,8 @@ import {
sendAnalytics
} from '../../analytics';
import { getCurrentConference } from '../../base/conference/functions';
import checkChromeExtensionsInstalled from '../../base/environment/checkChromeExtensionsInstalled';
import {
checkChromeExtensionsInstalled,
isMobileBrowser
} from '../../base/environment/utils';
import { translate } from '../../base/i18n';

View File

@@ -1,6 +1,6 @@
import { JitsiParticipantConnectionStatus, JitsiTrackStreamingStatus } from '../base/lib-jitsi-meet';
import { IParticipant } from '../base/participants/types';
import { ITrack } from '../base/tracks/reducer';
import { ITrack } from '../base/tracks/types';
/**
* Checks if the passed track's streaming status is active.

View File

@@ -8,7 +8,7 @@ import { createDeepLinkingPageEvent, sendAnalytics } from '../../analytics';
import { isSupportedBrowser } from '../../base/environment';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { Button } from '../../base/ui/components/web';
import Button from '../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../base/ui/constants';
import {
openDesktopApp,

View File

@@ -6,13 +6,13 @@ import {
setAudioInputDeviceAndUpdateSettings,
setAudioOutputDevice,
setVideoInputDeviceAndUpdateSettings
} from '../base/devices/actions';
} from '../base/devices/actions.web';
import {
areDeviceLabelsInitialized,
getAudioOutputDeviceId,
getDeviceIdByLabel,
groupDevicesByKind
} from '../base/devices/functions';
} from '../base/devices/functions.web';
import { isIosMobileBrowser } from '../base/environment/utils';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
@@ -20,7 +20,7 @@ import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId,
getUserSelectedOutputDeviceId
} from '../base/settings/functions.any';
} from '../base/settings/functions.web';
/**
* Returns the properties for the device selection dialog from Redux state.

View File

@@ -8,7 +8,7 @@ import {
KICKED_OUT
} from '../base/conference';
import { SET_CONFIG } from '../base/config';
import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices';
import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices/actionTypes';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import {
DOMINANT_SPEAKER_CHANGED,

View File

@@ -1,4 +1,6 @@
export type DetectInput = {
// @ts-ignore
image: ImageBitmap | ImageData;
threshold: number;
};

View File

@@ -1,3 +1,4 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
@@ -7,10 +8,13 @@ import { IconHorizontalPoints } from '../../../base/icons/svg';
import Button from '../../../base/ui/components/native/Button';
import IconButton from '../../../base/ui/components/native/IconButton';
import { BUTTON_TYPES } from '../../../base/ui/constants';
// @ts-ignore
import MuteEveryoneDialog from '../../../video-menu/components/native/MuteEveryoneDialog';
import { isMoreActionsVisible, isMuteAllVisible } from '../../functions';
// @ts-ignore
import { ContextMenuMore } from './ContextMenuMore';
// @ts-ignore
import styles from './styles';

View File

@@ -8,7 +8,7 @@ import { makeStyles } from 'tss-react/mui';
import { translate } from '../../../../base/i18n/functions';
import Icon from '../../../../base/icons/components/Icon';
import { IconArrowLeft } from '../../../../base/icons/svg';
import { Button } from '../../../../base/ui/components/web';
import Button from '../../../../base/ui/components/web/Button';
// @ts-ignore
import { getCountryCodeFromPhone } from '../../../utils';
// @ts-ignore

View File

@@ -8,7 +8,7 @@ import { makeStyles } from 'tss-react/mui';
import { translate } from '../../../../base/i18n/functions';
import Icon from '../../../../base/icons/components/Icon';
import { IconClose } from '../../../../base/icons/svg';
import { Button } from '../../../../base/ui/components/web';
import Button from '../../../../base/ui/components/web/Button';
// @ts-ignore
import Label from '../Label';
// @ts-ignore

View File

@@ -113,7 +113,7 @@ export interface IProps extends WithTranslation {
/**
* Callback to change the local recording only self setting.
*/
onLocalRecordingSelfChange: Function;
onLocalRecordingSelfChange: () => void;
/**
* Callback to be invoked on sharing setting change.

View File

@@ -1,6 +1,5 @@
// XXX CSS is used on Web, JavaScript styles are use only for mobile. Export an
// (empty) object so that styles[*] statements on Web don't trigger errors.
import BaseTheme from '../../../base/ui/components/BaseTheme';
export default {};
@@ -13,5 +12,3 @@ export const ICON_CLOUD = 'images/icon-cloud.png';
export const ICON_INFO = 'images/icon-info.png';
export const ICON_USERS = 'images/icon-users.png';
export const TRACK_COLOR = BaseTheme.palette.ui15;

View File

@@ -6,12 +6,12 @@ import {
Container,
Image,
LoadingIndicator,
Switch,
Text
// @ts-ignore
} from '../../../../base/react';
import { connect } from '../../../../base/redux/functions';
import Button from '../../../../base/ui/components/web/Button';
import Switch from '../../../../base/ui/components/web/Switch';
import { BUTTON_TYPES } from '../../../../base/ui/constants';
import { RECORDING_TYPES } from '../../../constants';
// @ts-ignore
@@ -25,8 +25,7 @@ import {
ICON_CLOUD,
ICON_INFO,
ICON_USERS,
LOCAL_RECORDING,
TRACK_COLOR
LOCAL_RECORDING
// @ts-ignore
} from '../styles.web';
@@ -76,11 +75,10 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
= integrationsEnabled || _localRecordingAvailable
? (
<Switch
checked = { selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE }
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this._onRecordingServiceSwitchChange }
trackColor = {{ false: TRACK_COLOR }}
value = { selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
onChange = { this._onRecordingServiceSwitchChange } />
) : null;
const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
@@ -141,11 +139,10 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
{ t('recording.fileSharingdescription') }
</Text>
<Switch
checked = { sharingSetting }
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { onSharingSettingChanged }
trackColor = {{ false: TRACK_COLOR }}
value = { sharingSetting } />
onChange = { onSharingSettingChanged } />
</Container>
);
}
@@ -282,12 +279,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
if (fileRecordingsServiceEnabled || _localRecordingAvailable) {
switchContent = (
<Switch
checked = { selectedRecordingService
=== RECORDING_TYPES.DROPBOX }
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this._onDropboxSwitchChange }
trackColor = {{ false: TRACK_COLOR }}
value = { selectedRecordingService
=== RECORDING_TYPES.DROPBOX } />
onChange = { this._onDropboxSwitchChange } />
);
}
@@ -351,12 +347,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
{ t('recording.saveLocalRecording') }
</Text>
<Switch
checked = { selectedRecordingService
=== RECORDING_TYPES.LOCAL }
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this._onLocalRecordingSwitchChange }
trackColor = {{ false: TRACK_COLOR }}
value = { selectedRecordingService
=== RECORDING_TYPES.LOCAL } />
onChange = { this._onLocalRecordingSwitchChange } />
</Container>
</Container>
{selectedRecordingService === RECORDING_TYPES.LOCAL && (
@@ -373,11 +368,10 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent<IP
{t('recording.onlyRecordSelf')}
</Text>
<Switch
checked = { localRecordingOnlySelf }
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { onLocalRecordingSelfChange }
trackColor = {{ false: TRACK_COLOR }}
value = { localRecordingOnlySelf } />
onChange = { onLocalRecordingSelfChange } />
</Container>
</Container>
)}

View File

@@ -5,7 +5,7 @@ import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { Button } from '../../../base/ui/components/web';
import Button from '../../../base/ui/components/web/Button';
import {
CALENDAR_TYPE,
MicrosoftSignInButton,

View File

@@ -11,7 +11,7 @@ import { AbstractDialogTab } from '../../../base/dialog';
// @ts-ignore
import type { Props as AbstractDialogTabProps } from '../../../base/dialog';
import { translate } from '../../../base/i18n/functions';
import { Button } from '../../../base/ui/components/web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
// @ts-ignore
import { openLogoutDialog } from '../../actions';

View File

@@ -4,11 +4,13 @@ import React from 'react';
import { areAudioLevelsEnabled } from '../../../../base/config/functions';
import {
getAudioInputDeviceData,
getAudioOutputDeviceData,
setAudioInputDeviceAndUpdateSettings,
setAudioOutputDevice as setAudioOutputDeviceAction
} from '../../../../base/devices';
} from '../../../../base/devices/actions.web';
import {
getAudioInputDeviceData,
getAudioOutputDeviceData
} from '../../../../base/devices/functions.web';
import Popover from '../../../../base/popover/components/Popover.web';
import { connect } from '../../../../base/redux';
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';

View File

@@ -3,9 +3,11 @@
import React from 'react';
import {
getVideoDeviceIds,
setVideoInputDeviceAndUpdateSettings
} from '../../../../base/devices';
} from '../../../../base/devices/actions.web';
import {
getVideoDeviceIds
} from '../../../../base/devices/functions.web';
import Popover from '../../../../base/popover/components/Popover.web';
import { connect } from '../../../../base/redux';
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';

View File

@@ -5,7 +5,6 @@ import { isNameReadOnly } from '../base/config/functions';
import { SERVER_URL_CHANGE_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n/i18next';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import {
getLocalParticipant,
isLocalParticipantModerator
@@ -256,67 +255,6 @@ export function getSoundsTabProps(stateful: IStateful) {
};
}
/**
* Returns a promise which resolves with a list of objects containing
* all the video jitsiTracks and appropriate errors for the given device ids.
*
* @param {string[]} ids - The list of the camera ids for which to create tracks.
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
*
* @returns {Promise<Object[]>}
*/
export function createLocalVideoTracks(ids: string[], timeout?: number) {
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout)
.then((jitsiTrack: any) => {
return {
jitsiTrack,
deviceId
};
})
.catch(() => {
return {
jitsiTrack: null,
deviceId,
error: 'deviceSelection.previewUnavailable'
};
})));
}
/**
* Returns a promise which resolves with a list of objects containing
* the audio track and the corresponding audio device information.
*
* @param {Object[]} devices - A list of microphone devices.
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
* @returns {Promise<{
* deviceId: string,
* hasError: boolean,
* jitsiTrack: Object,
* label: string
* }[]>}
*/
export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) {
return Promise.all(
devices.map(async ({ deviceId, label }) => {
let jitsiTrack = null;
let hasError = false;
try {
jitsiTrack = await createLocalTrack('audio', deviceId, timeout);
} catch (err) {
hasError = true;
}
return {
deviceId,
hasError,
jitsiTrack,
label
};
}));
}
/**
* Returns the visibility state of the audio settings.
*

View File

@@ -0,0 +1 @@
export * from './functions.any';

View File

@@ -0,0 +1,64 @@
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
export * from './functions.any';
/**
* Returns a promise which resolves with a list of objects containing
* all the video jitsiTracks and appropriate errors for the given device ids.
*
* @param {string[]} ids - The list of the camera ids for which to create tracks.
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
*
* @returns {Promise<Object[]>}
*/
export function createLocalVideoTracks(ids: string[], timeout?: number) {
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout)
.then((jitsiTrack: any) => {
return {
jitsiTrack,
deviceId
};
})
.catch(() => {
return {
jitsiTrack: null,
deviceId,
error: 'deviceSelection.previewUnavailable'
};
})));
}
/**
* Returns a promise which resolves with a list of objects containing
* the audio track and the corresponding audio device information.
*
* @param {Object[]} devices - A list of microphone devices.
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
* @returns {Promise<{
* deviceId: string,
* hasError: boolean,
* jitsiTrack: Object,
* label: string
* }[]>}
*/
export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) {
return Promise.all(
devices.map(async ({ deviceId, label }) => {
let jitsiTrack = null;
let hasError = false;
try {
jitsiTrack = await createLocalTrack('audio', deviceId, timeout);
} catch (err) {
hasError = true;
}
return {
deviceId,
hasError,
jitsiTrack,
label
};
}));
}

View File

@@ -3,7 +3,7 @@ import debounce from 'lodash/debounce';
import { getMultipleVideoSupportFeatureFlag } from '../base/config/functions';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { equals } from '../base/redux/functions';
import { ITrack } from '../base/tracks/reducer';
import { ITrack } from '../base/tracks/types';
import { isFollowMeActive } from '../follow-me/functions';
import { setRemoteParticipantsWithScreenShare, virtualScreenshareParticipantsUpdated } from './actions.web';

View File

@@ -37,6 +37,7 @@ curl https://get.acme.sh | sh -s email=$EMAIL
NGINX_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx' 2>/dev/null | awk '{print $3}' || true)"
NGINX_FULL_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx-full' 2>/dev/null | awk '{print $3}' || true)"
NGINX_EXTRAS_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx-extras' 2>/dev/null | awk '{print $3}' || true)"
OPENRESTY_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'openresty' 2>/dev/null | awk '{print $3}' || true)"
APACHE_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'apache2' 2>/dev/null | awk '{print $3}' || true)"
RELOAD_CMD=""
@@ -44,6 +45,8 @@ if [ "$NGINX_INSTALL_CHECK" = "installed" ] || [ "$NGINX_INSTALL_CHECK" = "unpac
|| [ "$NGINX_FULL_INSTALL_CHECK" = "installed" ] || [ "$NGINX_FULL_INSTALL_CHECK" = "unpacked" ] \
|| [ "$NGINX_EXTRAS_INSTALL_CHECK" = "installed" ] || [ "$NGINX_EXTRAS_INSTALL_CHECK" = "unpacked" ]; then
RELOAD_CMD="systemctl force-reload nginx.service"
elif [ "$OPENRESTY_INSTALL_CHECK" = "installed" ] || [ "$OPENRESTY_INSTALL_CHECK" = "unpacked" ] ; then
RELOAD_CMD="systemctl force-reload openresty.service"
elif [ "$APACHE_INSTALL_CHECK" = "installed" ] || [ "$APACHE_INSTALL_CHECK" = "unpacked" ] ; then
RELOAD_CMD="systemctl force-reload apache2.service"
else

View File

@@ -24,6 +24,7 @@
"react/features/embed-meeting",
"react/features/face-landmarks",
"react/features/feedback",
"react/features/no-audio-signal",
"react/features/noise-suppression",
"react/features/screen-share",
"react/features/stream-effects/noise-suppression",