Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbf1927c70 | ||
|
|
86130c1478 | ||
|
|
15fb8a1525 | ||
|
|
64cbfb648f | ||
|
|
3e40bb19cd | ||
|
|
bbca0fc357 | ||
|
|
9bb789472e | ||
|
|
06fa175a6c | ||
|
|
5940f2890a | ||
|
|
7d09088186 | ||
|
|
34be081d87 | ||
|
|
3c36eece5e | ||
|
|
8eb0acada6 | ||
|
|
8307a8be2d | ||
|
|
d62190c644 | ||
|
|
58c4248da0 | ||
|
|
74af70a1d5 | ||
|
|
7b25b847ba | ||
|
|
18536cb14c | ||
|
|
01e36e1c56 | ||
|
|
5d96a226ed | ||
|
|
9855fac805 | ||
|
|
ed5351d250 | ||
|
|
b64260e554 | ||
|
|
7d67cb583e | ||
|
|
8e3c301d03 | ||
|
|
f9071b8b6b | ||
|
|
01abc4e8a8 | ||
|
|
c5bddda781 | ||
|
|
c2bc92ae05 | ||
|
|
a425e9c92e | ||
|
|
3bfa4744c8 | ||
|
|
bd5901d59c | ||
|
|
306c8ba8c2 | ||
|
|
5a6335207f | ||
|
|
97e8a6c3f3 | ||
|
|
523926d598 | ||
|
|
ab5627212d | ||
|
|
1b6c5a7141 | ||
|
|
a9767eda72 | ||
|
|
6c3a5793b4 | ||
|
|
c05b4a43e8 | ||
|
|
385669cbb8 | ||
|
|
5cfae2c419 | ||
|
|
92740707f0 | ||
|
|
775c531fe7 | ||
|
|
b8eda2b68d | ||
|
|
470c115e7c | ||
|
|
d9cf7aef3a | ||
|
|
c20c4bd5a3 | ||
|
|
74a5eb2d81 | ||
|
|
b25db3ce2e | ||
|
|
31d9fb12c8 | ||
|
|
ca9ca04d0f | ||
|
|
5a027f1585 | ||
|
|
5d86d202bd | ||
|
|
7ea7afebfb | ||
|
|
710307725b | ||
|
|
1cde7e63c7 | ||
|
|
a53d284bbe | ||
|
|
579e650a1d | ||
|
|
ee525ae569 | ||
|
|
63a411168e | ||
|
|
ad68a87dba | ||
|
|
45aafe5432 | ||
|
|
ac0f3979b0 | ||
|
|
d2c2919aef | ||
|
|
a18ed3a779 | ||
|
|
22871f15d0 | ||
|
|
91fb4665d6 | ||
|
|
f6c94fffc4 | ||
|
|
c8939a133d | ||
|
|
f810483ff6 | ||
|
|
c2cf09a2ca | ||
|
|
c73ba37202 | ||
|
|
659eb6b789 | ||
|
|
87821eb2c0 | ||
|
|
8291f4597c | ||
|
|
fddaf7c8a8 | ||
|
|
960ffa7e78 | ||
|
|
779ed6bb5e | ||
|
|
47984d3ec1 | ||
|
|
d9282f873c | ||
|
|
9344138303 | ||
|
|
6c3968a434 | ||
|
|
e8e70d9d27 | ||
|
|
0be68b59b7 | ||
|
|
d5ab3da9f2 | ||
|
|
5ef0f527f9 | ||
|
|
e60a14d5af | ||
|
|
03abb244df | ||
|
|
f9ea8b034f | ||
|
|
ee551ea0a0 | ||
|
|
0b22f8e449 | ||
|
|
e21f99d9d6 | ||
|
|
edb56200d5 | ||
|
|
04313a4aa3 | ||
|
|
cdb606e7ee | ||
|
|
28781965ed | ||
|
|
cbe396be55 | ||
|
|
a3befe336f | ||
|
|
63ea496bcd | ||
|
|
db6547f48d | ||
|
|
34e329a4f2 | ||
|
|
42b71fc4b9 | ||
|
|
dd99268461 | ||
|
|
692c6a47b4 | ||
|
|
b3983aa766 | ||
|
|
a79ae9b576 | ||
|
|
e69da98348 | ||
|
|
e01d891bba | ||
|
|
f904626f5e | ||
|
|
1333fd1975 | ||
|
|
af918f8dc5 | ||
|
|
b7baf8fe98 | ||
|
|
4ebab6f9c0 | ||
|
|
8f93acb464 | ||
|
|
9b60537e0f | ||
|
|
7b9abd34a0 | ||
|
|
ed82443ffa | ||
|
|
a3cd331369 | ||
|
|
9c77ab7f4d | ||
|
|
55983ff62a | ||
|
|
b4be1bcd05 | ||
|
|
2420a68be9 | ||
|
|
ebfc5a95ff | ||
|
|
68cad276bd | ||
|
|
e683d70a18 | ||
|
|
9645391180 | ||
|
|
851b1a76a9 | ||
|
|
4890390ea2 | ||
|
|
7828bf8d46 | ||
|
|
191da551e3 | ||
|
|
55f35933e8 | ||
|
|
b125bff7c7 | ||
|
|
c1d261445e | ||
|
|
c494d6c48b | ||
|
|
4134d47f6e | ||
|
|
0b25e62c5c | ||
|
|
4d0cbff5a1 | ||
|
|
c79463aaee | ||
|
|
339e1c5fab | ||
|
|
36455c24c8 | ||
|
|
a622a4c713 | ||
|
|
1aaaae24ee | ||
|
|
9191000da4 | ||
|
|
8eb93086bd | ||
|
|
b64294af6d | ||
|
|
bbf33a8895 | ||
|
|
bcc1289a23 | ||
|
|
58bd48c1ae | ||
|
|
1a3736bf98 | ||
|
|
0eec182df4 | ||
|
|
c526844eb2 | ||
|
|
d856c1f328 | ||
|
|
15e47a9eb3 | ||
|
|
da98d39b61 | ||
|
|
411bafb5a6 | ||
|
|
0a64bf2068 | ||
|
|
db6a2673de | ||
|
|
e11d4d3101 | ||
|
|
8fd3bb2302 | ||
|
|
fb3a832a52 | ||
|
|
9c146c1245 | ||
|
|
792f506425 | ||
|
|
6121e9fc65 | ||
|
|
955fa1f49f | ||
|
|
2544d0a084 | ||
|
|
8f0a12016a | ||
|
|
34ccd3524f | ||
|
|
563e99ecd3 | ||
|
|
70f14be50f | ||
|
|
8bd0da886e | ||
|
|
1fd326f980 | ||
|
|
d9cc664ea6 | ||
|
|
d65e241056 | ||
|
|
fe2b1f3d9f | ||
|
|
17c1f50fc3 | ||
|
|
5c1c022291 | ||
|
|
72435dee56 | ||
|
|
42f2eff02a | ||
|
|
0b68bef0be | ||
|
|
676e943d81 | ||
|
|
2b4307dee9 | ||
|
|
f3f936c196 | ||
|
|
eb900ddbe1 | ||
|
|
c2c323347a | ||
|
|
af6642b91b | ||
|
|
ffded8d82a | ||
|
|
00b57c7983 | ||
|
|
5d40a8992a | ||
|
|
e543625295 | ||
|
|
0b25ff649e | ||
|
|
63344ac62d | ||
|
|
2e60aafebf | ||
|
|
131e8f4aea | ||
|
|
53f01a39c9 | ||
|
|
50f4796144 | ||
|
|
5bdfae377f | ||
|
|
44970648ea | ||
|
|
3cd7f0b77d | ||
|
|
4d243f9b92 | ||
|
|
6b716f8f56 | ||
|
|
5b99219f29 | ||
|
|
f0dcb51915 | ||
|
|
3ff658a13b | ||
|
|
3a46513d4b | ||
|
|
ad0064993d | ||
|
|
458d4acd22 | ||
|
|
8ebc99175c | ||
|
|
9889cb2b69 | ||
|
|
191e530071 | ||
|
|
ae30d39b4d | ||
|
|
c354e46846 | ||
|
|
5da4e43e50 | ||
|
|
eae6f7760f | ||
|
|
00161212c8 | ||
|
|
8976b92842 | ||
|
|
c3a6a8fb17 | ||
|
|
391e5ca483 | ||
|
|
36654cb808 | ||
|
|
6d16e087d9 | ||
|
|
fe90e5aa8f | ||
|
|
3c22cd8ef4 | ||
|
|
5429b8568e | ||
|
|
0eccaf9a21 | ||
|
|
be0950c1ec | ||
|
|
6ecd150f75 | ||
|
|
02fb37189b | ||
|
|
8fe2536996 | ||
|
|
4b9e156c5d | ||
|
|
9265e1ffec | ||
|
|
d11735b04c | ||
|
|
d33b700477 | ||
|
|
97d75c2cb9 | ||
|
|
287115f4c3 | ||
|
|
63a221212b | ||
|
|
6a916fd0e1 | ||
|
|
220691d61d | ||
|
|
5cafc4bcbd | ||
|
|
b3a78dc2e6 | ||
|
|
bebc6eabe5 | ||
|
|
26dc6a4ac2 | ||
|
|
ff2626723a | ||
|
|
72bb897269 | ||
|
|
f46387a226 | ||
|
|
a4cbbccb2a | ||
|
|
3e1a008399 | ||
|
|
7e70a8c1de | ||
|
|
8efee04a10 | ||
|
|
a35099f949 |
6
.gitignore
vendored
@@ -61,14 +61,8 @@ buck-out/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
|
||||
# Build artifacts
|
||||
*.jsbundle
|
||||
|
||||
17
README.md
@@ -27,10 +27,25 @@ You can download Debian/Ubuntu binaries:
|
||||
You can download source archives (produced by ```make source-package```):
|
||||
* [source builds](https://download.jitsi.org/jitsi-meet/src/)
|
||||
|
||||
You can get our mobile versions from here:
|
||||
### Mobile apps
|
||||
|
||||
* [Android](https://play.google.com/store/apps/details?id=org.jitsi.meet)
|
||||
|
||||
[<img src="resources/img/google-play-badge.png" height="50">](https://play.google.com/store/apps/details?id=org.jitsi.meet)
|
||||
|
||||
* [Android (F-Droid)](https://f-droid.org/en/packages/org.jitsi.meet/)
|
||||
|
||||
[<img src="resources/img/f-droid-badge.png" height="50">](https://f-droid.org/en/packages/org.jitsi.meet/)
|
||||
|
||||
* [iOS](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
|
||||
|
||||
[<img src="resources/img/appstore-badge.png" height="50">](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
|
||||
|
||||
You can also sign up for our open beta testing here:
|
||||
|
||||
* [Android](https://play.google.com/apps/testing/org.jitsi.meet)
|
||||
* [iOS](https://testflight.apple.com/join/isy6ja7S)
|
||||
|
||||
## Development
|
||||
|
||||
For web development see [here](doc/development.md), and for mobile see [here](doc/mobile.md).
|
||||
|
||||
@@ -32,12 +32,23 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
buildConfigField "boolean", "GOOGLE_SERVICES_ENABLED", "${googleServicesEnabled}"
|
||||
buildConfigField "boolean", "LIBRE_BUILD", "${rootProject.ext.libreBuild}"
|
||||
}
|
||||
release {
|
||||
// Uncomment the following line for singing a test release build.
|
||||
//signingConfig signingConfigs.debug
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules-release.pro'
|
||||
buildConfigField "boolean", "GOOGLE_SERVICES_ENABLED", "${googleServicesEnabled}"
|
||||
@@ -69,7 +80,9 @@ repositories {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-5'
|
||||
|
||||
if (!rootProject.ext.libreBuild) {
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
@@ -83,9 +96,6 @@ dependencies {
|
||||
}
|
||||
|
||||
implementation project(':sdk')
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
|
||||
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
|
||||
}
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
|
||||
BIN
android/app/debug.keystore
Normal file
6
android/app/proguard-rules.pro
vendored
@@ -85,4 +85,8 @@
|
||||
# ^^^ We added the above when we switched minifyEnabled on.
|
||||
|
||||
# Rule to avoid build errors related to SVGs.
|
||||
-keep public class com.horcrux.svg.** {*;}
|
||||
-keep public class com.horcrux.svg.** {*;}
|
||||
|
||||
# Hermes
|
||||
-keep class com.facebook.hermes.unicode.** { *; }
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:name=".MainApplication"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
|
||||
/**
|
||||
* Simple {@link Application} for hooking up LeakCanary:
|
||||
* https://github.com/square/leakcanary
|
||||
*/
|
||||
public class MainApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
if (!LeakCanary.isInAnalyzerProcess(this)) {
|
||||
LeakCanary.install(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'io.fabric.tools:gradle:1.27.0'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'io.fabric.tools:gradle:1.28.1'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files.
|
||||
@@ -27,8 +27,6 @@ allprojects {
|
||||
jcenter()
|
||||
// React Native (JS, Obj-C sources, Android binaries) is installed from npm.
|
||||
maven { url "$rootDir/../node_modules/react-native/android" }
|
||||
// Android JSC is installed from npm.
|
||||
maven { url("$rootDir/../node_modules/jsc-android/dist") }
|
||||
}
|
||||
|
||||
// Make sure we use the react-native version in node_modules and not the one
|
||||
@@ -165,50 +163,6 @@ ext {
|
||||
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
|
||||
}
|
||||
|
||||
// If Android SDK is not installed, accept its license so that it
|
||||
// is automatically downloaded.
|
||||
afterEvaluate { project ->
|
||||
// Either the environment variable ANDROID_HOME or the property sdk.dir in
|
||||
// local.properties identifies where Android SDK is installed.
|
||||
def androidHome = System.env.ANDROID_HOME
|
||||
if (!androidHome) {
|
||||
// ANDROID_HOME is not set. Is sdk.dir set?
|
||||
def file = file("${project.rootDir}/local.properties")
|
||||
def props = new Properties()
|
||||
if (file.canRead()) {
|
||||
file.withInputStream {
|
||||
props.load(it)
|
||||
androidHome = props.'sdk.dir'
|
||||
}
|
||||
}
|
||||
if (!androidHome && (!file.exists() || file.canWrite())) {
|
||||
// Neither ANDROID_HOME nor sdk.dir is set. Set sdk.dir (because
|
||||
// environment variables cannot be set).
|
||||
props.'sdk.dir' = "${project.buildDir}/android-sdk".toString()
|
||||
file.withOutputStream {
|
||||
props.store(it, null)
|
||||
androidHome = props.'sdk.dir'
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the license is not accepted, accept it so that automatic downloading
|
||||
// kicks in.
|
||||
// The license hash can be taken from the accepted licenses, by doing this
|
||||
// on your local machine the file is
|
||||
// ${androidHome}/licenses/android-sdk-license
|
||||
if (androidHome) {
|
||||
def dir = file("${androidHome}/licenses")
|
||||
dir.mkdirs()
|
||||
def file = file("${dir.path}/android-sdk-license")
|
||||
if (!file.exists()) {
|
||||
file.withWriter {
|
||||
def hash = 'd56f5187479451eabf01fb78af6dfcb131a6481e'
|
||||
it.write(hash, 0, hash.length())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force the version of the Android build tools we have chosen on all
|
||||
// subprojects. The forcing was introduced for react-native and the third-party
|
||||
// modules that we utilize such as react-native-background-timer.
|
||||
|
||||
@@ -24,7 +24,7 @@ platform :android do
|
||||
|
||||
# Upload built artifact to the Closed Beta track
|
||||
upload_to_play_store(
|
||||
track: "Closed Beta",
|
||||
track: "beta",
|
||||
json_key: ENV["JITSI_JSON_KEY_FILE"],
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_images: true,
|
||||
|
||||
BIN
android/fastlane/screenshots/Feature-Graphic-1024x500-1-1.png
Normal file
|
After Width: | Height: | Size: 512 KiB |
BIN
android/fastlane/screenshots/Feature-Graphic-1024x500-1.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
android/fastlane/screenshots/Feature-Graphic-1024x500-2.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
android/fastlane/screenshots/Feature-Graphic-1024x500-3.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
android/fastlane/screenshots/GroupCall.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
android/fastlane/screenshots/GroupCall_framed.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
android/fastlane/screenshots/More Menu.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
android/fastlane/screenshots/More Menu_framed.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
android/fastlane/screenshots/Nexus 9 Body.png
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
android/fastlane/screenshots/Nexus-9-Landscape.png
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
BIN
android/fastlane/screenshots/Nexus-9-Portrait.png
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
android/fastlane/screenshots/Video-Call-1-1024x768.png
Normal file
|
After Width: | Height: | Size: 948 KiB |
BIN
android/fastlane/screenshots/Video-Call-1-1280x720.png
Normal file
|
After Width: | Height: | Size: 983 KiB |
BIN
android/fastlane/screenshots/Video-Call-2-1024x768.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
android/fastlane/screenshots/Video-Call-2-1280x720.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
android/fastlane/screenshots/WelcomePage-Calendar.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
android/fastlane/screenshots/WelcomePage-Calendar_framed.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
android/fastlane/screenshots/WelcomeScreen-1024x768.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
android/fastlane/screenshots/WelcomeScreen-1280x720.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
@@ -20,5 +20,5 @@
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
appVersion=19.4.0
|
||||
sdkVersion=2.4.0
|
||||
appVersion=20.0.0
|
||||
sdkVersion=2.6.0
|
||||
|
||||
@@ -10,7 +10,7 @@ MVN_HTTP=0
|
||||
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
|
||||
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
|
||||
RN_VERSION=$(jq -r '.dependencies."react-native"' ${THIS_DIR}/../../package.json)
|
||||
JSC_VERSION="r"$(jq -r '.dependencies."jsc-android"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -d . -f 1)
|
||||
HERMES_VERSION=$(jq -r '.dependencies."hermes-engine"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -c 2-)
|
||||
DO_GIT_TAG=${GIT_TAG:-0}
|
||||
|
||||
if [[ $THE_MVN_REPO == http* ]]; then
|
||||
@@ -38,17 +38,19 @@ if [[ $MVN_HTTP == 1 ]]; then
|
||||
-DgeneratePom=false \
|
||||
-DpomFile=react-native-${RN_VERSION}.pom || true
|
||||
popd
|
||||
# Push JSC
|
||||
echo "Pushing JSC ${JSC_VERSION} to the Maven repo"
|
||||
pushd ${THIS_DIR}/../../node_modules/jsc-android/dist/org/webkit/android-jsc/${JSC_VERSION}
|
||||
# Push Hermes
|
||||
echo "Pushing Hermes ${HERMES_VERSION} to the Maven repo"
|
||||
pushd ${THIS_DIR}/../../node_modules/hermes-engine/android/
|
||||
mvn \
|
||||
deploy:deploy-file \
|
||||
-Durl=${MVN_REPO} \
|
||||
-DrepositoryId=${MVN_REPO_ID} \
|
||||
-Dfile=android-jsc-${JSC_VERSION}.aar \
|
||||
-Dfile=hermes-release.aar \
|
||||
-Dpackaging=aar \
|
||||
-DgeneratePom=false \
|
||||
-DpomFile=android-jsc-${JSC_VERSION}.pom || true
|
||||
-DgroupId=com.facebook \
|
||||
-DartifactId=hermes \
|
||||
-Dversion=${HERMES_VERSION} \
|
||||
-DgeneratePom=true || true
|
||||
popd
|
||||
else
|
||||
# Push React Native, if necessary
|
||||
@@ -65,17 +67,19 @@ else
|
||||
popd
|
||||
fi
|
||||
|
||||
# Push JSC, if necessary
|
||||
if [[ ! -d ${MVN_REPO}/org/webkit/android-jsc/${JSC_VERSION} ]]; then
|
||||
echo "Pushing JSC ${JSC_VERSION} to the Maven repo"
|
||||
pushd ${THIS_DIR}/../../node_modules/jsc-android/dist/org/webkit/android-jsc/${JSC_VERSION}
|
||||
# Push Hermes, if necessary
|
||||
if [[ ! -d ${MVN_REPO}/com/facebook/hermes/${HERMES_VERSION} ]]; then
|
||||
echo "Pushing Hermes ${HERMES_VERSION} to the Maven repo"
|
||||
pushd ${THIS_DIR}/../../node_modules/hermes-engine/android/
|
||||
mvn \
|
||||
deploy:deploy-file \
|
||||
-Durl=${MVN_REPO} \
|
||||
-Dfile=android-jsc-${JSC_VERSION}.aar \
|
||||
-Dfile=hermes-release.aar \
|
||||
-Dpackaging=aar \
|
||||
-DgeneratePom=false \
|
||||
-DpomFile=android-jsc-${JSC_VERSION}.pom
|
||||
-DgroupId=com.facebook \
|
||||
-DartifactId=hermes \
|
||||
-Dversion=${HERMES_VERSION} \
|
||||
-DgeneratePom=true
|
||||
popd
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
@@ -31,17 +33,26 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/libc++_shared.so'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.fragment:fragment:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.fragment:fragment:1.2.0'
|
||||
|
||||
//noinspection GradleDynamicVersion
|
||||
api 'com.facebook.react:react-native:+'
|
||||
implementation 'org.webkit:android-jsc:+'
|
||||
|
||||
// Hermes JS engine
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/"
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||
|
||||
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
@@ -213,6 +224,14 @@ publishing {
|
||||
dependency.appendNode('artifactId', artifactId)
|
||||
dependency.appendNode('version', it.moduleVersion)
|
||||
}
|
||||
|
||||
// Add Hermes dependency.
|
||||
def hermesPkg = new File("$rootDir/../node_modules/hermes-engine/package.json")
|
||||
def hermesVersion = new JsonSlurper().parseText(hermesPkg.text).version
|
||||
def hermesDependency = dependencies.appendNode('dependency')
|
||||
hermesDependency.appendNode('groupId', "com.facebook")
|
||||
hermesDependency.appendNode('artifactId', "hermes")
|
||||
hermesDependency.appendNode('version', hermesVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
@@ -49,8 +53,16 @@ class AmplitudeModule
|
||||
* @param apiKey The API_KEY of the Amplitude project.
|
||||
*/
|
||||
@ReactMethod
|
||||
@SuppressLint("HardwareIds")
|
||||
public void init(String instanceName, String apiKey) {
|
||||
Amplitude.getInstance(instanceName).initialize(getCurrentActivity(), apiKey);
|
||||
|
||||
// Set the device ID to something consistent.
|
||||
String android_id
|
||||
= Settings.Secure.getString(getReactApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
if (!TextUtils.isEmpty(android_id)) {
|
||||
Amplitude.getInstance(instanceName).setDeviceId(android_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright @ 2017-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.telecom.CallAudioState;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
|
||||
/**
|
||||
* {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
|
||||
* Android versions >= O when ConnectionService is enabled.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class AudioDeviceHandlerConnectionService implements
|
||||
AudioModeModule.AudioDeviceHandlerInterface,
|
||||
RNConnectionService.CallAudioStateListener {
|
||||
|
||||
private final static String TAG = AudioDeviceHandlerConnectionService.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* {@link AudioManager} instance used to interact with the Android audio subsystem.
|
||||
*/
|
||||
private AudioManager audioManager;
|
||||
|
||||
/**
|
||||
* Reference to the main {@code AudioModeModule}.
|
||||
*/
|
||||
private AudioModeModule module;
|
||||
|
||||
/**
|
||||
* Converts any of the "DEVICE_" constants into the corresponding
|
||||
* {@link android.telecom.CallAudioState} "ROUTE_" number.
|
||||
*
|
||||
* @param audioDevice one of the "DEVICE_" constants.
|
||||
* @return a route number {@link android.telecom.CallAudioState#ROUTE_EARPIECE} if
|
||||
* no match is found.
|
||||
*/
|
||||
private static int audioDeviceToRouteInt(String audioDevice) {
|
||||
if (audioDevice == null) {
|
||||
return CallAudioState.ROUTE_SPEAKER;
|
||||
}
|
||||
switch (audioDevice) {
|
||||
case AudioModeModule.DEVICE_BLUETOOTH:
|
||||
return CallAudioState.ROUTE_BLUETOOTH;
|
||||
case AudioModeModule.DEVICE_EARPIECE:
|
||||
return CallAudioState.ROUTE_EARPIECE;
|
||||
case AudioModeModule.DEVICE_HEADPHONES:
|
||||
return CallAudioState.ROUTE_WIRED_HEADSET;
|
||||
case AudioModeModule.DEVICE_SPEAKER:
|
||||
return CallAudioState.ROUTE_SPEAKER;
|
||||
default:
|
||||
JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
|
||||
return CallAudioState.ROUTE_SPEAKER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates given route mask into the "DEVICE_" list.
|
||||
*
|
||||
* @param supportedRouteMask an integer coming from
|
||||
* {@link android.telecom.CallAudioState#getSupportedRouteMask()}.
|
||||
* @return a list of device names.
|
||||
*/
|
||||
private static Set<String> routesToDeviceNames(int supportedRouteMask) {
|
||||
Set<String> devices = new HashSet<>();
|
||||
if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
|
||||
devices.add(AudioModeModule.DEVICE_EARPIECE);
|
||||
}
|
||||
if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH) == CallAudioState.ROUTE_BLUETOOTH) {
|
||||
devices.add(AudioModeModule.DEVICE_BLUETOOTH);
|
||||
}
|
||||
if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
|
||||
devices.add(AudioModeModule.DEVICE_SPEAKER);
|
||||
}
|
||||
if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) == CallAudioState.ROUTE_WIRED_HEADSET) {
|
||||
devices.add(AudioModeModule.DEVICE_HEADPHONES);
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to store the most recently reported audio devices.
|
||||
* Makes it easier to compare for a change, because the devices are stored
|
||||
* as a mask in the {@link android.telecom.CallAudioState}. The mask is populated into
|
||||
* the {@code availableDevices} on each update.
|
||||
*/
|
||||
private int supportedRouteMask = -1;
|
||||
|
||||
public AudioDeviceHandlerConnectionService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAudioStateChange(final CallAudioState state) {
|
||||
module.runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean audioRouteChanged
|
||||
= audioDeviceToRouteInt(module.getSelectedDevice()) != state.getRoute();
|
||||
int newSupportedRoutes = state.getSupportedRouteMask();
|
||||
boolean audioDevicesChanged = supportedRouteMask != newSupportedRoutes;
|
||||
if (audioDevicesChanged) {
|
||||
supportedRouteMask = newSupportedRoutes;
|
||||
Set<String> devices = routesToDeviceNames(supportedRouteMask);
|
||||
module.replaceDevices(devices);
|
||||
JitsiMeetLogger.i(TAG + " Available audio devices: " + devices.toString());
|
||||
}
|
||||
|
||||
if (audioRouteChanged || audioDevicesChanged) {
|
||||
module.resetSelectedDevice();
|
||||
module.updateAudioRoute();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Context context, AudioModeModule audioModeModule) {
|
||||
JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
|
||||
|
||||
module = audioModeModule;
|
||||
audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
|
||||
if (rcs != null) {
|
||||
rcs.setCallAudioStateListener(this);
|
||||
} else {
|
||||
JitsiMeetLogger.w(TAG + " Couldn't set call audio state listener, module is null");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
|
||||
if (rcs != null) {
|
||||
rcs.setCallAudioStateListener(null);
|
||||
} else {
|
||||
JitsiMeetLogger.w(TAG + " Couldn't set call audio state listener, module is null");
|
||||
}
|
||||
}
|
||||
|
||||
public void setAudioRoute(String audioDevice) {
|
||||
int newAudioRoute = audioDeviceToRouteInt(audioDevice);
|
||||
|
||||
RNConnectionService.setAudioRoute(newAudioRoute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setMode(int mode) {
|
||||
if (mode != AudioModeModule.DEFAULT) {
|
||||
// This shouldn't be needed when using ConnectionService, but some devices have been
|
||||
// observed not doing it.
|
||||
try {
|
||||
audioManager.setMicrophoneMute(false);
|
||||
} catch (Throwable tr) {
|
||||
JitsiMeetLogger.w(tr, TAG + " Failed to unmute the microphone");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright @ 2017-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
|
||||
/**
|
||||
* {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
|
||||
* all post-M Android versions. This handler can be used on any Android versions >= M, but by
|
||||
* default it's only used on versions < O, since versions >= O use ConnectionService, but it
|
||||
* can be disabled.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class AudioDeviceHandlerGeneric implements
|
||||
AudioModeModule.AudioDeviceHandlerInterface,
|
||||
AudioManager.OnAudioFocusChangeListener {
|
||||
|
||||
private final static String TAG = AudioDeviceHandlerGeneric.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Reference to the main {@code AudioModeModule}.
|
||||
*/
|
||||
private AudioModeModule module;
|
||||
|
||||
/**
|
||||
* Constant defining a USB headset. Only available on API level >= 26.
|
||||
* The value of: AudioDeviceInfo.TYPE_USB_HEADSET
|
||||
*/
|
||||
private static final int TYPE_USB_HEADSET = 22;
|
||||
|
||||
/**
|
||||
* Indicator that we have lost audio focus.
|
||||
*/
|
||||
private boolean audioFocusLost = false;
|
||||
|
||||
/**
|
||||
* {@link AudioManager} instance used to interact with the Android audio
|
||||
* subsystem.
|
||||
*/
|
||||
private AudioManager audioManager;
|
||||
|
||||
/**
|
||||
* {@link Runnable} for running audio device detection the main thread.
|
||||
* This is only used on Android >= M.
|
||||
*/
|
||||
private final Runnable onAudioDeviceChangeRunner = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Set<String> devices = new HashSet<>();
|
||||
AudioDeviceInfo[] deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
|
||||
|
||||
for (AudioDeviceInfo info: deviceInfos) {
|
||||
switch (info.getType()) {
|
||||
case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
|
||||
devices.add(AudioModeModule.DEVICE_BLUETOOTH);
|
||||
break;
|
||||
case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
|
||||
devices.add(AudioModeModule.DEVICE_EARPIECE);
|
||||
break;
|
||||
case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
|
||||
devices.add(AudioModeModule.DEVICE_SPEAKER);
|
||||
break;
|
||||
case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
|
||||
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
|
||||
case TYPE_USB_HEADSET:
|
||||
devices.add(AudioModeModule.DEVICE_HEADPHONES);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
module.replaceDevices(devices);
|
||||
|
||||
JitsiMeetLogger.i(TAG + " Available audio devices: " + devices.toString());
|
||||
|
||||
module.updateAudioRoute();
|
||||
}
|
||||
};
|
||||
|
||||
private final android.media.AudioDeviceCallback audioDeviceCallback =
|
||||
new android.media.AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(
|
||||
AudioDeviceInfo[] addedDevices) {
|
||||
JitsiMeetLogger.d(TAG + " Audio devices added");
|
||||
onAudioDeviceChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(
|
||||
AudioDeviceInfo[] removedDevices) {
|
||||
JitsiMeetLogger.d(TAG + " Audio devices removed");
|
||||
onAudioDeviceChange();
|
||||
}
|
||||
};
|
||||
|
||||
public AudioDeviceHandlerGeneric() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when devices change. It
|
||||
* makes sure the operation is performed on the audio thread.
|
||||
*/
|
||||
private void onAudioDeviceChange() {
|
||||
module.runInAudioThread(onAudioDeviceChangeRunner);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
|
||||
* when the audio focus of the system is updated.
|
||||
*
|
||||
* @param focusChange - The type of focus change.
|
||||
*/
|
||||
@Override
|
||||
public void onAudioFocusChange(final int focusChange) {
|
||||
module.runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
switch (focusChange) {
|
||||
case AudioManager.AUDIOFOCUS_GAIN: {
|
||||
JitsiMeetLogger.d(TAG + " Audio focus gained");
|
||||
// Some other application potentially stole our audio focus
|
||||
// temporarily. Restore our mode.
|
||||
if (audioFocusLost) {
|
||||
module.updateAudioRoute();
|
||||
}
|
||||
audioFocusLost = false;
|
||||
break;
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
|
||||
JitsiMeetLogger.d(TAG + " Audio focus lost");
|
||||
audioFocusLost = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set the output route to a Bluetooth device.
|
||||
*
|
||||
* @param enabled true if Bluetooth should use used, false otherwise.
|
||||
*/
|
||||
private void setBluetoothAudioRoute(boolean enabled) {
|
||||
if (enabled) {
|
||||
audioManager.startBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(true);
|
||||
} else {
|
||||
audioManager.setBluetoothScoOn(false);
|
||||
audioManager.stopBluetoothSco();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Context context, AudioModeModule audioModeModule) {
|
||||
JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
|
||||
|
||||
module = audioModeModule;
|
||||
audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// Setup runtime device change detection.
|
||||
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
|
||||
|
||||
// Do an initial detection.
|
||||
onAudioDeviceChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAudioRoute(String device) {
|
||||
// Turn speaker on / off
|
||||
audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
|
||||
|
||||
// Turn bluetooth on / off
|
||||
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setMode(int mode) {
|
||||
if (mode == AudioModeModule.DEFAULT) {
|
||||
audioFocusLost = false;
|
||||
audioManager.setMode(AudioManager.MODE_NORMAL);
|
||||
audioManager.abandonAudioFocus(this);
|
||||
audioManager.setSpeakerphoneOn(false);
|
||||
setBluetoothAudioRoute(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||
audioManager.setMicrophoneMute(false);
|
||||
|
||||
if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
|
||||
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
JitsiMeetLogger.w(TAG + " Audio focus request failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright @ 2017-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.AudioManager;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
|
||||
/**
|
||||
* {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
|
||||
* legacy (pre-M) Android versions.
|
||||
*/
|
||||
class AudioDeviceHandlerLegacy implements
|
||||
AudioModeModule.AudioDeviceHandlerInterface,
|
||||
AudioManager.OnAudioFocusChangeListener,
|
||||
BluetoothHeadsetMonitor.Listener {
|
||||
|
||||
private final static String TAG = AudioDeviceHandlerLegacy.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Reference to the main {@code AudioModeModule}.
|
||||
*/
|
||||
private AudioModeModule module;
|
||||
|
||||
/**
|
||||
* Indicator that we have lost audio focus.
|
||||
*/
|
||||
private boolean audioFocusLost = false;
|
||||
|
||||
/**
|
||||
* {@link AudioManager} instance used to interact with the Android audio
|
||||
* subsystem.
|
||||
*/
|
||||
private AudioManager audioManager;
|
||||
|
||||
/**
|
||||
* {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
|
||||
* old (< M) Android versions.
|
||||
*/
|
||||
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
|
||||
|
||||
public AudioDeviceHandlerLegacy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when Bluetooth devices are
|
||||
* connected / disconnected.
|
||||
*/
|
||||
@Override
|
||||
public void onBluetoothDeviceChange(final boolean deviceAvailable) {
|
||||
module.runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (deviceAvailable) {
|
||||
module.addDevice(AudioModeModule.DEVICE_BLUETOOTH);
|
||||
} else {
|
||||
module.removeDevice(AudioModeModule.DEVICE_BLUETOOTH);
|
||||
}
|
||||
|
||||
module.updateAudioRoute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when a headset is plugged
|
||||
* or unplugged.
|
||||
*/
|
||||
private void onHeadsetDeviceChange() {
|
||||
module.runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// XXX: isWiredHeadsetOn is not deprecated when used just for
|
||||
// knowing if there is a wired headset connected, regardless of
|
||||
// audio being routed to it.
|
||||
//noinspection deprecation
|
||||
if (audioManager.isWiredHeadsetOn()) {
|
||||
module.addDevice(AudioModeModule.DEVICE_HEADPHONES);
|
||||
} else {
|
||||
module.removeDevice(AudioModeModule.DEVICE_HEADPHONES);
|
||||
}
|
||||
|
||||
module.updateAudioRoute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
|
||||
* when the audio focus of the system is updated.
|
||||
*
|
||||
* @param focusChange - The type of focus change.
|
||||
*/
|
||||
@Override
|
||||
public void onAudioFocusChange(final int focusChange) {
|
||||
module.runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
switch (focusChange) {
|
||||
case AudioManager.AUDIOFOCUS_GAIN: {
|
||||
JitsiMeetLogger.d(TAG + " Audio focus gained");
|
||||
// Some other application potentially stole our audio focus
|
||||
// temporarily. Restore our mode.
|
||||
if (audioFocusLost) {
|
||||
module.updateAudioRoute();
|
||||
}
|
||||
audioFocusLost = false;
|
||||
break;
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
|
||||
JitsiMeetLogger.d(TAG + " Audio focus lost");
|
||||
audioFocusLost = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set the output route to a Bluetooth device.
|
||||
*
|
||||
* @param enabled true if Bluetooth should use used, false otherwise.
|
||||
*/
|
||||
private void setBluetoothAudioRoute(boolean enabled) {
|
||||
if (enabled) {
|
||||
audioManager.startBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(true);
|
||||
} else {
|
||||
audioManager.setBluetoothScoOn(false);
|
||||
audioManager.stopBluetoothSco();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Context context, AudioModeModule audioModeModule) {
|
||||
JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
|
||||
|
||||
module = audioModeModule;
|
||||
audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// Setup runtime device change detection.
|
||||
//
|
||||
|
||||
// Detect changes in wired headset connections.
|
||||
IntentFilter wiredHeadSetFilter = new IntentFilter(AudioManager.ACTION_HEADSET_PLUG);
|
||||
BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
JitsiMeetLogger.d(TAG + " Wired headset added / removed");
|
||||
onHeadsetDeviceChange();
|
||||
}
|
||||
};
|
||||
context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
|
||||
|
||||
// Detect Bluetooth device changes.
|
||||
bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(context, this);
|
||||
|
||||
// On Android < M, detect if we have an earpiece.
|
||||
PackageManager pm = context.getPackageManager();
|
||||
if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
|
||||
module.addDevice(AudioModeModule.DEVICE_EARPIECE);
|
||||
}
|
||||
|
||||
// Always assume there is a speaker.
|
||||
module.addDevice(AudioModeModule.DEVICE_SPEAKER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
bluetoothHeadsetMonitor.stop();
|
||||
bluetoothHeadsetMonitor = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAudioRoute(String device) {
|
||||
// Turn speaker on / off
|
||||
audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
|
||||
|
||||
// Turn bluetooth on / off
|
||||
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setMode(int mode) {
|
||||
if (mode == AudioModeModule.DEFAULT) {
|
||||
audioFocusLost = false;
|
||||
audioManager.setMode(AudioManager.MODE_NORMAL);
|
||||
audioManager.abandonAudioFocus(this);
|
||||
audioManager.setSpeakerphoneOn(false);
|
||||
setBluetoothAudioRoute(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||
audioManager.setMicrophoneMute(false);
|
||||
|
||||
if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
|
||||
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
JitsiMeetLogger.w(TAG + " Audio focus request failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
* Copyright @ 2017-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,16 +16,8 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
@@ -61,9 +53,7 @@ import java.util.concurrent.Executors;
|
||||
* {@code AudioModeModule.DEFAULT} mode should be used.
|
||||
*/
|
||||
@ReactModule(name = AudioModeModule.NAME)
|
||||
class AudioModeModule extends ReactContextBaseJavaModule
|
||||
implements AudioManager.OnAudioFocusChangeListener {
|
||||
|
||||
class AudioModeModule extends ReactContextBaseJavaModule {
|
||||
public static final String NAME = "AudioMode";
|
||||
|
||||
/**
|
||||
@@ -75,161 +65,33 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
* - VIDEO_CALL: Used for video calls. It will use the speaker by default,
|
||||
* unless a wired or Bluetooth headset is connected.
|
||||
*/
|
||||
private static final int DEFAULT = 0;
|
||||
private static final int AUDIO_CALL = 1;
|
||||
private static final int VIDEO_CALL = 2;
|
||||
|
||||
/**
|
||||
* Constant defining the action for plugging in a headset. This is used on
|
||||
* our device detection system for API < 23.
|
||||
*/
|
||||
private static final String ACTION_HEADSET_PLUG
|
||||
= (Build.VERSION.SDK_INT >= 21)
|
||||
? AudioManager.ACTION_HEADSET_PLUG
|
||||
: Intent.ACTION_HEADSET_PLUG;
|
||||
|
||||
/**
|
||||
* Constant defining a USB headset. Only available on API level >= 26.
|
||||
* The value of: AudioDeviceInfo.TYPE_USB_HEADSET
|
||||
*/
|
||||
private static final int TYPE_USB_HEADSET = 22;
|
||||
static final int DEFAULT = 0;
|
||||
static final int AUDIO_CALL = 1;
|
||||
static final int VIDEO_CALL = 2;
|
||||
|
||||
/**
|
||||
* The {@code Log} tag {@code AudioModeModule} is to log messages with.
|
||||
*/
|
||||
static final String TAG = NAME;
|
||||
|
||||
/**
|
||||
* Converts any of the "DEVICE_" constants into the corresponding
|
||||
* {@link android.telecom.CallAudioState} "ROUTE_" number.
|
||||
*
|
||||
* @param audioDevice one of the "DEVICE_" constants.
|
||||
* @return a route number {@link android.telecom.CallAudioState#ROUTE_EARPIECE} if
|
||||
* no match is found.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static int audioDeviceToRouteInt(String audioDevice) {
|
||||
if (audioDevice == null) {
|
||||
return android.telecom.CallAudioState.ROUTE_EARPIECE;
|
||||
}
|
||||
switch (audioDevice) {
|
||||
case DEVICE_BLUETOOTH:
|
||||
return android.telecom.CallAudioState.ROUTE_BLUETOOTH;
|
||||
case DEVICE_EARPIECE:
|
||||
return android.telecom.CallAudioState.ROUTE_EARPIECE;
|
||||
case DEVICE_HEADPHONES:
|
||||
return android.telecom.CallAudioState.ROUTE_WIRED_HEADSET;
|
||||
case DEVICE_SPEAKER:
|
||||
return android.telecom.CallAudioState.ROUTE_SPEAKER;
|
||||
default:
|
||||
JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
|
||||
return android.telecom.CallAudioState.ROUTE_EARPIECE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates given route mask into the "DEVICE_" list.
|
||||
*
|
||||
* @param supportedRouteMask an integer coming from
|
||||
* {@link android.telecom.CallAudioState#getSupportedRouteMask()}.
|
||||
* @return a list of device names.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
private static Set<String> routesToDeviceNames(int supportedRouteMask) {
|
||||
Set<String> devices = new HashSet<>();
|
||||
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_EARPIECE)
|
||||
== android.telecom.CallAudioState.ROUTE_EARPIECE) {
|
||||
devices.add(DEVICE_EARPIECE);
|
||||
}
|
||||
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_BLUETOOTH)
|
||||
== android.telecom.CallAudioState.ROUTE_BLUETOOTH) {
|
||||
devices.add(DEVICE_BLUETOOTH);
|
||||
}
|
||||
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_SPEAKER)
|
||||
== android.telecom.CallAudioState.ROUTE_SPEAKER) {
|
||||
devices.add(DEVICE_SPEAKER);
|
||||
}
|
||||
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_WIRED_HEADSET)
|
||||
== android.telecom.CallAudioState.ROUTE_WIRED_HEADSET) {
|
||||
devices.add(DEVICE_HEADPHONES);
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the ConnectionService is used for selecting audio devices.
|
||||
*/
|
||||
|
||||
private static final boolean supportsConnectionService = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||
private static boolean useConnectionService_ = supportsConnectionService;
|
||||
|
||||
static boolean useConnectionService() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||
return supportsConnectionService && useConnectionService_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicator that we have lost audio focus.
|
||||
*/
|
||||
private boolean audioFocusLost = false;
|
||||
|
||||
/**
|
||||
* {@link AudioManager} instance used to interact with the Android audio
|
||||
* subsystem.
|
||||
*/
|
||||
private final AudioManager audioManager;
|
||||
|
||||
/**
|
||||
* {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
|
||||
* old (< M) Android versions.
|
||||
*/
|
||||
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
|
||||
private AudioDeviceHandlerInterface audioDeviceHandler;
|
||||
|
||||
/**
|
||||
* {@link ExecutorService} for running all audio operations on a dedicated
|
||||
* thread.
|
||||
*/
|
||||
private static final ExecutorService executor
|
||||
= Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* {@link Runnable} for running audio device detection the main thread.
|
||||
* This is only used on Android >= M.
|
||||
*/
|
||||
private final Runnable onAudioDeviceChangeRunner = new Runnable() {
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public void run() {
|
||||
Set<String> devices = new HashSet<>();
|
||||
AudioDeviceInfo[] deviceInfos
|
||||
= audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
|
||||
|
||||
for (AudioDeviceInfo info: deviceInfos) {
|
||||
switch (info.getType()) {
|
||||
case AudioDeviceInfo.TYPE_BLUETOOTH_SCO:
|
||||
devices.add(DEVICE_BLUETOOTH);
|
||||
break;
|
||||
case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
|
||||
devices.add(DEVICE_EARPIECE);
|
||||
break;
|
||||
case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
|
||||
devices.add(DEVICE_SPEAKER);
|
||||
break;
|
||||
case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
|
||||
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
|
||||
case TYPE_USB_HEADSET:
|
||||
devices.add(DEVICE_HEADPHONES);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
availableDevices = devices;
|
||||
JitsiMeetLogger.i(TAG + " Available audio devices: " +
|
||||
availableDevices.toString());
|
||||
|
||||
// Reset user selection
|
||||
userSelectedDevice = null;
|
||||
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
};
|
||||
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Audio mode currently in use.
|
||||
@@ -239,10 +101,10 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
/**
|
||||
* Audio device types.
|
||||
*/
|
||||
private static final String DEVICE_BLUETOOTH = "BLUETOOTH";
|
||||
private static final String DEVICE_EARPIECE = "EARPIECE";
|
||||
private static final String DEVICE_HEADPHONES = "HEADPHONES";
|
||||
private static final String DEVICE_SPEAKER = "SPEAKER";
|
||||
static final String DEVICE_BLUETOOTH = "BLUETOOTH";
|
||||
static final String DEVICE_EARPIECE = "EARPIECE";
|
||||
static final String DEVICE_HEADPHONES = "HEADPHONES";
|
||||
static final String DEVICE_SPEAKER = "SPEAKER";
|
||||
|
||||
/**
|
||||
* Device change event.
|
||||
@@ -259,15 +121,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
*/
|
||||
private String selectedDevice;
|
||||
|
||||
/**
|
||||
* Used on API >= 26 to store the most recently reported audio devices.
|
||||
* Makes it easier to compare for a change, because the devices are stored
|
||||
* as a mask in the {@link android.telecom.CallAudioState}. The mask is populated into
|
||||
* the {@link #availableDevices} on each update.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private int supportedRouteMask;
|
||||
|
||||
/**
|
||||
* User selected device. When null the default is used depending on the
|
||||
* mode.
|
||||
@@ -283,31 +136,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
*/
|
||||
public AudioModeModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
|
||||
audioManager
|
||||
= (AudioManager)
|
||||
reactContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
// Starting Oreo the ConnectionImpl from ConnectionService is used to
|
||||
// detect the available devices.
|
||||
if (!useConnectionService()) {
|
||||
// Setup runtime device change detection.
|
||||
setupAudioRouteChangeDetection();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Do an initial detection on Android >= M.
|
||||
onAudioDeviceChange();
|
||||
} else {
|
||||
// On Android < M, detect if we have an earpiece.
|
||||
PackageManager pm = reactContext.getPackageManager();
|
||||
if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
|
||||
availableDevices.add(DEVICE_EARPIECE);
|
||||
}
|
||||
|
||||
// Always assume there is a speaker.
|
||||
availableDevices.add(DEVICE_SPEAKER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,132 +192,36 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when devices change. It
|
||||
* makes sure the operation is performed on the main thread.
|
||||
*
|
||||
* Only used on Android >= M.
|
||||
*/
|
||||
void onAudioDeviceChange() {
|
||||
runInAudioThread(onAudioDeviceChangeRunner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when Bluetooth devices are
|
||||
* connected / disconnected.
|
||||
*
|
||||
* Only used on Android < M. Runs on the main thread.
|
||||
*/
|
||||
void onBluetoothDeviceChange() {
|
||||
if (bluetoothHeadsetMonitor != null && bluetoothHeadsetMonitor.isHeadsetAvailable()) {
|
||||
availableDevices.add(DEVICE_BLUETOOTH);
|
||||
} else {
|
||||
availableDevices.remove(DEVICE_BLUETOOTH);
|
||||
}
|
||||
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when a headset is plugged
|
||||
* or unplugged.
|
||||
*
|
||||
* Only used on Android < M.
|
||||
*/
|
||||
void onHeadsetDeviceChange() {
|
||||
runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// XXX: isWiredHeadsetOn is not deprecated when used just for
|
||||
// knowing if there is a wired headset connected, regardless of
|
||||
// audio being routed to it.
|
||||
//noinspection deprecation
|
||||
if (audioManager.isWiredHeadsetOn()) {
|
||||
availableDevices.add(DEVICE_HEADPHONES);
|
||||
} else {
|
||||
availableDevices.remove(DEVICE_HEADPHONES);
|
||||
}
|
||||
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
void onCallAudioStateChange(Object callAudioState_) {
|
||||
final android.telecom.CallAudioState callAudioState
|
||||
= (android.telecom.CallAudioState)callAudioState_;
|
||||
runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int newSupportedRoutes = callAudioState.getSupportedRouteMask();
|
||||
boolean audioDevicesChanged
|
||||
= supportedRouteMask != newSupportedRoutes;
|
||||
if (audioDevicesChanged) {
|
||||
supportedRouteMask = newSupportedRoutes;
|
||||
availableDevices = routesToDeviceNames(supportedRouteMask);
|
||||
JitsiMeetLogger.i(TAG + " Available audio devices: "
|
||||
+ availableDevices.toString());
|
||||
}
|
||||
|
||||
boolean audioRouteChanged
|
||||
= audioDeviceToRouteInt(selectedDevice)
|
||||
!= callAudioState.getRoute();
|
||||
|
||||
if (audioRouteChanged || audioDevicesChanged) {
|
||||
// Reset user selection
|
||||
userSelectedDevice = null;
|
||||
|
||||
// If the OS changes the Audio Route or Devices we could have lost
|
||||
// the selected audio device
|
||||
selectedDevice = null;
|
||||
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
|
||||
* when the audio focus of the system is updated.
|
||||
*
|
||||
* @param focusChange - The type of focus change.
|
||||
* Initializes the audio device handler module. This function is called *after* all Catalyst
|
||||
* modules have been created, and that's why we use it, because {@link AudioDeviceHandlerConnectionService}
|
||||
* needs access to another Catalyst module, so doing this in the constructor would be too early.
|
||||
*/
|
||||
@Override
|
||||
public void onAudioFocusChange(int focusChange) {
|
||||
switch (focusChange) {
|
||||
case AudioManager.AUDIOFOCUS_GAIN: {
|
||||
JitsiMeetLogger.d(TAG + " Audio focus gained");
|
||||
// Some other application potentially stole our audio focus
|
||||
// temporarily. Restore our mode.
|
||||
if (audioFocusLost) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
audioFocusLost = false;
|
||||
break;
|
||||
}
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
|
||||
JitsiMeetLogger.d(TAG + " Audio focus lost");
|
||||
audioFocusLost = true;
|
||||
break;
|
||||
public void initialize() {
|
||||
setAudioDeviceHandler();
|
||||
}
|
||||
|
||||
private void setAudioDeviceHandler() {
|
||||
if (audioDeviceHandler != null) {
|
||||
audioDeviceHandler.stop();
|
||||
}
|
||||
|
||||
if (useConnectionService()) {
|
||||
audioDeviceHandler = new AudioDeviceHandlerConnectionService();
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
audioDeviceHandler = new AudioDeviceHandlerGeneric();
|
||||
} else {
|
||||
audioDeviceHandler = new AudioDeviceHandlerLegacy();
|
||||
}
|
||||
|
||||
audioDeviceHandler.start(getReactApplicationContext(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to run operations on a dedicated thread.
|
||||
* @param runnable
|
||||
*/
|
||||
public void runInAudioThread(Runnable runnable) {
|
||||
void runInAudioThread(Runnable runnable) {
|
||||
executor.execute(runnable);
|
||||
}
|
||||
|
||||
@@ -518,46 +250,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The API >= 26 way of adjusting the audio route.
|
||||
*
|
||||
* @param audioDevice one of the "DEVICE_" names to set as the audio route.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void setAudioRoute(String audioDevice) {
|
||||
int newAudioRoute = audioDeviceToRouteInt(audioDevice);
|
||||
|
||||
RNConnectionService.setAudioRoute(newAudioRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* The API < 26 way of adjusting the audio route.
|
||||
*
|
||||
* @param audioDevice one of the "DEVICE_" names to set as the audio route.
|
||||
*/
|
||||
private void setAudioRoutePreO(String audioDevice) {
|
||||
// Turn bluetooth on / off
|
||||
setBluetoothAudioRoute(audioDevice.equals(DEVICE_BLUETOOTH));
|
||||
|
||||
// Turn speaker on / off
|
||||
audioManager.setSpeakerphoneOn(audioDevice.equals(DEVICE_SPEAKER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set the output route to a Bluetooth device.
|
||||
*
|
||||
* @param enabled true if Bluetooth should use used, false otherwise.
|
||||
*/
|
||||
private void setBluetoothAudioRoute(boolean enabled) {
|
||||
if (enabled) {
|
||||
audioManager.startBluetoothSco();
|
||||
audioManager.setBluetoothScoOn(true);
|
||||
} else {
|
||||
audioManager.setBluetoothScoOn(false);
|
||||
audioManager.stopBluetoothSco();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to set the current audio mode.
|
||||
*
|
||||
@@ -587,70 +279,22 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
AudioModeModule.this.mode = mode;
|
||||
promise.resolve(null);
|
||||
} else {
|
||||
promise.reject(
|
||||
"setMode",
|
||||
"Failed to set audio mode to " + mode);
|
||||
promise.reject("setMode", "Failed to set audio mode to " + mode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the audio route change detection mechanism. We use the
|
||||
* {@link android.media.AudioDeviceCallback} on 23 >= Android API < 26.
|
||||
* Sets whether ConnectionService should be used (if available) for setting the audio mode
|
||||
* or not.
|
||||
*
|
||||
* @param use Boolean indicator of where it should be used or not.
|
||||
*/
|
||||
private void setupAudioRouteChangeDetection() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
setupAudioRouteChangeDetectionM();
|
||||
} else {
|
||||
setupAudioRouteChangeDetectionPreM();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio route change detection mechanism for 23 >= Android API < 26.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private void setupAudioRouteChangeDetectionM() {
|
||||
android.media.AudioDeviceCallback audioDeviceCallback =
|
||||
new android.media.AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(
|
||||
AudioDeviceInfo[] addedDevices) {
|
||||
JitsiMeetLogger.d(TAG + " Audio devices added");
|
||||
onAudioDeviceChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(
|
||||
AudioDeviceInfo[] removedDevices) {
|
||||
JitsiMeetLogger.d(TAG + " Audio devices removed");
|
||||
onAudioDeviceChange();
|
||||
}
|
||||
};
|
||||
|
||||
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio route change detection mechanism for Android API < 23.
|
||||
*/
|
||||
private void setupAudioRouteChangeDetectionPreM() {
|
||||
Context context = getReactApplicationContext();
|
||||
|
||||
// Detect changes in wired headset connections.
|
||||
IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG);
|
||||
BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
JitsiMeetLogger.d(TAG + " Wired headset added / removed");
|
||||
onHeadsetDeviceChange();
|
||||
}
|
||||
};
|
||||
context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
|
||||
|
||||
// Detect Bluetooth device changes.
|
||||
bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(this, context);
|
||||
@ReactMethod
|
||||
public void setUseConnectionService(boolean use) {
|
||||
useConnectionService_ = use;
|
||||
setAudioDeviceHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -663,14 +307,11 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
private boolean updateAudioRoute(int mode) {
|
||||
JitsiMeetLogger.i(TAG + " Update audio route for mode: " + mode);
|
||||
|
||||
if (!audioDeviceHandler.setMode(mode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == DEFAULT) {
|
||||
if (!useConnectionService()) {
|
||||
audioFocusLost = false;
|
||||
audioManager.setMode(AudioManager.MODE_NORMAL);
|
||||
audioManager.abandonAudioFocus(this);
|
||||
audioManager.setSpeakerphoneOn(false);
|
||||
setBluetoothAudioRoute(false);
|
||||
}
|
||||
selectedDevice = null;
|
||||
userSelectedDevice = null;
|
||||
|
||||
@@ -678,20 +319,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!useConnectionService()) {
|
||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||
audioManager.setMicrophoneMute(false);
|
||||
|
||||
if (audioManager.requestAudioFocus(
|
||||
this,
|
||||
AudioManager.STREAM_VOICE_CALL,
|
||||
AudioManager.AUDIOFOCUS_GAIN)
|
||||
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
JitsiMeetLogger.w(TAG + " Audio focus request failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
|
||||
boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
|
||||
|
||||
@@ -706,8 +333,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
}
|
||||
|
||||
// Consider the user's selection
|
||||
if (userSelectedDevice != null
|
||||
&& availableDevices.contains(userSelectedDevice)) {
|
||||
if (userSelectedDevice != null && availableDevices.contains(userSelectedDevice)) {
|
||||
audioDevice = userSelectedDevice;
|
||||
}
|
||||
|
||||
@@ -720,13 +346,97 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
selectedDevice = audioDevice;
|
||||
JitsiMeetLogger.i(TAG + " Selected audio device: " + audioDevice);
|
||||
|
||||
if (useConnectionService()) {
|
||||
setAudioRoute(audioDevice);
|
||||
} else {
|
||||
setAudioRoutePreO(audioDevice);
|
||||
}
|
||||
audioDeviceHandler.setAudioRoute(audioDevice);
|
||||
|
||||
notifyDevicesChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently selected audio device.
|
||||
*
|
||||
* @return The selected audio device.
|
||||
*/
|
||||
String getSelectedDevice() {
|
||||
return selectedDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the current device selection.
|
||||
*/
|
||||
void resetSelectedDevice() {
|
||||
selectedDevice = null;
|
||||
userSelectedDevice = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new device to the list of available devices.
|
||||
*
|
||||
* @param device The new device.
|
||||
*/
|
||||
void addDevice(String device) {
|
||||
availableDevices.add(device);
|
||||
resetSelectedDevice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a device from the list of available devices.
|
||||
*
|
||||
* @param device The old device to the removed.
|
||||
*/
|
||||
void removeDevice(String device) {
|
||||
availableDevices.remove(device);
|
||||
resetSelectedDevice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current list of available devices with a new one.
|
||||
*
|
||||
* @param devices The new devices list.
|
||||
*/
|
||||
void replaceDevices(Set<String> devices) {
|
||||
availableDevices = devices;
|
||||
resetSelectedDevice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sets the current audio route. Needed when devices changes have happened.
|
||||
*/
|
||||
void updateAudioRoute() {
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the modules implementing the actual audio device management.
|
||||
*/
|
||||
interface AudioDeviceHandlerInterface {
|
||||
/**
|
||||
* Start detecting audio device changes.
|
||||
* @param context Android {@link Context} where detection should take place.
|
||||
* @param audioModeModule Reference to the main {@link AudioModeModule}.
|
||||
*/
|
||||
void start(Context context, AudioModeModule audioModeModule);
|
||||
|
||||
/**
|
||||
* Stop audio device detection.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Set the appropriate route for the given audio device.
|
||||
*
|
||||
* @param device Audio device for which the route must be set.
|
||||
*/
|
||||
void setAudioRoute(String device);
|
||||
|
||||
/**
|
||||
* Set the given audio mode.
|
||||
*
|
||||
* @param mode The new audio mode to be used.
|
||||
* @return Whether the operation was successful or not.
|
||||
*/
|
||||
boolean setMode(int mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
* Copyright @ 2017-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -33,68 +33,43 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
* about device changes when this occurs.
|
||||
*/
|
||||
class BluetoothHeadsetMonitor {
|
||||
/**
|
||||
* {@link AudioModeModule} where this monitor reports.
|
||||
*/
|
||||
private final AudioModeModule audioModeModule;
|
||||
private final static String TAG = BluetoothHeadsetMonitor.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The {@link Context} in which {@link #audioModeModule} executes.
|
||||
* The {@link Context} in which this module executes.
|
||||
*/
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Reference to the {@link BluetoothAdapter} object, used to access Bluetooth functionality.
|
||||
*/
|
||||
private BluetoothAdapter adapter;
|
||||
|
||||
/**
|
||||
* Reference to a proxy object which allows us to query connected devices.
|
||||
*/
|
||||
private BluetoothHeadset headset;
|
||||
|
||||
/**
|
||||
* Flag indicating if there are any Bluetooth headset devices currently
|
||||
* available.
|
||||
* receiver registered for receiving Bluetooth connection state changes.
|
||||
*/
|
||||
private boolean headsetAvailable = false;
|
||||
private BroadcastReceiver receiver;
|
||||
|
||||
/**
|
||||
* Helper for running Bluetooth operations on the main thread.
|
||||
* Listener for receiving Bluetooth device change events.
|
||||
*/
|
||||
private final Runnable updateDevicesRunnable
|
||||
= new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
headsetAvailable
|
||||
= (headset != null)
|
||||
&& !headset.getConnectedDevices().isEmpty();
|
||||
audioModeModule.onBluetoothDeviceChange();
|
||||
}
|
||||
};
|
||||
private Listener listener;
|
||||
|
||||
public BluetoothHeadsetMonitor(
|
||||
AudioModeModule audioModeModule,
|
||||
Context context) {
|
||||
this.audioModeModule = audioModeModule;
|
||||
public BluetoothHeadsetMonitor(Context context, Listener listener) {
|
||||
this.context = context;
|
||||
|
||||
AudioManager audioManager
|
||||
= (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
if (!audioManager.isBluetoothScoAvailableOffCall()) {
|
||||
JitsiMeetLogger.w(AudioModeModule.TAG + " Bluetooth SCO is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (getBluetoothHeadsetProfileProxy()) {
|
||||
registerBluetoothReceiver();
|
||||
|
||||
// Initial detection.
|
||||
updateDevices();
|
||||
}
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private boolean getBluetoothHeadsetProfileProxy() {
|
||||
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
|
||||
if (adapter == null) {
|
||||
JitsiMeetLogger.w(AudioModeModule.TAG + " Device doesn't support Bluetooth");
|
||||
JitsiMeetLogger.w(TAG + " Device doesn't support Bluetooth");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -104,9 +79,7 @@ class BluetoothHeadsetMonitor {
|
||||
BluetoothProfile.ServiceListener listener
|
||||
= new BluetoothProfile.ServiceListener() {
|
||||
@Override
|
||||
public void onServiceConnected(
|
||||
int profile,
|
||||
BluetoothProfile proxy) {
|
||||
public void onServiceConnected(int profile, BluetoothProfile proxy) {
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
headset = (BluetoothHeadset) proxy;
|
||||
updateDevices();
|
||||
@@ -120,21 +93,7 @@ class BluetoothHeadsetMonitor {
|
||||
}
|
||||
};
|
||||
|
||||
return
|
||||
adapter.getProfileProxy(
|
||||
context,
|
||||
listener,
|
||||
BluetoothProfile.HEADSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current headset availability.
|
||||
*
|
||||
* @return {@code true} if there is a Bluetooth headset connected;
|
||||
* {@code false}, otherwise.
|
||||
*/
|
||||
public boolean isHeadsetAvailable() {
|
||||
return headsetAvailable;
|
||||
return adapter.getProfileProxy(context, listener, BluetoothProfile.HEADSET);
|
||||
}
|
||||
|
||||
private void onBluetoothReceiverReceive(Context context, Intent intent) {
|
||||
@@ -149,7 +108,7 @@ class BluetoothHeadsetMonitor {
|
||||
switch (state) {
|
||||
case BluetoothHeadset.STATE_CONNECTED:
|
||||
case BluetoothHeadset.STATE_DISCONNECTED:
|
||||
JitsiMeetLogger.d(AudioModeModule.TAG + " BT headset connection state changed: " + state);
|
||||
JitsiMeetLogger.d(TAG + " BT headset connection state changed: " + state);
|
||||
updateDevices();
|
||||
break;
|
||||
}
|
||||
@@ -157,13 +116,12 @@ class BluetoothHeadsetMonitor {
|
||||
// XXX: This action will be fired when the connection established
|
||||
// with a Bluetooth headset (called a SCO connection) changes state.
|
||||
// When the SCO connection is active we route audio to it.
|
||||
int state
|
||||
= intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
|
||||
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
|
||||
|
||||
switch (state) {
|
||||
case AudioManager.SCO_AUDIO_STATE_CONNECTED:
|
||||
case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
|
||||
JitsiMeetLogger.d(AudioModeModule.TAG + " BT SCO connection state changed: " + state);
|
||||
JitsiMeetLogger.d(TAG + " BT SCO connection state changed: " + state);
|
||||
updateDevices();
|
||||
break;
|
||||
}
|
||||
@@ -171,24 +129,63 @@ class BluetoothHeadsetMonitor {
|
||||
}
|
||||
|
||||
private void registerBluetoothReceiver() {
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
onBluetoothReceiverReceive(context, intent);
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter();
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
|
||||
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
|
||||
|
||||
context.registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if there are new devices connected / disconnected and fires the
|
||||
* {@link AudioModeModule#onAudioDeviceChange()} callback.
|
||||
* {@link Listener} registered event.
|
||||
*/
|
||||
private void updateDevices() {
|
||||
audioModeModule.runInAudioThread(updateDevicesRunnable);
|
||||
boolean headsetAvailable = (headset != null) && !headset.getConnectedDevices().isEmpty();
|
||||
listener.onBluetoothDeviceChange(headsetAvailable);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
if (!audioManager.isBluetoothScoAvailableOffCall()) {
|
||||
JitsiMeetLogger.w(TAG + " Bluetooth SCO is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getBluetoothHeadsetProfileProxy()) {
|
||||
JitsiMeetLogger.w(TAG + " Couldn't get BT profile proxy");
|
||||
return;
|
||||
}
|
||||
|
||||
registerBluetoothReceiver();
|
||||
|
||||
// Initial detection.
|
||||
updateDevices();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (receiver != null) {
|
||||
context.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
if (adapter != null && headset != null) {
|
||||
adapter.closeProfileProxy(BluetoothProfile.HEADSET, headset);
|
||||
}
|
||||
|
||||
receiver = null;
|
||||
headset = null;
|
||||
adapter = null;
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void onBluetoothDeviceChange(boolean deviceAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,14 +123,17 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
* {@link android.telecom.Connection#STATE_ACTIVE}.
|
||||
*
|
||||
* @param callUUID the call UUID which identifies the connection.
|
||||
* @return Whether the connection was set as active or not.
|
||||
*/
|
||||
static void setConnectionActive(String callUUID) {
|
||||
static boolean setConnectionActive(String callUUID) {
|
||||
ConnectionImpl connection = connections.get(callUUID);
|
||||
|
||||
if (connection != null) {
|
||||
connection.setActive();
|
||||
return true;
|
||||
} else {
|
||||
JitsiMeetLogger.e("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
|
||||
JitsiMeetLogger.w("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,11 +406,9 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
@Override
|
||||
public void onCallAudioStateChanged(CallAudioState state) {
|
||||
JitsiMeetLogger.d(TAG + " onCallAudioStateChanged: " + state);
|
||||
AudioModeModule audioModeModule
|
||||
= ReactInstanceManagerHolder
|
||||
.getNativeModule(AudioModeModule.class);
|
||||
if (audioModeModule != null) {
|
||||
audioModeModule.onCallAudioStateChange(state);
|
||||
RNConnectionService module = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
|
||||
if (module != null) {
|
||||
module.onCallAudioStateChange(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
urlProps.putString("jwt", token);
|
||||
}
|
||||
|
||||
if (token == null && userInfo != null) {
|
||||
if (userInfo != null) {
|
||||
props.putBundle("userInfo", userInfo.asBundle());
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,18 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@ReactModule(name = RNConnectionService.NAME)
|
||||
class RNConnectionService
|
||||
extends ReactContextBaseJavaModule {
|
||||
class RNConnectionService extends ReactContextBaseJavaModule {
|
||||
|
||||
public static final String NAME = "ConnectionService";
|
||||
|
||||
private static final String TAG = ConnectionService.TAG;
|
||||
|
||||
/**
|
||||
* Handler for dealing with call state changes. We are acting as a proxy between ConnectionService
|
||||
* and other modules such as {@link AudioModeModule}.
|
||||
*/
|
||||
private CallAudioStateListener callAudioStateListener;
|
||||
|
||||
/**
|
||||
* Sets the audio route on all existing {@link android.telecom.Connection}s
|
||||
*
|
||||
@@ -84,9 +89,17 @@ class RNConnectionService
|
||||
ReactApplicationContext ctx = getReactApplicationContext();
|
||||
|
||||
Uri address = Uri.fromParts(PhoneAccount.SCHEME_SIP, handle, null);
|
||||
PhoneAccountHandle accountHandle
|
||||
= ConnectionService.registerPhoneAccount(
|
||||
getReactApplicationContext(), address, callUUID);
|
||||
PhoneAccountHandle accountHandle;
|
||||
|
||||
try {
|
||||
accountHandle
|
||||
= ConnectionService.registerPhoneAccount(getReactApplicationContext(), address, callUUID);
|
||||
} catch (Throwable tr) {
|
||||
JitsiMeetLogger.e(tr, TAG + " error in startCall");
|
||||
|
||||
promise.reject(tr);
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(
|
||||
@@ -100,15 +113,23 @@ class RNConnectionService
|
||||
|
||||
ConnectionService.registerStartCallPromise(callUUID, promise);
|
||||
|
||||
try {
|
||||
TelecomManager tm
|
||||
= (TelecomManager) ctx.getSystemService(
|
||||
Context.TELECOM_SERVICE);
|
||||
TelecomManager tm = null;
|
||||
|
||||
try {
|
||||
tm = (TelecomManager) ctx.getSystemService(Context.TELECOM_SERVICE);
|
||||
tm.placeCall(address, extras);
|
||||
} catch (Exception e) {
|
||||
} catch (Throwable tr) {
|
||||
JitsiMeetLogger.e(tr, TAG + " error in startCall");
|
||||
if (tm != null) {
|
||||
try {
|
||||
tm.unregisterPhoneAccount(accountHandle);
|
||||
} catch (Throwable tr1) {
|
||||
// UnsupportedOperationException: System does not support feature android.software.connectionservice
|
||||
// was observed here. Ignore.
|
||||
}
|
||||
}
|
||||
ConnectionService.unregisterStartCallPromise(callUUID);
|
||||
promise.reject(e);
|
||||
promise.reject(tr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +167,11 @@ class RNConnectionService
|
||||
@ReactMethod
|
||||
public void reportConnectedOutgoingCall(String callUUID, Promise promise) {
|
||||
JitsiMeetLogger.d(TAG + " reportConnectedOutgoingCall " + callUUID);
|
||||
ConnectionService.setConnectionActive(callUUID);
|
||||
promise.resolve(null);
|
||||
if (ConnectionService.setConnectionActive(callUUID)) {
|
||||
promise.resolve(null);
|
||||
} else {
|
||||
promise.reject("CONNECTION_NOT_FOUND_ERROR", "Connection wasn't found.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -168,4 +192,28 @@ class RNConnectionService
|
||||
public void updateCall(String callUUID, ReadableMap callState) {
|
||||
ConnectionService.updateCall(callUUID, callState);
|
||||
}
|
||||
|
||||
public CallAudioStateListener getCallAudioStateListener() {
|
||||
return callAudioStateListener;
|
||||
}
|
||||
|
||||
public void setCallAudioStateListener(CallAudioStateListener callAudioStateListener) {
|
||||
this.callAudioStateListener = callAudioStateListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for call state changes. {@code ConnectionServiceImpl} will call this handler when the
|
||||
* call audio state changes.
|
||||
*
|
||||
* @param callAudioState The current call's audio state.
|
||||
*/
|
||||
void onCallAudioStateChange(android.telecom.CallAudioState callAudioState) {
|
||||
if (callAudioStateListener != null) {
|
||||
callAudioStateListener.onCallAudioStateChange(callAudioState);
|
||||
}
|
||||
}
|
||||
|
||||
interface CallAudioStateListener {
|
||||
void onCallAudioStateChange(android.telecom.CallAudioState callAudioState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
* Copyright @ 2017-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +19,7 @@ package org.jitsi.meet.sdk;
|
||||
import android.app.Activity;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
@@ -27,14 +27,16 @@ import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.common.LifecycleState;
|
||||
import com.facebook.react.devsupport.DevInternalSettings;
|
||||
import com.facebook.react.jscexecutor.JSCExecutorFactory;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.oney.WebRTCModule.EglUtils;
|
||||
import com.oney.WebRTCModule.RTCVideoViewManager;
|
||||
import com.oney.WebRTCModule.WebRTCModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
import org.webrtc.DefaultVideoDecoderFactory;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.SoftwareVideoDecoderFactory;
|
||||
import org.webrtc.SoftwareVideoEncoderFactory;
|
||||
import org.webrtc.VideoDecoderFactory;
|
||||
@@ -85,10 +87,19 @@ class ReactInstanceManagerHolder {
|
||||
|
||||
AudioDeviceModule adm = JavaAudioDeviceModule.builder(reactContext)
|
||||
.createAudioDeviceModule();
|
||||
VideoDecoderFactory videoDecoderFactory = new SoftwareVideoDecoderFactory();
|
||||
VideoEncoderFactory videoEncoderFactory = new SoftwareVideoEncoderFactory();
|
||||
|
||||
options.setAudioDeviceModule(adm);
|
||||
|
||||
VideoEncoderFactory videoEncoderFactory = new SoftwareVideoEncoderFactory();
|
||||
VideoDecoderFactory videoDecoderFactory;
|
||||
// Initialize EGL context required for HW acceleration. We are only going to use it for
|
||||
// decoding.
|
||||
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
|
||||
if (eglContext == null) {
|
||||
// Fallback to the software decoder.
|
||||
videoDecoderFactory = new SoftwareVideoDecoderFactory();
|
||||
} else {
|
||||
videoDecoderFactory = new DefaultVideoDecoderFactory(eglContext);
|
||||
}
|
||||
options.setVideoDecoderFactory(videoDecoderFactory);
|
||||
options.setVideoEncoderFactory(videoEncoderFactory);
|
||||
|
||||
@@ -219,9 +230,8 @@ class ReactInstanceManagerHolder {
|
||||
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
|
||||
}
|
||||
|
||||
// Keep on using JSC, the jury is out on Hermes.
|
||||
JSCExecutorFactory jsFactory
|
||||
= new JSCExecutorFactory("", "");
|
||||
// Use the Hermes JavaScript engine.
|
||||
HermesExecutorFactory jsFactory = new HermesExecutorFactory();
|
||||
|
||||
reactInstanceManager
|
||||
= ReactInstanceManager.builder()
|
||||
|
||||
@@ -24,11 +24,24 @@ public class JitsiMeetLogger {
|
||||
}
|
||||
|
||||
public static void addHandler(JitsiMeetBaseLogHandler handler) {
|
||||
Timber.plant(handler);
|
||||
if (!Timber.forest().contains(handler)) {
|
||||
try {
|
||||
Timber.plant(handler);
|
||||
} catch (Throwable t) {
|
||||
Timber.w(t, "Couldn't add log handler");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeHandler(JitsiMeetBaseLogHandler handler) {
|
||||
Timber.uproot(handler);
|
||||
if (Timber.forest().contains(handler)) {
|
||||
try {
|
||||
Timber.uproot(handler);
|
||||
} catch (Throwable t) {
|
||||
Timber.w(t, "Couldn't remove log handler");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void v(String message, Object... args) {
|
||||
|
||||
385
conference.js
@@ -41,6 +41,7 @@ import {
|
||||
conferenceJoined,
|
||||
conferenceLeft,
|
||||
conferenceSubjectChanged,
|
||||
conferenceTimestampChanged,
|
||||
conferenceWillJoin,
|
||||
conferenceWillLeave,
|
||||
dataChannelOpened,
|
||||
@@ -93,10 +94,15 @@ import {
|
||||
participantRoleChanged,
|
||||
participantUpdated
|
||||
} from './react/features/base/participants';
|
||||
import { updateSettings } from './react/features/base/settings';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
updateSettings
|
||||
} from './react/features/base/settings';
|
||||
import {
|
||||
createLocalPresenterTrack,
|
||||
createLocalTracksF,
|
||||
destroyLocalTracks,
|
||||
isLocalVideoTrackMuted,
|
||||
isLocalTrackMuted,
|
||||
isUserInteractionRequiredForUnmute,
|
||||
replaceLocalTrack,
|
||||
@@ -113,7 +119,10 @@ import {
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
|
||||
import { suspendDetected } from './react/features/power-monitor';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
||||
import { endpointMessageReceived } from './react/features/subtitles';
|
||||
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
|
||||
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -122,6 +131,15 @@ const eventEmitter = new EventEmitter();
|
||||
let room;
|
||||
let connection;
|
||||
|
||||
/**
|
||||
* This promise is used for chaining mutePresenterVideo calls in order to avoid calling GUM multiple times if it takes
|
||||
* a while to finish.
|
||||
*
|
||||
* @type {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
let _prevMutePresenterVideo = Promise.resolve();
|
||||
|
||||
/*
|
||||
* Logic to open a desktop picker put on the window global for
|
||||
* lib-jitsi-meet to detect and invoke
|
||||
@@ -417,7 +435,6 @@ export default {
|
||||
* the tracks won't exist).
|
||||
*/
|
||||
_localTracksInitialized: false,
|
||||
isModerator: false,
|
||||
isSharingScreen: false,
|
||||
|
||||
/**
|
||||
@@ -437,6 +454,12 @@ export default {
|
||||
*/
|
||||
localAudio: null,
|
||||
|
||||
/**
|
||||
* The local presenter video track (if any).
|
||||
* @type {JitsiLocalTrack|null}
|
||||
*/
|
||||
localPresenterVideo: null,
|
||||
|
||||
/**
|
||||
* The local video track (if any).
|
||||
* FIXME tracks from redux store should be the single source of truth, but
|
||||
@@ -468,14 +491,18 @@ export default {
|
||||
audioOnlyError,
|
||||
screenSharingError,
|
||||
videoOnlyError;
|
||||
const initialDevices = [];
|
||||
let requestedAudio = false;
|
||||
const initialDevices = [ 'audio' ];
|
||||
const requestedAudio = true;
|
||||
let requestedVideo = false;
|
||||
|
||||
if (!options.startWithAudioMuted) {
|
||||
initialDevices.push('audio');
|
||||
requestedAudio = true;
|
||||
// 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 (options.startWithAudioMuted) {
|
||||
this.muteAudio(true, true);
|
||||
}
|
||||
|
||||
if (!options.startWithVideoMuted
|
||||
&& !options.startAudioOnly
|
||||
&& !options.startScreenSharing) {
|
||||
@@ -722,9 +749,8 @@ export default {
|
||||
isLocalVideoMuted() {
|
||||
// If the tracks are not ready, read from base/media state
|
||||
return this._localTracksInitialized
|
||||
? isLocalTrackMuted(
|
||||
APP.store.getState()['features/base/tracks'],
|
||||
MEDIA_TYPE.VIDEO)
|
||||
? isLocalVideoTrackMuted(
|
||||
APP.store.getState()['features/base/tracks'])
|
||||
: isVideoMutedByUser(APP.store);
|
||||
},
|
||||
|
||||
@@ -798,6 +824,35 @@ export default {
|
||||
this.muteAudio(!this.isLocalAudioMuted(), showUI);
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates toolbar button click for presenter video mute. Used by
|
||||
* shortcuts and API.
|
||||
* @param mute true for mute and false for unmute.
|
||||
* @param {boolean} [showUI] when set to false will not display any error
|
||||
* dialogs in case of media permissions error.
|
||||
*/
|
||||
async mutePresenter(mute, showUI = true) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.store.dispatch(notifyCameraError(error));
|
||||
};
|
||||
|
||||
if (mute) {
|
||||
try {
|
||||
await this.localVideo.setEffect(undefined);
|
||||
} catch (err) {
|
||||
logger.error('Failed to remove the presenter effect', err);
|
||||
maybeShowErrorDialog(err);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await this.localVideo.setEffect(await this._createPresenterStreamEffect());
|
||||
} catch (err) {
|
||||
logger.error('Failed to apply the presenter effect', err);
|
||||
maybeShowErrorDialog(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
||||
* @param mute true for mute and false for unmute.
|
||||
@@ -812,6 +867,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSharingScreen) {
|
||||
// Chain _mutePresenterVideo calls
|
||||
_prevMutePresenterVideo = _prevMutePresenterVideo.then(() => this._mutePresenterVideo(mute));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If not ready to modify track's state yet adjust the base/media
|
||||
if (!this._localTracksInitialized) {
|
||||
// This will only modify base/media.video.muted which is then synced
|
||||
@@ -863,14 +925,6 @@ export default {
|
||||
this.muteVideo(!this.isLocalVideoMuted(), showUI);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve list of conference participants (without local user).
|
||||
* @returns {JitsiParticipant[]}
|
||||
*/
|
||||
listMembers() {
|
||||
return room.getParticipants();
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve list of ids of conference participants (without local user).
|
||||
* @returns {string[]}
|
||||
@@ -1205,17 +1259,22 @@ export default {
|
||||
|
||||
_getConferenceOptions() {
|
||||
const options = config;
|
||||
const { email, name: nick } = getLocalParticipant(APP.store.getState());
|
||||
|
||||
const nick = APP.store.getState()['features/base/settings'].displayName;
|
||||
const { locationURL } = APP.store.getState()['features/base/connection'];
|
||||
|
||||
if (nick) {
|
||||
options.displayName = nick;
|
||||
if (options.enableDisplayNameInStats && nick) {
|
||||
options.statisticsDisplayName = nick;
|
||||
}
|
||||
|
||||
if (options.enableEmailInStats && email) {
|
||||
options.statisticsId = email;
|
||||
}
|
||||
|
||||
options.applicationName = interfaceConfig.APP_NAME;
|
||||
options.getWiFiStatsMethod = this._getWiFiStatsMethod;
|
||||
options.confID = `${locationURL.host}${locationURL.pathname}`;
|
||||
options.createVADProcessor = createRnnoiseProcessorPromise;
|
||||
|
||||
return options;
|
||||
},
|
||||
@@ -1347,7 +1406,7 @@ export default {
|
||||
* in case it fails.
|
||||
* @private
|
||||
*/
|
||||
_turnScreenSharingOff(didHaveVideo, wasVideoMuted) {
|
||||
_turnScreenSharingOff(didHaveVideo) {
|
||||
this._untoggleScreenSharing = null;
|
||||
this.videoSwitchInProgress = true;
|
||||
const { receiver } = APP.remoteControl;
|
||||
@@ -1358,20 +1417,27 @@ export default {
|
||||
|
||||
this._stopProxyConnection();
|
||||
|
||||
let promise = null;
|
||||
// It can happen that presenter GUM is in progress while screensharing is being turned off. Here it needs to
|
||||
// wait for that GUM to be resolved in order to prevent leaking the presenter track(this.localPresenterVideo
|
||||
// will be null when SS is being turned off, but it will initialize once GUM resolves).
|
||||
let promise = _prevMutePresenterVideo = _prevMutePresenterVideo.then(() => {
|
||||
// mute the presenter track if it exists.
|
||||
if (this.localPresenterVideo) {
|
||||
APP.store.dispatch(setVideoMuted(true, MEDIA_TYPE.PRESENTER));
|
||||
|
||||
return this.localPresenterVideo.dispose().then(() => {
|
||||
APP.store.dispatch(trackRemoved(this.localPresenterVideo));
|
||||
this.localPresenterVideo = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (didHaveVideo) {
|
||||
promise = createLocalTracksF({ devices: [ 'video' ] })
|
||||
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
|
||||
.then(([ stream ]) => this.useVideoStream(stream))
|
||||
.then(() => {
|
||||
sendAnalytics(createScreenSharingEvent('stopped'));
|
||||
logger.log('Screen sharing stopped, switching to video.');
|
||||
|
||||
if (!this.localVideo && wasVideoMuted) {
|
||||
return Promise.reject('No local video to be muted!');
|
||||
} else if (wasVideoMuted && this.localVideo) {
|
||||
return this.localVideo.mute();
|
||||
}
|
||||
logger.log('Screen sharing stopped.');
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('failed to switch back to local video', error);
|
||||
@@ -1383,7 +1449,10 @@ export default {
|
||||
);
|
||||
});
|
||||
} else {
|
||||
promise = this.useVideoStream(null);
|
||||
promise = promise.then(() => this.useVideoStream(null));
|
||||
}
|
||||
if (config.enableScreenshotCapture) {
|
||||
APP.store.dispatch(toggleScreenshotCaptureEffect(false));
|
||||
}
|
||||
|
||||
return promise.then(
|
||||
@@ -1411,7 +1480,7 @@ export default {
|
||||
* 'window', etc.).
|
||||
* @return {Promise.<T>}
|
||||
*/
|
||||
toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
|
||||
async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
|
||||
if (this.videoSwitchInProgress) {
|
||||
return Promise.reject('Switch in progress.');
|
||||
}
|
||||
@@ -1425,7 +1494,15 @@ export default {
|
||||
}
|
||||
|
||||
if (toggle) {
|
||||
return this._switchToScreenSharing(options);
|
||||
try {
|
||||
await this._switchToScreenSharing(options);
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
logger.error('Failed to switch to screensharing', err);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return this._untoggleScreenSharing
|
||||
@@ -1450,8 +1527,7 @@ export default {
|
||||
_createDesktopTrack(options = {}) {
|
||||
let externalInstallation = false;
|
||||
let DSExternalInstallationInProgress = false;
|
||||
const didHaveVideo = Boolean(this.localVideo);
|
||||
const wasVideoMuted = this.isLocalVideoMuted();
|
||||
const didHaveVideo = !this.isLocalVideoMuted();
|
||||
|
||||
const getDesktopStreamPromise = options.desktopStream
|
||||
? Promise.resolve([ options.desktopStream ])
|
||||
@@ -1502,8 +1578,7 @@ export default {
|
||||
// Stores the "untoggle" handler which remembers whether was
|
||||
// there any video before and whether was it muted.
|
||||
this._untoggleScreenSharing
|
||||
= this._turnScreenSharingOff
|
||||
.bind(this, didHaveVideo, wasVideoMuted);
|
||||
= this._turnScreenSharingOff.bind(this, didHaveVideo);
|
||||
desktopStream.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => {
|
||||
@@ -1528,6 +1603,126 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new instance of presenter effect. A new video track is created
|
||||
* using the new set of constraints that are calculated based on
|
||||
* the height of the desktop that is being currently shared.
|
||||
*
|
||||
* @param {number} height - The height of the desktop stream that is being
|
||||
* currently shared.
|
||||
* @param {string} cameraDeviceId - The device id of the camera to be used.
|
||||
* @return {Promise<JitsiStreamPresenterEffect>} - A promise resolved with
|
||||
* {@link JitsiStreamPresenterEffect} if it succeeds.
|
||||
*/
|
||||
async _createPresenterStreamEffect(height = null, cameraDeviceId = null) {
|
||||
if (!this.localPresenterVideo) {
|
||||
try {
|
||||
this.localPresenterVideo = await createLocalPresenterTrack({ cameraDeviceId }, height);
|
||||
} catch (err) {
|
||||
logger.error('Failed to create a camera track for presenter', err);
|
||||
|
||||
return;
|
||||
}
|
||||
APP.store.dispatch(trackAdded(this.localPresenterVideo));
|
||||
}
|
||||
try {
|
||||
const effect = await createPresenterEffect(this.localPresenterVideo.stream);
|
||||
|
||||
return effect;
|
||||
} catch (err) {
|
||||
logger.error('Failed to create the presenter effect', err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Tries to turn the presenter video track on or off. If a presenter track
|
||||
* doesn't exist, a new video track is created.
|
||||
*
|
||||
* @param mute - true for mute and false for unmute.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _mutePresenterVideo(mute) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
APP.store.dispatch(notifyCameraError(error));
|
||||
};
|
||||
|
||||
// Check for NO-OP
|
||||
if (mute && (!this.localPresenterVideo || this.localPresenterVideo.isMuted())) {
|
||||
|
||||
return;
|
||||
} else if (!mute && this.localPresenterVideo && !this.localPresenterVideo.isMuted()) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new presenter track and apply the presenter effect.
|
||||
if (!this.localPresenterVideo && !mute) {
|
||||
let { aspectRatio, height } = this.localVideo.track.getSettings();
|
||||
const { width } = this.localVideo.track.getSettings();
|
||||
let desktopResizeConstraints = {};
|
||||
let resizeDesktopStream = false;
|
||||
const DESKTOP_STREAM_CAP = 720;
|
||||
|
||||
// Determine the constraints if the desktop track needs to be resized.
|
||||
// Resizing is needed when the resolution cannot be determined or when
|
||||
// the window is bigger than 720p.
|
||||
if (height && width) {
|
||||
aspectRatio = aspectRatio ?? (width / height).toPrecision(4);
|
||||
const advancedConstraints = [ { aspectRatio } ];
|
||||
const isPortrait = height >= width;
|
||||
|
||||
// Determine which dimension needs resizing and resize only that side
|
||||
// keeping the aspect ratio same as before.
|
||||
if (isPortrait && width > DESKTOP_STREAM_CAP) {
|
||||
resizeDesktopStream = true;
|
||||
advancedConstraints.push({ width: DESKTOP_STREAM_CAP });
|
||||
} else if (!isPortrait && height > DESKTOP_STREAM_CAP) {
|
||||
resizeDesktopStream = true;
|
||||
advancedConstraints.push({ height: DESKTOP_STREAM_CAP });
|
||||
}
|
||||
desktopResizeConstraints.advanced = advancedConstraints;
|
||||
} else {
|
||||
resizeDesktopStream = true;
|
||||
desktopResizeConstraints = {
|
||||
width: 1280,
|
||||
height: 720
|
||||
};
|
||||
}
|
||||
if (resizeDesktopStream) {
|
||||
try {
|
||||
await this.localVideo.track.applyConstraints(desktopResizeConstraints);
|
||||
} catch (err) {
|
||||
logger.error('Failed to apply constraints on the desktop stream for presenter mode', err);
|
||||
|
||||
return;
|
||||
}
|
||||
height = this.localVideo.track.getSettings().height ?? DESKTOP_STREAM_CAP;
|
||||
}
|
||||
const defaultCamera = getUserSelectedCameraDeviceId(APP.store.getState());
|
||||
let effect;
|
||||
|
||||
try {
|
||||
effect = await this._createPresenterStreamEffect(height,
|
||||
defaultCamera);
|
||||
} catch (err) {
|
||||
logger.error('Failed to unmute Presenter Video');
|
||||
maybeShowErrorDialog(err);
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.localVideo.setEffect(effect);
|
||||
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
|
||||
this.setVideoMuteStatus(mute);
|
||||
} catch (err) {
|
||||
logger.error('Failed to apply the Presenter effect', err);
|
||||
}
|
||||
} else {
|
||||
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Tries to switch to the screensharing mode by disposing camera stream and
|
||||
* replacing it with a desktop one.
|
||||
@@ -1552,6 +1747,9 @@ export default {
|
||||
.then(stream => this.useVideoStream(stream))
|
||||
.then(() => {
|
||||
this.videoSwitchInProgress = false;
|
||||
if (config.enableScreenshotCapture) {
|
||||
APP.store.dispatch(toggleScreenshotCaptureEffect(true));
|
||||
}
|
||||
sendAnalytics(createScreenSharingEvent('started'));
|
||||
logger.log('Screen sharing started');
|
||||
})
|
||||
@@ -1665,7 +1863,10 @@ export default {
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.CONFERENCE_LEFT,
|
||||
(...args) => APP.store.dispatch(conferenceLeft(room, ...args)));
|
||||
(...args) => {
|
||||
APP.store.dispatch(conferenceTimestampChanged(0));
|
||||
APP.store.dispatch(conferenceLeft(room, ...args));
|
||||
});
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
|
||||
@@ -1684,9 +1885,6 @@ export default {
|
||||
|
||||
logger.log(`USER ${id} connnected:`, user);
|
||||
APP.UI.addUser(user);
|
||||
|
||||
// check the roles for the new user and reflect them
|
||||
APP.UI.updateUserRole(user);
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
|
||||
@@ -1717,19 +1915,8 @@ export default {
|
||||
logger.info(`My role changed, new role: ${role}`);
|
||||
|
||||
APP.store.dispatch(localParticipantRoleChanged(role));
|
||||
|
||||
if (this.isModerator !== room.isModerator()) {
|
||||
this.isModerator = room.isModerator();
|
||||
APP.UI.updateLocalRole(room.isModerator());
|
||||
}
|
||||
} else {
|
||||
APP.store.dispatch(participantRoleChanged(id, role));
|
||||
|
||||
const user = room.getParticipantById(id);
|
||||
|
||||
if (user) {
|
||||
APP.UI.updateUserRole(user);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1795,6 +1982,10 @@ export default {
|
||||
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
||||
id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
|
||||
conferenceTimestamp => APP.store.dispatch(conferenceTimestampChanged(conferenceTimestamp)));
|
||||
|
||||
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
||||
APP.store.dispatch(localParticipantConnectionStatusChanged(
|
||||
JitsiParticipantConnectionStatus.INTERRUPTED));
|
||||
@@ -1988,36 +2179,65 @@ export default {
|
||||
const videoWasMuted = this.isLocalVideoMuted();
|
||||
|
||||
sendAnalytics(createDeviceChangedEvent('video', 'input'));
|
||||
createLocalTracksF({
|
||||
devices: [ 'video' ],
|
||||
cameraDeviceId,
|
||||
micDeviceId: null
|
||||
})
|
||||
.then(([ stream ]) => {
|
||||
// if we are in audio only mode or video was muted before
|
||||
// changing device, then mute
|
||||
if (this.isAudioOnly() || videoWasMuted) {
|
||||
return stream.mute()
|
||||
.then(() => stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
})
|
||||
.then(stream => {
|
||||
// if we are screen sharing we do not want to stop it
|
||||
if (this.isSharingScreen) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// If both screenshare and video are in progress, restart the
|
||||
// presenter mode with the new camera device.
|
||||
if (this.isSharingScreen && !videoWasMuted) {
|
||||
const { height } = this.localVideo.track.getSettings();
|
||||
|
||||
return this.useVideoStream(stream);
|
||||
})
|
||||
.then(() => {
|
||||
// dispose the existing presenter track and create a new
|
||||
// camera track.
|
||||
// FIXME JitsiLocalTrack.dispose is async and should be waited for
|
||||
this.localPresenterVideo && this.localPresenterVideo.dispose();
|
||||
this.localPresenterVideo = null;
|
||||
|
||||
return this._createPresenterStreamEffect(height, cameraDeviceId)
|
||||
.then(effect => this.localVideo.setEffect(effect))
|
||||
.then(() => {
|
||||
this.setVideoMuteStatus(false);
|
||||
logger.log('switched local video device');
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => APP.store.dispatch(notifyCameraError(err)));
|
||||
|
||||
// If screenshare is in progress but video is muted, update the default device
|
||||
// id for video, dispose the existing presenter track and create a new effect
|
||||
// that can be applied on un-mute.
|
||||
} else if (this.isSharingScreen && videoWasMuted) {
|
||||
logger.log('switched local video device');
|
||||
const { height } = this.localVideo.track.getSettings();
|
||||
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => {
|
||||
APP.store.dispatch(notifyCameraError(err));
|
||||
});
|
||||
|
||||
// FIXME JitsiLocalTrack.dispose is async and should be waited for
|
||||
this.localPresenterVideo && this.localPresenterVideo.dispose();
|
||||
this.localPresenterVideo = null;
|
||||
this._createPresenterStreamEffect(height, cameraDeviceId);
|
||||
|
||||
// if there is only video, switch to the new camera stream.
|
||||
} else {
|
||||
createLocalTracksF({
|
||||
devices: [ 'video' ],
|
||||
cameraDeviceId,
|
||||
micDeviceId: null
|
||||
})
|
||||
.then(([ stream ]) => {
|
||||
// if we are in audio only mode or video was muted before
|
||||
// changing device, then mute
|
||||
if (this.isAudioOnly() || videoWasMuted) {
|
||||
return stream.mute()
|
||||
.then(() => stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
})
|
||||
.then(stream => this.useVideoStream(stream))
|
||||
.then(() => {
|
||||
logger.log('switched local video device');
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => APP.store.dispatch(notifyCameraError(err)));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2247,6 +2467,13 @@ export default {
|
||||
cameraDeviceId: this.localVideo.getDeviceId()
|
||||
}));
|
||||
}
|
||||
|
||||
// If screenshare is in progress, get the device id from the presenter track.
|
||||
if (this.localPresenterVideo) {
|
||||
APP.store.dispatch(updateSettings({
|
||||
cameraDeviceId: this.localPresenterVideo.getDeviceId()
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
48
config.js
@@ -30,6 +30,9 @@ var config = {
|
||||
// BOSH URL. FIXME: use XEP-0156 to discover it.
|
||||
bosh: '//jitsi-meet.example.com/http-bind',
|
||||
|
||||
// Websocket URL
|
||||
// websocket: 'wss://jitsi-meet.example.com/xmpp-websocket',
|
||||
|
||||
// The name of client node advertised in XEP-0115 'c' stanza
|
||||
clientNode: 'http://jitsi.org/jitsimeet',
|
||||
|
||||
@@ -73,6 +76,17 @@ var config = {
|
||||
// Disable measuring of audio levels.
|
||||
// disableAudioLevels: false,
|
||||
|
||||
// Enabling this will run the lib-jitsi-meet no audio detection module which
|
||||
// will notify the user if the current selected microphone has no audio
|
||||
// input and will suggest another valid device if one is present.
|
||||
enableNoAudioDetection: true,
|
||||
|
||||
// Enabling this will run the lib-jitsi-meet noise detection module which will
|
||||
// notify the user if there is noise, other than voice, coming from the current
|
||||
// selected microphone. The purpose it to let the user know that the input could
|
||||
// be potentially unpleasant for other meeting participants.
|
||||
enableNoisyMicDetection: true,
|
||||
|
||||
// Start the conference in audio only mode (no video is being received nor
|
||||
// sent).
|
||||
// startAudioOnly: false,
|
||||
@@ -292,13 +306,11 @@ var config = {
|
||||
// callStatsID: '',
|
||||
// callStatsSecret: '',
|
||||
|
||||
// enables callstatsUsername to be reported as statsId and used
|
||||
// by callstats as repoted remote id
|
||||
// enableStatsID: false
|
||||
|
||||
// enables sending participants display name to callstats
|
||||
// enableDisplayNameInStats: false
|
||||
|
||||
// enables sending participants email if available to callstats and other analytics
|
||||
// enableEmailInStats: false
|
||||
|
||||
// Privacy
|
||||
//
|
||||
@@ -326,6 +338,8 @@ var config = {
|
||||
|
||||
// The STUN servers that will be used in the peer to peer connections
|
||||
stunServers: [
|
||||
|
||||
// { urls: 'stun:jitsi-meet.example.com:443' },
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
{ urls: 'stun:stun2.l.google.com:19302' }
|
||||
@@ -374,6 +388,20 @@ var config = {
|
||||
// userRegion: "asia"
|
||||
}
|
||||
|
||||
// Information for the chrome extension banner
|
||||
// chromeExtensionBanner: {
|
||||
// // The chrome extension to be installed address
|
||||
// url: 'https://chrome.google.com/webstore/detail/jitsi-meetings/kglhbbefdnlheedjiejgomgmfplipfeb',
|
||||
|
||||
// // Extensions info which allows checking if they are installed or not
|
||||
// chromeExtensionsInfo: [
|
||||
// {
|
||||
// id: 'kglhbbefdnlheedjiejgomgmfplipfeb',
|
||||
// path: 'jitsi-logo-48x48.png'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
|
||||
// Local Recording
|
||||
//
|
||||
|
||||
@@ -419,9 +447,15 @@ var config = {
|
||||
// the menu has option to flip the locally seen video for local presentations
|
||||
// disableLocalVideoFlip: false
|
||||
|
||||
// If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
|
||||
// user documentation.
|
||||
// userDocumentationURL: 'https://docs.example.com/video-meetings.html'
|
||||
// Deployment specific URLs.
|
||||
// deploymentUrls: {
|
||||
// // If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
|
||||
// // user documentation.
|
||||
// userDocumentationURL: 'https://docs.example.com/video-meetings.html',
|
||||
// // If specified a 'Download our apps' button will be displayed in the overflow menu with a link
|
||||
// // to the specified URL for an app download page.
|
||||
// downloadAppsUrl: 'https://docs.example.com/our-apps.html'
|
||||
// }
|
||||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,13 @@ import {
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The feature announced so we can distinguish jibri participants.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const DISCO_JIBRI_FEATURE = 'http://jitsi.org/protocol/jibri';
|
||||
|
||||
/**
|
||||
* Checks if we have data to use attach instead of connect. If we have the data
|
||||
* executes attach otherwise check if we have to wait for the data. If we have
|
||||
@@ -75,7 +82,15 @@ function connect(id, password, roomName) {
|
||||
const connectionConfig = Object.assign({}, config);
|
||||
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
|
||||
|
||||
connectionConfig.bosh += `?room=${roomName}`;
|
||||
// Use Websocket URL for the web app if configured. Note that there is no 'isWeb' check, because there's assumption
|
||||
// that this code executes only on web browsers/electron. This needs to be changed when mobile and web are unified.
|
||||
let serviceUrl = connectionConfig.websocket || connectionConfig.bosh;
|
||||
|
||||
serviceUrl += `?room=${roomName}`;
|
||||
|
||||
// FIXME Remove deprecated 'bosh' option assignment at some point(LJM will be accepting only 'serviceUrl' option
|
||||
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
|
||||
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
|
||||
|
||||
const connection
|
||||
= new JitsiMeetJS.JitsiConnection(
|
||||
@@ -83,6 +98,10 @@ function connect(id, password, roomName) {
|
||||
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
|
||||
connectionConfig);
|
||||
|
||||
if (config.iAmRecorder) {
|
||||
connection.addFeature(DISCO_JIBRI_FEATURE);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
|
||||
|
||||
@@ -17,10 +17,11 @@ import parseURLParams from '../react/features/base/config/parseURLParams';
|
||||
|
||||
if (typeof createConnectionExternally === 'function') {
|
||||
// URL params have higher priority than config params.
|
||||
// Do not use external connect if websocket is enabled.
|
||||
let url
|
||||
= parseURLParams(window.location, true, 'hash')[
|
||||
'config.externalConnectUrl']
|
||||
|| config.externalConnectUrl;
|
||||
|| config.websocket ? undefined : config.externalConnectUrl;
|
||||
const isRecorder
|
||||
= parseURLParams(window.location, true, 'hash')['config.iAmRecorder'];
|
||||
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
.avatar {
|
||||
align-items: center;
|
||||
background-color: #AAA;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-weight: 100;
|
||||
justify-content: center;
|
||||
object-fit: cover;
|
||||
|
||||
&.avatar-small {
|
||||
height: 28px !important;
|
||||
width: 28px !important;
|
||||
}
|
||||
|
||||
&.avatar-xsmall {
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
.jitsi-icon {
|
||||
transform: translateY(50%);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-foreign {
|
||||
@@ -28,4 +39,28 @@
|
||||
|
||||
.defaultAvatar {
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
.avatar-badge {
|
||||
position: relative;
|
||||
|
||||
&-available::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-available;
|
||||
}
|
||||
|
||||
&-away::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-away;
|
||||
}
|
||||
|
||||
&-busy::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-busy;
|
||||
}
|
||||
|
||||
&-idle::after {
|
||||
@include avatarBadge;
|
||||
background-color: $presence-idle;
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,11 @@ body {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.jitsi-icon.gray svg {
|
||||
fill: #5E6D7A;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
|
||||
* preventing transparency in filmstrip-only mode. The selector chosen to
|
||||
@@ -161,6 +166,7 @@ form {
|
||||
::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
width: 7px;
|
||||
height: $scrollHeight;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
|
||||
@@ -82,9 +82,10 @@
|
||||
|
||||
#chat-recipient {
|
||||
align-items: center;
|
||||
background-color: $defaultWarningColor;
|
||||
background-color: $chatPrivateMessageBackgroundColor;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: 100;
|
||||
padding: 10px;
|
||||
|
||||
span {
|
||||
@@ -132,6 +133,7 @@
|
||||
#chat-input {
|
||||
border-top: 1px solid $chatInputSeparatorColor;
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
|
||||
* {
|
||||
background-color: transparent;
|
||||
@@ -152,8 +154,7 @@
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
line-height: 30px;
|
||||
padding: 5px;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
@@ -183,6 +184,7 @@
|
||||
.display-name {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -196,7 +198,6 @@
|
||||
color: white;
|
||||
margin-top: 3px;
|
||||
max-width: 100%;
|
||||
padding-bottom: 3px;
|
||||
position: relative;
|
||||
|
||||
&.localuser {
|
||||
@@ -204,6 +205,10 @@
|
||||
border-radius: 6px 0px 6px 6px;
|
||||
}
|
||||
|
||||
.usermessage {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-radius: 0px;
|
||||
|
||||
@@ -219,8 +224,14 @@
|
||||
}
|
||||
|
||||
.privatemessagenotice {
|
||||
color: $defaultWarningColor;
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.messagecontent {
|
||||
margin: 5px 10px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,10 +308,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#usermsg::-webkit-input-placeholder {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#usermsg::-webkit-scrollbar-track-piece {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
@@ -315,6 +322,10 @@
|
||||
.chatmessage {
|
||||
background-color: $chatLocalMessageBackgroundColor;
|
||||
border-radius: 6px 0px 6px 6px;
|
||||
|
||||
&.privatemessage {
|
||||
background-color: $chatPrivateMessageBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
@@ -328,8 +339,9 @@
|
||||
|
||||
&.error {
|
||||
.chatmessage {
|
||||
background-color: $defaultWarningColor;
|
||||
border-radius: 0px;
|
||||
color: red;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
@@ -345,8 +357,17 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.toolbox-icon {
|
||||
cursor: pointer;
|
||||
.messageactions {
|
||||
align-self: stretch;
|
||||
border-left: 1px solid $chatActionsSeparatorColor;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
|
||||
.toolbox-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,6 +378,9 @@
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
color: white;
|
||||
padding: 8px;
|
||||
|
||||
&.privatemessage {
|
||||
background-color: $chatPrivateMessageBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
css/_chrome-extension-banner.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
.chrome-extension-banner {
|
||||
position: fixed;
|
||||
width: 406px;
|
||||
height: $chromeExtensionBannerHeight;
|
||||
background: #FFF;
|
||||
box-shadow: 0px 2px 48px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
z-index: 1000;
|
||||
float: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 20px;
|
||||
top: $chromeExtensionBannerTop;
|
||||
right: $chromeExtensionBannerRight;
|
||||
&__pos_in_meeting {
|
||||
top: $chromeExtensionBannerTopInMeeting;
|
||||
right: $chromeExtensionBannerRightInMeeeting;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__button-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__checkbox-container {
|
||||
display: $chromeExtensionBannerDontShowAgainDisplay;
|
||||
margin-left: 45px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__checkbox-label {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
letter-spacing: -0.006em;
|
||||
color: #1C2025;
|
||||
}
|
||||
|
||||
&__icon-container {
|
||||
display: flex;
|
||||
background: url('../images/chromeLogo.svg');
|
||||
background-repeat: no-repeat;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
&__text-container {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
letter-spacing: -0.006em;
|
||||
color: #151531;
|
||||
width: 329px;
|
||||
}
|
||||
|
||||
&__close-container {
|
||||
display: flex;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&__gray-close-icon {
|
||||
fill: #5E6D7A;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__button-open-url {
|
||||
background: #0A57EB;
|
||||
border-radius: 24px;
|
||||
margin-left: 45px;
|
||||
width: 236px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__button-text {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
letter-spacing: -0.006em;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
66
css/_mini_toolbox.scss
Normal file
@@ -0,0 +1,66 @@
|
||||
.filmstrip-toolbox,
|
||||
.always-on-top-toolbox {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
.toolbox-icon {
|
||||
cursor: pointer;
|
||||
padding: 7px;
|
||||
|
||||
&.toggled {
|
||||
background: $AOTToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.always-on-top-toolbox {
|
||||
flex-direction: row;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.toolbox-button {
|
||||
&:first-child {
|
||||
.toolbox-icon {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
svg {
|
||||
fill: $hangupColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.toolbox-icon {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-toolbox {
|
||||
flex-direction: column;
|
||||
|
||||
.toolbox-button {
|
||||
&:nth-child(1) {
|
||||
svg {
|
||||
fill: $hangupColor;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,4 +192,17 @@
|
||||
*/
|
||||
@mixin transparentBg($color, $alpha) {
|
||||
background-color: rgba(red($color), green($color), blue($color), $alpha);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar status badge mixin
|
||||
*/
|
||||
@mixin avatarBadge {
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 35%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
26
css/_participants-count.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.participants-count {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
color: #5e6d7a;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-left: 16px;
|
||||
padding: 4px 8px;
|
||||
pointer-events: auto;
|
||||
|
||||
&-number {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
background: url('../images/user-groups.svg');
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
1
css/_promotional-footer.scss
Normal file
@@ -0,0 +1 @@
|
||||
/** Insert custom CSS for any additional content in the promotional footer **/
|
||||
@@ -9,7 +9,7 @@
|
||||
text-align: center;
|
||||
font-size: 17px;
|
||||
color: #fff;
|
||||
z-index: $toolbarBackgroundZ;
|
||||
z-index: $zindex10;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-sizing: border-box;
|
||||
@@ -19,4 +19,14 @@
|
||||
&.visible {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&-conference-timer {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,89 +270,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.always-on-top-toolbox,
|
||||
.filmstrip-toolbox {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: $AOTToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: $AOTToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
i.toggled:hover:not(.disabled) {
|
||||
background-color: $AOTToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.always-on-top-toolbox {
|
||||
flex-direction: row;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
transform: translateX(-50%);
|
||||
z-index: $toolbarZ;
|
||||
|
||||
i {
|
||||
font-size: $newToolbarFontSize;
|
||||
height: $newToolbarSize;
|
||||
line-height: $newToolbarSize;
|
||||
width: $newToolbarSize;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.toolbox-button:first-child i {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbox-button:last-child i {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-toolbox {
|
||||
i {
|
||||
font-size: 1.9em;
|
||||
height: 37px;
|
||||
line-height: 37px;
|
||||
width: 37px;
|
||||
}
|
||||
|
||||
.toolbox-button:first-child i {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbox-button:last-child i {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* START of fade in animation for main toolbar
|
||||
*/
|
||||
|
||||
@@ -31,13 +31,6 @@
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows as a list item
|
||||
**/
|
||||
.show-list-item {
|
||||
display: list-item !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a flex element.
|
||||
*/
|
||||
|
||||
@@ -29,11 +29,14 @@ $defaultSideBarFontColor: #44A5FF;
|
||||
$defaultSemiDarkColor: #ACACAC;
|
||||
$defaultDarkColor: #2b3d5c;
|
||||
$defaultWarningColor: rgb(215, 121, 118);
|
||||
$presence-available: rgb(110, 176, 5);
|
||||
$presence-away: rgb(250, 201, 20);
|
||||
$presence-busy: rgb(233, 0, 27);
|
||||
$presence-idle: rgb(172, 172, 172);
|
||||
|
||||
/**
|
||||
* Toolbar
|
||||
*/
|
||||
$defaultToolbarSize: 50px;
|
||||
$newToolbarBackgroundColor: rgba(22, 38, 55, 0.8);
|
||||
$newToolbarButtonHoverColor: rgba(255, 255, 255, 0.15);
|
||||
$newToolbarButtonToggleColor: rgba(255, 255, 255, 0.2);
|
||||
@@ -86,10 +89,12 @@ $modalTextColor: #333;
|
||||
/**
|
||||
* Chat
|
||||
*/
|
||||
$chatActionsSeparatorColor: rgb(173, 105, 112);
|
||||
$chatHeaderBackgroundColor: rgba(42, 58, 75, 0.9);
|
||||
$chatInputSeparatorColor: #A4B8D1;
|
||||
$chatLocalMessageBackgroundColor: rgba(26, 108, 180, 1);
|
||||
$chatRemoteMessageBackgroundColor: rgba(240, 243, 247, 0.15);
|
||||
$chatLocalMessageBackgroundColor: rgb(4, 98, 178);
|
||||
$chatPrivateMessageBackgroundColor: rgb(153, 69, 77);
|
||||
$chatRemoteMessageBackgroundColor: rgb(86, 101, 114);
|
||||
$sidebarWidth: 375px;
|
||||
|
||||
/**
|
||||
@@ -100,6 +105,7 @@ $defaultWatermarkLink: '../images/watermark.png';
|
||||
$popoverMenuPadding: 13px;
|
||||
$happySoftwareBackground: transparent;
|
||||
$desktopAppDragBarHeight: 25px;
|
||||
$scrollHeight: 7px;
|
||||
|
||||
/**
|
||||
* Z-indexes. TODO: Replace this by a function.
|
||||
@@ -178,8 +184,17 @@ $welcomePageHeaderTextMarginTop: 35px;
|
||||
$welcomePageHeaderTextMarginBottom: 35px;
|
||||
|
||||
$welcomePageHeaderTextTitleMarginBottom: 16px;
|
||||
$welcomePageHeaderTextTitleFontSize: 2.5rem;
|
||||
$welcomePageHeaderTextTitleFontWeight: 500;
|
||||
$welcomePageHeaderTextTitleLineHeight: 1.18;
|
||||
$welcomePageHeaderTextTitleOpacity: 1;
|
||||
|
||||
$welcomePageHeaderTextDescriptionDisplay: inherit;
|
||||
$welcomePageHeaderTextDescriptionFontSize: 1rem;
|
||||
$welcomePageHeaderTextDescriptionFontWeight: 400;
|
||||
$welcomePageHeaderTextDescriptionLineHeight: 24px;
|
||||
$welcomePageHeaderTextDescriptionMarginBottom: 20px;
|
||||
$welcomePageHeaderTextDescriptionAlignSelf: inherit;
|
||||
|
||||
$welcomePageEnterRoomWidth: 680px;
|
||||
$welcomePageEnterRoomPadding: 25px 30px;
|
||||
@@ -198,6 +213,8 @@ $welcomePageTabButtonsDisplay: flex;
|
||||
$welcomePageTabDisplay: block;
|
||||
|
||||
$welcomePageButtonWidth: 51px;
|
||||
$welcomePageButtonMinWidth: inherit;
|
||||
$welcomePageButtonFontSize: 14px;
|
||||
$welcomePageButtonHeight: 35px;
|
||||
$welcomePageButtonFontWeight: inherit;
|
||||
$welcomePageButtonBorderRadius: 4px;
|
||||
@@ -207,4 +224,50 @@ $welcomePageButtonLineHeight: 35px;
|
||||
* Deep-linking page variables.
|
||||
*/
|
||||
$deepLinkingMobileLogoHeight: 40px;
|
||||
|
||||
$deepLinkingMobileHeaderBackground: #f1f2f5;
|
||||
|
||||
$deepLinkingMobileLinkColor: inherit;
|
||||
$deepLinkingMobileTextFontSize: inherit;
|
||||
$deepLinkingMobileTextLineHeight: inherit;
|
||||
|
||||
$deepLinkingDialInConferenceIdMargin: 10px 0 10px 0;
|
||||
$deepLinkingDialInConferenceIdPadding: inherit;
|
||||
$deepLinkingDialInConferenceIdBackgroundColor: inherit;
|
||||
$deepLinkingDialInConferenceIdBorderRadius: inherit;
|
||||
|
||||
$deepLinkingDialInConferenceNameFontSize: inherit;
|
||||
$deepLinkingDialInConferenceNameLineHeight: inherit;
|
||||
$deepLinkingDialInConferenceNameMarginBottom: none;
|
||||
$deepLinkingDialInConferenceNameFontWeight: inherit;
|
||||
|
||||
$deepLinkingDialInConferenceDescriptionFontSize: 0.8em;
|
||||
$deepLinkingDialInConferenceDescriptionLineHeight: inherit;
|
||||
$deepLinkingDialInConferenceDescriptionMarginBottom: none;
|
||||
|
||||
$deepLinkingDialInConferencePinFontSize: inherit;
|
||||
$deepLinkingDialInConferencePinLineHeight: inherit;
|
||||
|
||||
$depLinkingMobileHrefLineHeight: 2.2857142857142856em;
|
||||
$deepLinkingMobileHrefFontWeight: bolder;
|
||||
$deepLinkingMobileHrefFontSize: inherit;
|
||||
|
||||
$deepLinkingMobileButtonHeight: 2.2857142857142856em;
|
||||
$deepLinkingMobileButtonLineHeight: 2.2857142857142856em;
|
||||
$deepLinkingMobileButtonMargin: 18px auto 10px;
|
||||
$deepLinkingMobileButtonWidth: auto;
|
||||
$deepLinkingMobileButtonFontWeight: bold;
|
||||
$deepLinkingMobileButtonFontSize: inherit;
|
||||
|
||||
$primaryDeepLinkingMobileButtonBorderRadius: inherit;
|
||||
|
||||
/**
|
||||
* Chrome extension banner variables.
|
||||
*/
|
||||
$chromeExtensionBannerDontShowAgainDisplay: flex;
|
||||
$chromeExtensionBannerHeight: 128px;
|
||||
$chromeExtensionBannerTop: 80px;
|
||||
$chromeExtensionBannerRight: 16px;
|
||||
$chromeExtensionBannerTopInMeeting: 10px;
|
||||
$chromeExtensionBannerRightInMeeeting: 10px;
|
||||
|
||||
|
||||
@@ -173,18 +173,14 @@
|
||||
&__hoverOverlay {
|
||||
background: rgba(0,0,0,.6);
|
||||
border-radius: $borderRadius;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
visibility: hidden;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
&.audio-only {
|
||||
.videoThumbnailProblemFilter {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
@@ -489,6 +485,8 @@
|
||||
height: 300px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#mixedstream {
|
||||
@@ -524,13 +522,16 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 50%;
|
||||
overflow: hidden;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
|
||||
.userAvatar {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,6 +554,9 @@
|
||||
}
|
||||
|
||||
.sharedVideoAvatar {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
@@ -563,21 +567,6 @@
|
||||
filter: grayscale(.5) opacity(0.8);
|
||||
}
|
||||
|
||||
.remoteVideoProblemFilter {
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.videoProblemFilter {
|
||||
-webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
|
||||
filter: blur(10px) grayscale(.5) opacity(0.8);
|
||||
}
|
||||
|
||||
.videoThumbnailProblemFilter {
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
#remotePresenceMessage,
|
||||
#remoteConnectionMessage {
|
||||
position: absolute;
|
||||
@@ -608,24 +597,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#localConnectionMessage {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
top:50%;
|
||||
z-index: $zindex2;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
opacity: .80;
|
||||
text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
|
||||
0px 1px 1px rgba(0,0,0,0.3),
|
||||
1px 0px 1px rgba(0,0,0,0.3),
|
||||
0px 0px 1px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.display-avatar-with-name {
|
||||
.avatar-container {
|
||||
visibility: visible;
|
||||
|
||||
@@ -38,19 +38,21 @@ body.welcome-page {
|
||||
|
||||
.header-text-title {
|
||||
color: $welcomePageTitleColor;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.18;
|
||||
font-size: $welcomePageHeaderTextTitleFontSize;
|
||||
font-weight: $welcomePageHeaderTextTitleFontWeight;
|
||||
line-height: $welcomePageHeaderTextTitleLineHeight;
|
||||
margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
|
||||
opacity: $welcomePageHeaderTextTitleOpacity;
|
||||
}
|
||||
|
||||
.header-text-description {
|
||||
display: $welcomePageHeaderTextDescriptionDisplay;
|
||||
color: $welcomePageDescriptionColor;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
margin-bottom: 20px;
|
||||
font-size: $welcomePageHeaderTextDescriptionFontSize;
|
||||
font-weight: $welcomePageHeaderTextDescriptionFontWeight;
|
||||
line-height: $welcomePageHeaderTextDescriptionLineHeight;
|
||||
margin-bottom: $welcomePageHeaderTextDescriptionMarginBottom;
|
||||
align-self: $welcomePageHeaderTextDescriptionAlignSelf;
|
||||
}
|
||||
|
||||
#enter_room {
|
||||
@@ -148,8 +150,9 @@ body.welcome-page {
|
||||
|
||||
.welcome-page-button {
|
||||
width: $welcomePageButtonWidth;
|
||||
min-width: $welcomePageButtonMinWidth;
|
||||
height: $welcomePageButtonHeight;
|
||||
font-size: 14px;
|
||||
font-size: $welcomePageButtonFontSize;
|
||||
font-weight: $welcomePageButtonFontWeight;
|
||||
background: #0074E0;
|
||||
border-radius: $welcomePageButtonBorderRadius;
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none
|
||||
text-decoration: none;
|
||||
color: $deepLinkingMobileLinkColor;
|
||||
}
|
||||
|
||||
&__body {
|
||||
@@ -41,6 +42,8 @@
|
||||
|
||||
&__text {
|
||||
font-weight: bolder;
|
||||
font-size: $deepLinkingMobileTextFontSize;
|
||||
line-height: $deepLinkingMobileTextLineHeight;
|
||||
padding: 10px 10px 0px 10px;
|
||||
}
|
||||
|
||||
@@ -65,11 +68,28 @@
|
||||
}
|
||||
|
||||
.dial-in-conference-id {
|
||||
margin: 10px 0 10px 0;
|
||||
margin: $deepLinkingDialInConferenceIdMargin;
|
||||
padding: $deepLinkingDialInConferenceIdPadding;
|
||||
background-color: $deepLinkingDialInConferenceIdBackgroundColor;
|
||||
border-radius: $deepLinkingDialInConferenceIdBorderRadius;
|
||||
}
|
||||
|
||||
.dial-in-conference-name {
|
||||
font-size: $deepLinkingDialInConferenceNameFontSize;
|
||||
line-height: $deepLinkingDialInConferenceNameLineHeight;
|
||||
margin-bottom: $deepLinkingDialInConferenceNameMarginBottom;
|
||||
font-weight: $deepLinkingDialInConferenceNameFontWeight;
|
||||
}
|
||||
|
||||
.dial-in-conference-description {
|
||||
font-size: 0.8em;
|
||||
font-size: $deepLinkingDialInConferenceDescriptionFontSize;
|
||||
line-height: $deepLinkingDialInConferenceDescriptionLineHeight;
|
||||
margin-bottom: $deepLinkingDialInConferenceDescriptionMarginBottom;
|
||||
}
|
||||
|
||||
.dial-in-conference-pin {
|
||||
font-size: $deepLinkingDialInConferencePinFontSize;
|
||||
line-height: $deepLinkingDialInConferencePinLineHeight;
|
||||
}
|
||||
|
||||
.toll-free-list {
|
||||
@@ -88,25 +108,27 @@
|
||||
|
||||
&__href {
|
||||
height: 2.2857142857142856em;
|
||||
line-height: 2.2857142857142856em;
|
||||
line-height: $depLinkingMobileHrefLineHeight;
|
||||
margin: 18px auto 20px;
|
||||
max-width: 300px;
|
||||
width: auto;
|
||||
font-weight: bolder;
|
||||
font-weight: $deepLinkingMobileHrefFontWeight;
|
||||
font-size: $deepLinkingMobileHrefFontSize;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border: 0;
|
||||
height: 2.2857142857142856em;
|
||||
line-height: 2.2857142857142856em;
|
||||
margin: 18px auto 10px;
|
||||
height: $deepLinkingMobileButtonHeight;
|
||||
line-height: $deepLinkingMobileButtonLineHeight;
|
||||
margin: $deepLinkingMobileButtonMargin;
|
||||
padding: 0px 10px 0px 10px;
|
||||
max-width: 300px;
|
||||
width: auto;
|
||||
width: $deepLinkingMobileButtonWidth;
|
||||
@include border-radius(3px);
|
||||
background-color: $unsupportedBrowserButtonBgColor;
|
||||
color: #505F79;
|
||||
font-weight: bold;
|
||||
font-weight: $deepLinkingMobileButtonFontWeight;
|
||||
font-size: $deepLinkingMobileButtonFontSize;
|
||||
|
||||
&:active {
|
||||
background-color: $unsupportedBrowserButtonBgColor;
|
||||
@@ -115,7 +137,7 @@
|
||||
&_primary {
|
||||
background-color: $primaryUnsupportedBrowserButtonBgColor;
|
||||
color: #FFFFFF;
|
||||
|
||||
border-radius: $primaryDeepLinkingMobileButtonBorderRadius;
|
||||
&:active {
|
||||
background-color: $primaryUnsupportedBrowserButtonBgColor;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,24 @@
|
||||
@extend %align-right;
|
||||
transition: bottom .3s;
|
||||
z-index: $filmstripVideosZ;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
|
||||
/*
|
||||
* Firefox sets flex items to min-height: auto and min-width: auto,
|
||||
* preventing flex children from shrinking like they do on other browsers.
|
||||
* Setting min-height and min-width 0 is a workaround for the issue so
|
||||
* Firefox behaves like other browsers.
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1043520
|
||||
*/
|
||||
@mixin minHWAutoFix() {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&.reduce-height {
|
||||
bottom: $newToolbarSizeWithPadding;
|
||||
bottom: calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight});
|
||||
}
|
||||
|
||||
&__videos {
|
||||
@@ -29,8 +44,9 @@
|
||||
|
||||
&#remoteVideos {
|
||||
border: $thumbnailsBorder solid transparent;
|
||||
padding-left: $defaultToolbarSize + 5;
|
||||
transition: bottom 2s;
|
||||
flex-grow: 1;
|
||||
@include minHWAutoFix()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,6 +55,7 @@
|
||||
&#filmstripLocalVideo {
|
||||
align-self: flex-end;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
@@ -74,4 +91,52 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
@include minHWAutoFix();
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
|
||||
#filmstripRemoteVideosContainer {
|
||||
flex-direction: row-reverse;
|
||||
/**
|
||||
* Add padding as a hack for Firefox not to show scrollbars when
|
||||
* unnecessary.
|
||||
*/
|
||||
padding: 1px 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.videocontainer {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Workarounds for Edge and Firefox not handling scrolling properly with
|
||||
* flex-direction: row-reverse.
|
||||
*/
|
||||
@mixin undoRowReverseVideos() {
|
||||
.horizontal-filmstrip {
|
||||
#remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Firefox detection hack **/
|
||||
@-moz-document url-prefix() {
|
||||
@include undoRowReverseVideos();
|
||||
}
|
||||
|
||||
/** Edge detection hack **/
|
||||
@supports (-ms-ime-align:auto) {
|
||||
@include undoRowReverseVideos();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.filmstrip__videos .videocontainer {
|
||||
display: none;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-size: contain;
|
||||
border: $thumbnailVideoBorder solid transparent;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#localConnectionMessage,
|
||||
#remoteConnectionMessage,
|
||||
.watermark {
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($desktopAppDragBarHeight - 5px) 5px 10px;
|
||||
/**
|
||||
* fixed positioning is necessary for remote menus and tooltips to pop
|
||||
@@ -48,7 +49,6 @@
|
||||
.filmstrip__videos {
|
||||
@extend %align-right;
|
||||
bottom: 0;
|
||||
overflow: visible !important;
|
||||
padding: 0;
|
||||
position:relative;
|
||||
right: 0;
|
||||
@@ -67,6 +67,7 @@
|
||||
border: $thumbnailsBorder solid transparent;
|
||||
padding-left: 0;
|
||||
transition: right 2s;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +81,16 @@
|
||||
flex-direction: column-reverse;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
|
||||
#filmstripLocalVideoThumbnail {
|
||||
width: calc(100% - 15px);
|
||||
|
||||
.videocontainer {
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,18 +107,21 @@
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
flex-direction: column-reverse;
|
||||
height: auto;
|
||||
justify-content: flex-end;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
#filmstripRemoteVideosContainer {
|
||||
@include minHWAutoFix();
|
||||
flex-direction: column-reverse;
|
||||
/**
|
||||
* Add padding as a hack for Firefox not to show scrollbars when
|
||||
* unnecessary.
|
||||
*/
|
||||
padding: 1px 0;
|
||||
overflow-x: hidden;
|
||||
overflow: visible;
|
||||
width: calc(100% - 8px); // 8px for margin + border of the thumbnails
|
||||
|
||||
.videocontainer {
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,9 +174,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FF does not include the scroll width when calculating the size of the content. That's why we need to include
|
||||
* ourselves the width of the scroll so that the remote videos are aligned with the local one.
|
||||
*/
|
||||
@mixin filmstripSizeWithoutScroll {
|
||||
.vertical-filmstrip {
|
||||
#remoteVideos #filmstripRemoteVideos {
|
||||
#filmstripRemoteVideosContainer {
|
||||
width: calc(100% - 15px) // 8 px - margins + border of the thumbnails; 7px - for the scroll
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Firefox detection hack **/
|
||||
@-moz-document url-prefix() {
|
||||
@include undoColumnReverseVideos();
|
||||
@include filmstripSizeWithoutScroll();
|
||||
}
|
||||
|
||||
/** Edge detection hack **/
|
||||
|
||||
@@ -56,6 +56,10 @@
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.indicator-icon-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.indicator-container {
|
||||
float: none;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ $flagsImagePath: "../images/";
|
||||
@import 'overlay/overlay';
|
||||
@import 'inlay';
|
||||
@import 'reload_overlay/reload_overlay';
|
||||
@import 'mini_toolbox';
|
||||
@import 'modals/desktop-picker/desktop-picker';
|
||||
@import 'modals/device-selection/device-selection';
|
||||
@import 'modals/dialog';
|
||||
@@ -44,6 +45,7 @@ $flagsImagePath: "../images/";
|
||||
@import 'videolayout_default';
|
||||
@import 'notice';
|
||||
@import 'subject';
|
||||
@import 'participants-count';
|
||||
@import 'popup_menu';
|
||||
@import 'recording';
|
||||
@import 'login_menu';
|
||||
@@ -82,5 +84,7 @@ $flagsImagePath: "../images/";
|
||||
@import 'third-party-branding/google';
|
||||
@import 'third-party-branding/microsoft';
|
||||
@import 'avatar';
|
||||
@import 'promotional-footer';
|
||||
@import 'chrome-extension-banner';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
width: -webkit-max-content;
|
||||
word-break: break-all;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-dialog-dial-in {
|
||||
@@ -86,6 +88,15 @@
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.info-dialog-url-icon {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
|
||||
svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.info-dialog-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
@@ -129,15 +140,13 @@
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 5px;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
tr {
|
||||
border-bottom: 1px solid #d1dbe8;
|
||||
}
|
||||
|
||||
@@ -214,4 +223,10 @@
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.info-dialog-url-text-unselectable {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
7
debian/control
vendored
@@ -21,7 +21,7 @@ Description: WebRTC JavaScript video conferences
|
||||
|
||||
Package: jitsi-meet-web-config
|
||||
Architecture: all
|
||||
Depends: openssl, openjdk-8-jre-headless | nginx | nginx-extras | apache2
|
||||
Depends: openssl, nginx | nginx-full | nginx-extras | apache2
|
||||
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.
|
||||
@@ -37,6 +37,7 @@ Description: Configuration for web serving of Jitsi Meet
|
||||
Package: jitsi-meet-prosody
|
||||
Architecture: all
|
||||
Depends: openssl, prosody | prosody-trunk | prosody-0.11
|
||||
Replaces: jitsi-meet-tokens
|
||||
Description: Prosody configuration for Jitsi Meet
|
||||
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
|
||||
Videobridge to provide high quality, scalable video conferences.
|
||||
@@ -53,3 +54,7 @@ Architecture: all
|
||||
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly747) | prosody-0.11 | prosody (>= 0.11.2), libssl-dev, luarocks, jitsi-meet-prosody
|
||||
Description: Prosody token authentication plugin for Jitsi Meet
|
||||
|
||||
Package: jitsi-meet-turnserver
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, nginx (>= 1.13.10) | nginx-full (>= 1.13.10) | nginx-extras (>= 1.13.10), jitsi-meet-prosody, coturn, dnsutils
|
||||
Description: Configures coturn to be used with Jitsi Meet
|
||||
|
||||
1
debian/jitsi-meet-prosody.docs
vendored
@@ -1,2 +1 @@
|
||||
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
|
||||
doc/debian/jitsi-meet-prosody/README
|
||||
|
||||
2
debian/jitsi-meet-prosody.install
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example /usr/share/jitsi-meet-prosody/
|
||||
resources/prosody-plugins/ /usr/share/jitsi-meet/
|
||||
29
debian/jitsi-meet-prosody.postinst
vendored
@@ -80,6 +80,15 @@ case "$1" in
|
||||
# stores the hostname so we will reuse it later, like in purge
|
||||
db_set jitsi-meet-prosody/jvb-hostname "$JVB_HOSTNAME"
|
||||
|
||||
db_get jitsi-meet-prosody/turn-secret
|
||||
if [ -z "$RET" ] ; then
|
||||
# 8-chars random secret used for the turnserver
|
||||
TURN_SECRET=`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1`
|
||||
db_set jitsi-meet-prosody/turn-secret "$TURN_SECRET"
|
||||
else
|
||||
TURN_SECRET="$RET"
|
||||
fi
|
||||
|
||||
# and we're done with debconf
|
||||
db_stop
|
||||
|
||||
@@ -93,11 +102,11 @@ case "$1" in
|
||||
PROSODY_CONFIG_PRESENT="false"
|
||||
mkdir -p /etc/prosody/conf.avail/
|
||||
mkdir -p /etc/prosody/conf.d/
|
||||
cp /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
|
||||
cp /usr/share/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeetSecret/$JVB_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/focusSecret/$JICOFO_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/focusUser/$JICOFO_AUTH_USER/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/__turnSecret__/$TURN_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
if [ ! -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua ]; then
|
||||
ln -s $PROSODY_HOST_CONFIG /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
|
||||
fi
|
||||
@@ -116,6 +125,20 @@ case "$1" in
|
||||
PROSODY_CONFIG_PRESENT="false"
|
||||
fi
|
||||
|
||||
USER_EXISTS_CHECK=`prosodyctl adduser jvb@$JICOFO_AUTH_DOMAIN < /dev/null || true`
|
||||
if [ ! "$USER_EXISTS_CHECK" = "That user already exists" ]; then
|
||||
prosodyctl register jvb $JICOFO_AUTH_DOMAIN $JVB_SECRET || true
|
||||
fi
|
||||
|
||||
# Check whether prosody config has the internal muc, if not add it,
|
||||
# as we are migrating configs
|
||||
if [ -f $PROSODY_HOST_CONFIG ] && ! grep -q "internal.auth.$JVB_HOSTNAME" $PROSODY_HOST_CONFIG; then
|
||||
echo -e "\nComponent \"internal.auth.$JVB_HOSTNAME\" \"muc\"" >> $PROSODY_HOST_CONFIG
|
||||
echo -e " storage = \"null\"" >> $PROSODY_HOST_CONFIG
|
||||
echo -e " modules_enabled = { \"ping\"; }" >> $PROSODY_HOST_CONFIG
|
||||
echo -e " admins = { \"focusUser@auth.jitmeet.example.com\", \"jvb@auth.jitmeet.example.com\" }" >> $PROSODY_HOST_CONFIG
|
||||
fi
|
||||
|
||||
if [ ! -f /var/lib/prosody/$JVB_HOSTNAME.crt ]; then
|
||||
# prosodyctl takes care for the permissions
|
||||
# echo for using all default values
|
||||
@@ -179,7 +202,7 @@ case "$1" in
|
||||
fi
|
||||
|
||||
if [ "$PROSODY_CONFIG_PRESENT" = "false" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
invoke-rc.d prosody restart || true
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
2
debian/jitsi-meet-prosody.postrm
vendored
@@ -25,7 +25,7 @@ set -e
|
||||
case "$1" in
|
||||
remove)
|
||||
if [ -x "/etc/init.d/prosody" ]; then
|
||||
invoke-rc.d prosody reload
|
||||
invoke-rc.d prosody reload || true
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
5
debian/jitsi-meet-prosody.templates
vendored
@@ -28,3 +28,8 @@ Template: jicofo/jicofosecret
|
||||
Type: password
|
||||
_Description: Jicofo Component secret:
|
||||
The secret used to connect to xmpp server as component
|
||||
|
||||
Template: jitsi-meet-prosody/turn-secret
|
||||
Type: string
|
||||
_Description: The turn server secret
|
||||
The secret used to connect to turnserver server.
|
||||
|
||||
1
debian/jitsi-meet-tokens.install
vendored
@@ -1 +0,0 @@
|
||||
resources/prosody-plugins/ /usr/share/jitsi-meet/
|
||||
2
debian/jitsi-meet-tokens.postinst
vendored
@@ -78,7 +78,7 @@ case "$1" in
|
||||
fi
|
||||
|
||||
if [ -x "/etc/init.d/prosody" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
invoke-rc.d prosody restart || true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
|
||||
4
debian/jitsi-meet-tokens.postrm
vendored
@@ -41,10 +41,10 @@ case "$1" in
|
||||
sed -i 's/authentication = "token"/authentication = "anonymous"/g' $PROSODY_HOST_CONFIG
|
||||
sed -i "s/ app_id=\"$APP_ID\"/ --app_id=\"example_app_id\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/ app_secret=\"$APP_SECRET\"/ --app_secret=\"example_app_secret\"/g" $PROSODY_HOST_CONFIG
|
||||
sed -i 's/ modules_enabled = { "token_verification" }/ --modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
|
||||
sed -i 's/ -- "token_verification"/ "token_verification"/g' $PROSODY_HOST_CONFIG
|
||||
|
||||
if [ -x "/etc/init.d/prosody" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
invoke-rc.d prosody restart || true
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
2
debian/jitsi-meet-turnserver.install
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
doc/debian/jitsi-meet-turn/turnserver.conf /usr/share/jitsi-meet-turnserver/
|
||||
doc/debian/jitsi-meet/jitsi-meet.conf /usr/share/jitsi-meet-turnserver/
|
||||
1
debian/jitsi-meet-turnserver.links
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/usr/share/jitsi-meet-turnserver/jitsi-meet.conf /etc/nginx/modules-enabled/60-jitsi-meet.conf
|
||||
127
debian/jitsi-meet-turnserver.postinst
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# postinst script for jitsi-meet-turnserver
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
set -e
|
||||
|
||||
# summary of how this script can be called:
|
||||
# * <postinst> `configure' <most-recently-configured-version>
|
||||
# * <old-postinst> `abort-upgrade' <new version>
|
||||
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
|
||||
# <new-version>
|
||||
# * <postinst> `abort-remove'
|
||||
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
|
||||
# <failed-install-package> <version> `removing'
|
||||
# <conflicting-package> <version>
|
||||
# for details, see http://www.debian.org/doc/debian-policy/ or
|
||||
# the debian-policy package
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# loading debconf
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
# try to get host from jitsi-videobridge
|
||||
db_get jitsi-videobridge/jvb-hostname
|
||||
if [ -z "$RET" ] ; then
|
||||
# server hostname
|
||||
db_set jitsi-videobridge/jvb-hostname "localhost"
|
||||
db_input critical jitsi-videobridge/jvb-hostname || true
|
||||
db_go
|
||||
fi
|
||||
JVB_HOSTNAME="$RET"
|
||||
|
||||
TURN_CONFIG="/etc/turnserver.conf"
|
||||
NGINX_CONFIG="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
|
||||
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
|
||||
|
||||
# detect dpkg-reconfigure, just delete old links
|
||||
db_get jitsi-meet-turnserver/jvb-hostname
|
||||
JVB_HOSTNAME_OLD=$RET
|
||||
if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
|
||||
rm -f $TURN_CONFIG
|
||||
fi
|
||||
|
||||
# this detect only old installations with no nginx
|
||||
db_get jitsi-meet/jvb-serve || true
|
||||
if [ ! -f $NGINX_CONFIG -o "$RET" = "true" ] ; then
|
||||
# nothing to do
|
||||
echo ""
|
||||
echo "turnserver not configured as no nginx found to multiplex traffic"
|
||||
echo ""
|
||||
db_stop
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# stores the hostname so we will reuse it later, like in purge
|
||||
db_set jitsi-meet-turnserver/jvb-hostname "$JVB_HOSTNAME"
|
||||
|
||||
# try to get turnserver password
|
||||
db_get jitsi-meet-prosody/turn-secret
|
||||
if [ -z "$RET" ] ; then
|
||||
db_input critical jitsi-meet-prosody/turn-secret || true
|
||||
db_go
|
||||
fi
|
||||
TURN_SECRET="$RET"
|
||||
|
||||
if [[ -f $TURN_CONFIG ]] && ! grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
|
||||
PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com)
|
||||
cp /usr/share/jitsi-meet-turnserver/turnserver.conf $TURN_CONFIG
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
|
||||
sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
|
||||
sed -i "s/__external_ip_address__/$JVB_HOSTNAME/g" $TURN_CONFIG
|
||||
|
||||
# SSL for nginx
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
|
||||
if [ "$CERT_CHOICE" = "I want to use my own certificate" ] ; then
|
||||
db_get jitsi-meet/cert-path-key
|
||||
CERT_KEY="$RET"
|
||||
db_get jitsi-meet/cert-path-crt
|
||||
CERT_CRT="$RET"
|
||||
|
||||
# replace self-signed certificate paths with user provided ones
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/pkey=\/etc\/jitsi\/meet\/.*key/pkey=$CERT_KEY_ESC/g" $TURN_CONFIG
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/cert=\/etc\/jitsi\/meet\/.*crt/cert=$CERT_CRT_ESC/g" $TURN_CONFIG
|
||||
fi
|
||||
|
||||
sed -i "s/#TURNSERVER_ENABLED/TURNSERVER_ENABLED/g" /etc/default/coturn
|
||||
invoke-rc.d coturn restart || true
|
||||
|
||||
NGINX_STREAM_CONFIG="/etc/nginx/modules-enabled/60-jitsi-meet.conf"
|
||||
if [ -f $NGINX_STREAM_CONFIG ] && [ -f $NGINX_CONFIG ] ; then
|
||||
sed -i "s/listen 443 ssl/listen 4444 ssl http2/g" $NGINX_CONFIG
|
||||
invoke-rc.d nginx reload || true
|
||||
fi
|
||||
|
||||
# Enable turn server in config.js
|
||||
if [ -f $JITSI_MEET_CONFIG ] ; then
|
||||
sed -i "s/\/\/ useStunTurn: true/useStunTurn: true/g" $JITSI_MEET_CONFIG
|
||||
fi
|
||||
fi
|
||||
|
||||
# and we're done with debconf
|
||||
db_stop
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# dh_installdeb will replace this with shell code automatically
|
||||
# generated by other debhelper scripts.
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
9
debian/jitsi-meet-turnserver.templates
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
Template: jitsi-meet-turnserver/jvb-hostname
|
||||
Type: string
|
||||
_Description: The hostname of the current installation:
|
||||
The value for the hostname that is set in Jitsi Videobridge installation.
|
||||
|
||||
Template: jitsi-videobridge/jvb-hostname
|
||||
Type: string
|
||||
_Description: The hostname of the current installation:
|
||||
The value for the hostname that is set in Jitsi Videobridge installation.
|
||||
3
debian/jitsi-meet-web-config.docs
vendored
@@ -1,4 +1 @@
|
||||
doc/debian/jitsi-meet/jitsi-meet.example
|
||||
doc/debian/jitsi-meet/jitsi-meet.example-apache
|
||||
doc/debian/jitsi-meet/README
|
||||
config.js
|
||||
|
||||
3
debian/jitsi-meet-web-config.install
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
doc/debian/jitsi-meet/jitsi-meet.example /usr/share/jitsi-meet-web-config/
|
||||
doc/debian/jitsi-meet/jitsi-meet.example-apache /usr/share/jitsi-meet-web-config/
|
||||
config.js /usr/share/jitsi-meet-web-config/
|
||||
95
debian/jitsi-meet-web-config.postinst
vendored
@@ -43,7 +43,8 @@ case "$1" in
|
||||
fi
|
||||
|
||||
JVB_SERVE="false"
|
||||
db_get jitsi-meet/jvb-serve
|
||||
# this detect only old installations
|
||||
db_get jitsi-meet/jvb-serve || true
|
||||
if [ -n "$RET" ] && [ "$RET" = "true" ] ; then
|
||||
JVB_SERVE="true"
|
||||
fi
|
||||
@@ -52,9 +53,12 @@ case "$1" in
|
||||
db_set jitsi-meet/jvb-hostname $JVB_HOSTNAME
|
||||
|
||||
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)"
|
||||
if [ "$NGINX_INSTALL_CHECK" = "installed" ] \
|
||||
|| [ "$NGINX_INSTALL_CHECK" = "unpacked" ] \
|
||||
|| [ "$NGINX_FULL_INSTALL_CHECK" = "installed" ] \
|
||||
|| [ "$NGINX_FULL_INSTALL_CHECK" = "unpacked" ] \
|
||||
|| [ "$NGINX_EXTRAS_INSTALL_CHECK" = "installed" ] \
|
||||
|| [ "$NGINX_EXTRAS_INSTALL_CHECK" = "unpacked" ] ; then
|
||||
FORCE_NGINX="true"
|
||||
@@ -98,77 +102,27 @@ case "$1" in
|
||||
# jitsi meet
|
||||
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
|
||||
if [ ! -f $JITSI_MEET_CONFIG ] ; then
|
||||
cp /usr/share/doc/jitsi-meet-web-config/config.js $JITSI_MEET_CONFIG
|
||||
cp /usr/share/jitsi-meet-web-config/config.js $JITSI_MEET_CONFIG
|
||||
# replaces needed config for multidomain as it works only with nginx
|
||||
sed -i "s/conference.jitsi-meet.example.com/conference.<\!--# echo var=\"subdomain\" default=\"\" -->jitsi-meet.example.com/g" $JITSI_MEET_CONFIG
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $JITSI_MEET_CONFIG
|
||||
fi
|
||||
|
||||
# this is new install let's configure jvb to serve meet
|
||||
# no-nginx, no-apache installed on machine, this is new install or reconfiguring old one which have jvb_serve set
|
||||
if [[ -z "$FORCE_NGINX" && -z "$FORCE_APACHE" && ( -z "$JVB_HOSTNAME_OLD" || ( "$JVB_SERVE" = "true" && "$RECONFIGURING" = "true" )) ]] ; then
|
||||
|
||||
JVB_ETC_CONFIG="/etc/jitsi/videobridge/config"
|
||||
JVB_CONFIG="/etc/jitsi/videobridge/sip-communicator.properties"
|
||||
|
||||
# this is a reconfigure, lets just delete old links
|
||||
if [ "$RECONFIGURING" = "true" ] ; then
|
||||
rm -f $JVB_CONFIG
|
||||
fi
|
||||
|
||||
# we will write to the file if missing create it
|
||||
if [ ! -f $JVB_CONFIG ] ; then
|
||||
touch $JVB_CONFIG
|
||||
fi
|
||||
|
||||
# configure jvb
|
||||
echo "AUTHBIND=yes" >> $JVB_ETC_CONFIG
|
||||
sed -i "s/JVB_OPTS=.*/JVB_OPTS=--apis=rest,xmpp/g" $JVB_ETC_CONFIG
|
||||
|
||||
echo "org.jitsi.videobridge.rest.jetty.host=::" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.port=443" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ProxyServlet.hostHeader=$JVB_HOSTNAME" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ProxyServlet.pathSpec=/http-bind" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ProxyServlet.proxyTo=http://localhost:5280/http-bind" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ResourceHandler.resourceBase=/usr/share/jitsi-meet" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ResourceHandler.alias./config.js=/etc/jitsi/meet/$JVB_HOSTNAME-config.js" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ResourceHandler.alias./interface_config.js=/usr/share/jitsi-meet/interface_config.js" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ResourceHandler.alias./logging_config.js=/usr/share/jitsi-meet/logging_config.js" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.ResourceHandler.alias./external_api.js=/usr/share/jitsi-meet/libs/external_api.min.js" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.RewriteHandler.regex=^/([a-zA-Z0-9]+)$" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.RewriteHandler.replacement=/" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.SSIResourceHandler.paths=/" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.tls.port=443" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.TCP_HARVESTER_PORT=443" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.sslContextFactory.keyStorePath=/etc/jitsi/videobridge/$JVB_HOSTNAME.jks" >> $JVB_CONFIG
|
||||
echo "org.jitsi.videobridge.rest.jetty.sslContextFactory.keyStorePassword=changeit" >> $JVB_CONFIG
|
||||
|
||||
# configure authbind to allow jvb to bind to privileged ports
|
||||
OWNER=$(stat -c '%U' /usr/share/jitsi-videobridge)
|
||||
GROUP=$(stat -c '%G' /usr/share/jitsi-videobridge)
|
||||
JVB_UID="`id -u $OWNER`"
|
||||
if [ ! -f "/etc/authbind/byport/443" ] ; then
|
||||
if [ ! -d "/etc/authbind/byport" ] ; then
|
||||
mkdir -p /etc/authbind/byport
|
||||
chmod 755 /etc/authbind
|
||||
chmod 755 /etc/authbind/byport
|
||||
fi
|
||||
touch /etc/authbind/byport/443
|
||||
chown $OWNER /etc/authbind/byport/443
|
||||
chmod 755 /etc/authbind/byport/443
|
||||
fi
|
||||
|
||||
CERT_P12="/etc/jitsi/videobridge/$JVB_HOSTNAME.p12"
|
||||
CERT_JKS="/etc/jitsi/videobridge/$JVB_HOSTNAME.jks"
|
||||
# create jks from certs
|
||||
openssl pkcs12 -export \
|
||||
-in $CERT_CRT -inkey $CERT_KEY -passout pass:changeit > $CERT_P12
|
||||
keytool -importkeystore -destkeystore $CERT_JKS \
|
||||
-srckeystore $CERT_P12 -srcstoretype pkcs12 \
|
||||
-noprompt -storepass changeit -srcstorepass changeit
|
||||
|
||||
db_set jitsi-meet/jvb-serve "true"
|
||||
|
||||
invoke-rc.d jitsi-videobridge restart
|
||||
if [[ "$JVB_SERVE" = "true" ]] ; then
|
||||
echo ""
|
||||
echo "------------------------------------------------"
|
||||
echo "You are using jetty to serve jitsi-meet, it is recommended to uninstall(purge) and use default installation that comes with nginx!"
|
||||
echo ""
|
||||
echo "When using the following command, any custom config will be LOST, please backup /etc/jitsi !!!"
|
||||
echo ""
|
||||
echo "You can purge your installation using the following command:"
|
||||
echo "apt-get purge jitsi-meet jitsi-meet-web-config jitsi-meet-prosody jitsi-meet-web jicofo jitsi-videobridge"
|
||||
echo "------------------------------------------------"
|
||||
echo ""
|
||||
elif [[ "$FORCE_NGINX" = "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
|
||||
@@ -177,7 +131,7 @@ case "$1" in
|
||||
|
||||
# nginx conf
|
||||
if [ ! -f /etc/nginx/sites-available/$JVB_HOSTNAME.conf ] ; then
|
||||
cp /usr/share/doc/jitsi-meet-web-config/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
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
|
||||
fi
|
||||
@@ -196,8 +150,9 @@ case "$1" in
|
||||
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
|
||||
invoke-rc.d nginx reload
|
||||
invoke-rc.d nginx reload || true
|
||||
elif [[ "$FORCE_APACHE" = "true" && ( -z "$JVB_HOSTNAME_OLD" || "$RECONFIGURING" = "true" ) ]] ; then
|
||||
|
||||
# this is a reconfigure, lets just delete old links
|
||||
if [ "$RECONFIGURING" = "true" ] ; then
|
||||
a2dissite $JVB_HOSTNAME_OLD.conf
|
||||
@@ -208,7 +163,7 @@ case "$1" in
|
||||
if [ ! -f /etc/apache2/sites-available/$JVB_HOSTNAME.conf ] ; then
|
||||
# when creating new config, make sure all needed modules are enabled
|
||||
a2enmod rewrite ssl headers proxy_http include
|
||||
cp /usr/share/doc/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
a2ensite $JVB_HOSTNAME.conf
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
@@ -225,7 +180,7 @@ case "$1" in
|
||||
/etc/apache2/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
|
||||
invoke-rc.d apache2 reload
|
||||
invoke-rc.d apache2 reload || true
|
||||
fi
|
||||
|
||||
echo "----------------"
|
||||
|
||||
6
debian/jitsi-meet-web-config.postrm
vendored
@@ -25,10 +25,10 @@ set -e
|
||||
case "$1" in
|
||||
remove)
|
||||
if [ -x "/etc/init.d/nginx" ]; then
|
||||
invoke-rc.d nginx reload
|
||||
invoke-rc.d nginx reload || true
|
||||
fi
|
||||
if [ -x "/etc/init.d/apache2" ]; then
|
||||
invoke-rc.d apache2 reload
|
||||
invoke-rc.d apache2 reload || true
|
||||
fi
|
||||
;;
|
||||
purge)
|
||||
@@ -40,8 +40,6 @@ case "$1" in
|
||||
rm -f /etc/nginx/sites-enabled/$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/videobridge/$JVB_HOSTNAME.jks
|
||||
rm -f /etc/jitsi/videobridge/$JVB_HOSTNAME.p12
|
||||
rm -f /etc/jitsi/meet/$JVB_HOSTNAME.key
|
||||
rm -f /etc/jitsi/meet/$JVB_HOSTNAME.crt
|
||||
fi
|
||||
|
||||
6
debian/jitsi-meet-web-config.templates
vendored
@@ -26,12 +26,6 @@ Type: string
|
||||
_Description: The hostname of the current installation:
|
||||
The value for the hostname that is set in Jitsi Videobridge installation.
|
||||
|
||||
Template: jitsi-meet/jvb-serve
|
||||
Type: boolean
|
||||
Default: false
|
||||
_Description: for internal use
|
||||
for internal use.
|
||||
|
||||
Template: jitsi-videobridge/jvb-hostname
|
||||
Type: string
|
||||
_Description: Hostname:
|
||||
|
||||
2
debian/rules
vendored
@@ -14,7 +14,7 @@ override_dh_auto_build:
|
||||
|
||||
override_dh_install: $(LANGUAGES)
|
||||
dh_installdirs
|
||||
dh_install -X/config.js -X/package.json
|
||||
dh_install
|
||||
|
||||
$(LANGUAGES):
|
||||
LOCALE=$$(echo $@ | cut -c1-2) ; \
|
||||
|
||||
14
doc/api.md
@@ -30,6 +30,7 @@ Its constructor gets a number of options:
|
||||
* **onload**: (optional) handler for the iframe onload event.
|
||||
* **invitees**: (optional) Array of objects containing information about new participants that will be invited in the call.
|
||||
* **devices**: (optional) A map containing information about the initial devices that will be used in the call.
|
||||
* **userInfo**: (optional) JS object containing information about the participant opening the meeting, such as `email`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -84,6 +85,19 @@ const options = {
|
||||
const api = new JitsiMeetExternalAPI(domain, options);
|
||||
```
|
||||
|
||||
You can set the userInfo(email) for the call:
|
||||
|
||||
```javascript
|
||||
var domain = "meet.jit.si";
|
||||
var options = {
|
||||
...
|
||||
userInfo: {
|
||||
email: 'email@jitsiexamplemail.com'
|
||||
}
|
||||
}
|
||||
var api = new JitsiMeetExternalAPI(domain, options);
|
||||
```
|
||||
|
||||
### Controlling the embedded Jitsi Meet Conference
|
||||
|
||||
Device management `JitsiMeetExternalAPI` methods:
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
-- Plugins path gets uncommented during jitsi-meet-tokens package install - that's where token plugin is located
|
||||
--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
|
||||
plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
|
||||
|
||||
-- domain mapper options, must at least have domain base set to use the mapper
|
||||
muc_mapper_domain_base = "jitmeet.example.com";
|
||||
|
||||
turncredentials_secret = "__turnSecret__";
|
||||
|
||||
turncredentials = {
|
||||
{ type = "stun", host = "jitmeet.example.com", port = "443" },
|
||||
{ type = "turn", host = "jitmeet.example.com", port = "443", transport = "udp" },
|
||||
{ type = "turns", host = "jitmeet.example.com", port = "443", transport = "tcp" }
|
||||
};
|
||||
|
||||
cross_domain_bosh = false;
|
||||
consider_bosh_secure = true;
|
||||
|
||||
VirtualHost "jitmeet.example.com"
|
||||
-- enabled = false -- Remove this line to enable this host
|
||||
@@ -16,25 +29,44 @@ VirtualHost "jitmeet.example.com"
|
||||
key = "/etc/prosody/certs/jitmeet.example.com.key";
|
||||
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
|
||||
}
|
||||
speakerstats_component = "speakerstats.jitmeet.example.com"
|
||||
conference_duration_component = "conferenceduration.jitmeet.example.com"
|
||||
-- we need bosh
|
||||
modules_enabled = {
|
||||
"bosh";
|
||||
"pubsub";
|
||||
"ping"; -- Enable mod_ping
|
||||
"speakerstats";
|
||||
"turncredentials";
|
||||
"conference_duration";
|
||||
}
|
||||
|
||||
c2s_require_encryption = false
|
||||
|
||||
Component "conference.jitmeet.example.com" "muc"
|
||||
storage = "null"
|
||||
--modules_enabled = { "token_verification" }
|
||||
admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
modules_enabled = {
|
||||
"muc_meeting_id";
|
||||
"muc_domain_mapper";
|
||||
-- "token_verification";
|
||||
}
|
||||
admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
|
||||
Component "jitsi-videobridge.jitmeet.example.com"
|
||||
component_secret = "jitmeetSecret"
|
||||
-- internal muc component
|
||||
Component "internal.auth.jitmeet.example.com" "muc"
|
||||
storage = "null"
|
||||
modules_enabled = {
|
||||
"ping";
|
||||
}
|
||||
admins = { "focusUser@auth.jitmeet.example.com", "jvb@auth.jitmeet.example.com" }
|
||||
|
||||
VirtualHost "auth.jitmeet.example.com"
|
||||
authentication = "internal_plain"
|
||||
|
||||
Component "focus.jitmeet.example.com"
|
||||
component_secret = "focusSecret"
|
||||
|
||||
Component "speakerstats.jitmeet.example.com" "speakerstats_component"
|
||||
muc_component = "conference.jitmeet.example.com"
|
||||
|
||||
Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
|
||||
muc_component = "conference.jitmeet.example.com"
|
||||
|
||||
1
doc/debian/jitsi-meet-turn/RREADME
Normal file
@@ -0,0 +1 @@
|
||||
Coturn configuration for Jitsi Meet
|
||||
13
doc/debian/jitsi-meet-turn/turnserver.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
# jitsi-meet coturn config. Do not modify this line
|
||||
lt-cred-mech
|
||||
use-auth-secret
|
||||
keep-address-family
|
||||
static-auth-secret=__turnSecret__
|
||||
realm=jitsi-meet.example.com
|
||||
cert=/etc/jitsi/meet/jitsi-meet.example.com.crt
|
||||
pkey=/etc/jitsi/meet/jitsi-meet.example.com.key
|
||||
|
||||
no-tcp
|
||||
listening-port=443
|
||||
tls-listening-port=4445
|
||||
external-ip=__external_ip_address__
|
||||
30
doc/debian/jitsi-meet/jitsi-meet.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
# this is jitsi-meet nginx module configuration
|
||||
# this forward all http traffic to the nginx virtual host port
|
||||
# and the rest to the turn server
|
||||
|
||||
stream {
|
||||
upstream web {
|
||||
server 127.0.0.1:4444;
|
||||
}
|
||||
upstream turn {
|
||||
server 127.0.0.1:4445;
|
||||
}
|
||||
# since 1.13.10
|
||||
map $ssl_preread_alpn_protocols $upstream {
|
||||
"h2" web;
|
||||
"http/1.1" web;
|
||||
"h2,http/1.1" web;
|
||||
default turn;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443;
|
||||
|
||||
# since 1.11.5
|
||||
ssl_preread on;
|
||||
proxy_pass $upstream;
|
||||
|
||||
# Increase buffer to serve video
|
||||
proxy_buffer_size 10m;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,17 @@ server_names_hash_bucket_size 64;
|
||||
server {
|
||||
listen 80;
|
||||
server_name jitsi-meet.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
default_type "text/plain";
|
||||
root /usr/share/jitsi-meet;
|
||||
}
|
||||
location = /.well-known/acme-challenge/ {
|
||||
return 404;
|
||||
}
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 443 ssl;
|
||||
@@ -19,29 +29,84 @@ server {
|
||||
ssl_certificate_key /etc/jitsi/meet/jitsi-meet.example.com.key;
|
||||
|
||||
root /usr/share/jitsi-meet;
|
||||
|
||||
# ssi on with javascript for multidomain variables in config.js
|
||||
ssi on;
|
||||
ssi_types application/x-javascript application/javascript;
|
||||
|
||||
index index.html index.htm;
|
||||
error_page 404 /static/404.html;
|
||||
|
||||
location /config.js {
|
||||
location = /config.js {
|
||||
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
|
||||
}
|
||||
|
||||
location /external_api.js {
|
||||
location = /external_api.js {
|
||||
alias /usr/share/jitsi-meet/libs/external_api.min.js;
|
||||
}
|
||||
|
||||
location ~ ^/([a-zA-Z0-9=\?]+)$ {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
|
||||
location / {
|
||||
ssi on;
|
||||
#ensure all static content can always be found first
|
||||
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
|
||||
{
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
alias /usr/share/jitsi-meet/$1/$2;
|
||||
}
|
||||
|
||||
# BOSH
|
||||
location /http-bind {
|
||||
location = /http-bind {
|
||||
proxy_pass http://localhost:5280/http-bind;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# xmpp websockets
|
||||
location = /xmpp-websocket {
|
||||
proxy_pass http://127.0.0.1:5280/xmpp-websocket?prefix=$prefix&$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
tcp_nodelay on;
|
||||
}
|
||||
|
||||
location ~ ^/([^/?&:'"]+)$ {
|
||||
try_files $uri @root_path;
|
||||
}
|
||||
|
||||
location @root_path {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
|
||||
location ~ ^/([^/?&:'"]+)/config.js$
|
||||
{
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
|
||||
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
|
||||
}
|
||||
|
||||
#Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
|
||||
location ~ ^/([^/?&:'"]+)/(.*)$ {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
rewrite ^/([^/?&:'"]+)/(.*)$ /$2;
|
||||
}
|
||||
|
||||
# BOSH for subdomains
|
||||
location ~ ^/([^/?&:'"]+)/http-bind {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
set $prefix "$1";
|
||||
|
||||
rewrite ^/(.*)$ /http-bind;
|
||||
}
|
||||
|
||||
# websockets for subdomains
|
||||
location ~ ^/([^/?&:'"]+)/xmpp-websocket {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
set $prefix "$1";
|
||||
|
||||
rewrite ^/(.*)$ /xmpp-websocket;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
var subdomain = "<!--# echo var="subdomain" default="" -->";
|
||||
if (subdomain) {
|
||||
subdomain = subdomain.substr(0,subdomain.length-1).split('.').join('_').toLowerCase() + '.';
|
||||
}
|
||||
|
||||
var config = {
|
||||
hosts: {
|
||||
domain: 'jitsi.example.com',
|
||||
muc: 'conference.'+subdomain+'jitsi.example.com', // FIXME: use XEP-0030
|
||||
focus: 'focus.jitsi.example.com',
|
||||
},
|
||||
useNicks: false,
|
||||
bosh: '//jitsi.example.com/http-bind', // FIXME: use xep-0156 for that
|
||||
websocket: 'wss://jitsi.example.com/xmpp-websocket'
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name jitsi.example.com;
|
||||
# set the root
|
||||
root /srv/jitsi.example.com;
|
||||
# ssi on with javascript for multidomain variables in config.js
|
||||
ssi on;
|
||||
ssi_types application/x-javascript application/javascript;
|
||||
index index.html;
|
||||
set $prefix "";
|
||||
|
||||
|
||||
# BOSH
|
||||
location /http-bind {
|
||||
proxy_pass http://localhost:5280/http-bind;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# xmpp websockets
|
||||
location /xmpp-websocket {
|
||||
proxy_pass http://localhost:5280/xmpp-websocket;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
tcp_nodelay on;
|
||||
}
|
||||
|
||||
location ~ ^/([^/?&:'"]+)$ {
|
||||
try_files $uri @root_path;
|
||||
}
|
||||
|
||||
location @root_path {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
|
||||
location ~ ^/([^/?&:'"]+)/config.js$
|
||||
{
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
|
||||
alias /etc/jitsi/meet/{{jitsi_meet_domain_name}}-config.js;
|
||||
}
|
||||
|
||||
#Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
|
||||
location ~ ^/([^/?&:'"]+)/(.*)$ {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
rewrite ^/([^/?&:'"]+)/(.*)$ /$2;
|
||||
}
|
||||
|
||||
# BOSH for subdomains
|
||||
location ~ ^/([^/?&:'"]+)/http-bind {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
set $prefix "$1";
|
||||
|
||||
rewrite ^/(.*)$ /http-bind;
|
||||
}
|
||||
|
||||
# websockets for subdomains
|
||||
location ~ ^/([^/?&:'"]+)/xmpp-websocket {
|
||||
set $subdomain "$1.";
|
||||
set $subdir "$1/";
|
||||
set $prefix "$1";
|
||||
|
||||
rewrite ^/(.*)$ /xmpp-websocket;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
-- Prosody XMPP Server Configuration
|
||||
--
|
||||
-- Information on configuring Prosody can be found on our
|
||||
-- website at http://prosody.im/doc/configure
|
||||
--
|
||||
-- Tip: You can check that the syntax of this file is correct
|
||||
-- when you have finished by running: prosodyctl check config
|
||||
-- If there are any errors, it will let you know what and where
|
||||
-- they are, otherwise it will keep quiet.
|
||||
--
|
||||
-- Good luck, and happy Jabbering!
|
||||
|
||||
|
||||
---------- Server-wide settings ----------
|
||||
-- Settings in this section apply to the whole server and are the default settings
|
||||
-- for any virtual hosts
|
||||
|
||||
-- This is a (by default, empty) list of accounts that are admins
|
||||
-- for the server. Note that you must create the accounts separately
|
||||
-- (see http://prosody.im/doc/creating_accounts for info)
|
||||
-- Example: admins = { "user1@example.com", "user2@example.net" }
|
||||
admins = { }
|
||||
daemonize = true
|
||||
cross_domain_bosh = true;
|
||||
component_ports = { 5347 }
|
||||
--component_interface = "192.168.0.10"
|
||||
|
||||
-- Enable use of libevent for better performance under high load
|
||||
-- For more information see: http://prosody.im/doc/libevent
|
||||
--use_libevent = true
|
||||
|
||||
-- This is the list of modules Prosody will load on startup.
|
||||
-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
|
||||
-- Documentation on modules can be found at: http://prosody.im/doc/modules
|
||||
modules_enabled = {
|
||||
|
||||
-- Generally required
|
||||
"roster"; -- Allow users to have a roster. Recommended ;)
|
||||
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
|
||||
"tls"; -- Add support for secure TLS on c2s/s2s connections
|
||||
"dialback"; -- s2s dialback support
|
||||
"disco"; -- Service discovery
|
||||
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
|
||||
|
||||
-- Not essential, but recommended
|
||||
"private"; -- Private XML storage (for room bookmarks, etc.)
|
||||
"vcard"; -- Allow users to set vCards
|
||||
|
||||
-- These are commented by default as they have a performance impact
|
||||
--"privacy"; -- Support privacy lists
|
||||
"compression"; -- Stream compression (requires the lua-zlib package installed)
|
||||
|
||||
-- Nice to have
|
||||
"version"; -- Replies to server version requests
|
||||
"uptime"; -- Report how long server has been running
|
||||
"time"; -- Let others know the time here on this server
|
||||
"ping"; -- Replies to XMPP pings with pongs
|
||||
"pep"; -- Enables users to publish their mood, activity, playing music and more
|
||||
"register"; -- Allow users to register on this server using a client and change passwords
|
||||
|
||||
-- Admin interfaces
|
||||
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
|
||||
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
|
||||
|
||||
-- HTTP modules
|
||||
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
|
||||
--"http_files"; -- Serve static files from a directory over HTTP
|
||||
|
||||
-- Other specific functionality
|
||||
--"groups"; -- Shared roster support
|
||||
--"announce"; -- Send announcement to all online users
|
||||
--"welcome"; -- Welcome users who register accounts
|
||||
--"watchregistrations"; -- Alert admins of registrations
|
||||
--"motd"; -- Send a message to users when they log in
|
||||
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
|
||||
-- jitsi
|
||||
"smacks";
|
||||
"carbons";
|
||||
"mam";
|
||||
"lastactivity";
|
||||
"offline";
|
||||
"pubsub";
|
||||
"adhoc";
|
||||
"websocket";
|
||||
"http_altconnect";
|
||||
}
|
||||
|
||||
-- domain mapper options, must at least have domain base set to use the mapper
|
||||
muc_mapper_domain_base = "jitsi.example.com";
|
||||
|
||||
-- These modules are auto-loaded, but should you want
|
||||
-- to disable them then uncomment them here:
|
||||
modules_disabled = {
|
||||
-- "offline"; -- Store offline messages
|
||||
-- "c2s"; -- Handle client connections
|
||||
-- "s2s"; -- Handle server-to-server connections
|
||||
}
|
||||
|
||||
-- Disable account creation by default, for security
|
||||
-- For more information see http://prosody.im/doc/creating_accounts
|
||||
allow_registration = false
|
||||
|
||||
-- These are the SSL/TLS-related settings. If you don't want
|
||||
-- to use SSL/TLS, you may comment or remove this
|
||||
ssl = {
|
||||
key = "/etc/prosody/certs/localhost.key";
|
||||
certificate = "/etc/prosody/certs/localhost.crt";
|
||||
}
|
||||
|
||||
-- Force clients to use encrypted connections? This option will
|
||||
-- prevent clients from authenticating unless they are using encryption.
|
||||
|
||||
-- c2s_require_encryption = true
|
||||
|
||||
-- Force certificate authentication for server-to-server connections?
|
||||
-- This provides ideal security, but requires servers you communicate
|
||||
-- with to support encryption AND present valid, trusted certificates.
|
||||
-- NOTE: Your version of LuaSec must support certificate verification!
|
||||
-- For more information see http://prosody.im/doc/s2s#security
|
||||
|
||||
-- s2s_secure_auth = false
|
||||
|
||||
-- Many servers don't support encryption or have invalid or self-signed
|
||||
-- certificates. You can list domains here that will not be required to
|
||||
-- authenticate using certificates. They will be authenticated using DNS.
|
||||
|
||||
--s2s_insecure_domains = { "gmail.com" }
|
||||
|
||||
-- Even if you leave s2s_secure_auth disabled, you can still require valid
|
||||
-- certificates for some domains by specifying a list here.
|
||||
|
||||
--s2s_secure_domains = { "jabber.org" }
|
||||
|
||||
-- Required for init scripts and prosodyctl
|
||||
pidfile = "/var/run/prosody/prosody.pid"
|
||||
|
||||
-- Select the authentication backend to use. The 'internal' providers
|
||||
-- use Prosody's configured data storage to store the authentication data.
|
||||
-- To allow Prosody to offer secure authentication mechanisms to clients, the
|
||||
-- default provider stores passwords in plaintext. If you do not trust your
|
||||
-- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
|
||||
-- for information about using the hashed backend.
|
||||
|
||||
-- authentication = "internal_plain"
|
||||
authentication = "internal_hashed"
|
||||
|
||||
-- Select the storage backend to use. By default Prosody uses flat files
|
||||
-- in its configured data directory, but it also supports more backends
|
||||
-- through modules. An "sql" backend is included by default, but requires
|
||||
-- additional dependencies. See http://prosody.im/doc/storage for more info.
|
||||
|
||||
--storage = "sql" -- Default is "internal"
|
||||
|
||||
-- For the "sql" backend, you can uncomment *one* of the below to configure:
|
||||
--sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename.
|
||||
--sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
|
||||
--sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
|
||||
|
||||
-- Logging configuration
|
||||
-- For advanced logging see http://prosody.im/doc/logging
|
||||
log = {
|
||||
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
|
||||
error = "/var/log/prosody/prosody.err";
|
||||
"*syslog";
|
||||
}
|
||||
|
||||
----------- Virtual hosts -----------
|
||||
-- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
|
||||
-- Settings under each VirtualHost entry apply *only* to that host.
|
||||
|
||||
--VirtualHost "localhost"
|
||||
|
||||
VirtualHost "jitsi.example.com"
|
||||
-- enabled = false -- Remove this line to enable this host
|
||||
authentication = "anonymous"
|
||||
-- Assign this host a certificate for TLS, otherwise it would use the one
|
||||
-- set in the global section (if any).
|
||||
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
|
||||
-- use the global one.
|
||||
ssl = {
|
||||
key = "/var/lib/prosody/jitsi.example.com.key";
|
||||
certificate = "/var/lib/prosody/jitsi.example.com.crt";
|
||||
}
|
||||
|
||||
c2s_require_encryption = false
|
||||
|
||||
VirtualHost "auth.jitsi.example.com"
|
||||
ssl = {
|
||||
key = "/var/lib/prosody/auth.jitsi.example.com.key";
|
||||
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
|
||||
}
|
||||
authentication = "internal_plain"
|
||||
|
||||
------ Components ------
|
||||
-- You can specify components to add hosts that provide special services,
|
||||
-- like multi-user conferences, and transports.
|
||||
-- For more information on components, see http://prosody.im/doc/components
|
||||
|
||||
---Set up a MUC (multi-user chat) room server on conference.example.com:
|
||||
--Component "conference.example.com" "muc"
|
||||
|
||||
-- Set up a SOCKS5 bytestream proxy for server-proxied file transfers:
|
||||
--Component "proxy.example.com" "proxy65"
|
||||
|
||||
---Set up an external component (default component port is 5347)
|
||||
--
|
||||
-- External components allow adding various services, such as gateways/
|
||||
-- transports to other networks like ICQ, MSN and Yahoo. For more info
|
||||
-- see: http://prosody.im/doc/components#adding_an_external_component
|
||||
--
|
||||
--Component "gateway.example.com"
|
||||
-- component_secret = "password"
|
||||
|
||||
Component "conference.jitsi.example.com" "muc"
|
||||
modules_enabled = { "muc_domain_mapper" }
|
||||
|
||||
Component "jitsi-videobridge.jitsi.example.com"
|
||||
component_secret = "IfGaish6"
|
||||
@@ -210,6 +210,7 @@ Checkout and configure Jitsi Meet:
|
||||
cd /srv
|
||||
git clone https://github.com/jitsi/jitsi-meet.git
|
||||
mv jitsi-meet/ jitsi.example.com
|
||||
cd jitsi.example.com
|
||||
npm install
|
||||
make
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@ Debian Wheezy and other older systems may require additional things to be done.
|
||||
|
||||
Also note that a recent default Ubuntu installation has only the `main` repository enabled, and Jitsi Meet needs packages from `universe`. Check your `/etc/apt/sources.list` file, and if `universe` is not present refer to [Ubuntu's documentation](https://help.ubuntu.com/community/Repositories/Ubuntu) on how to enable it. (Usually it amounts to copying the `main` lines and changing to `universe`.)
|
||||
|
||||
N.B.:
|
||||
N.B.:
|
||||
|
||||
a.) All commands are supposed to be run by root. If you are logged in as a regular user with sudo rights, please prepend ___sudo___ to each of the commands.
|
||||
|
||||
@@ -46,7 +46,7 @@ During the installation, you will be asked to enter the hostname of the Jitsi Me
|
||||
|
||||
This hostname (or IP address) will be used for virtualhost configuration inside the Jitsi Meet and also, you and your correspondents will be using it to access the web conferences.
|
||||
|
||||
### Generate a Let's Encrypt certificate
|
||||
### Generate a Let's Encrypt certificate
|
||||
|
||||
Simply run the following in your shell
|
||||
|
||||
@@ -109,7 +109,7 @@ Enjoy!
|
||||
## Uninstall
|
||||
|
||||
```sh
|
||||
apt-get purge jigasi jitsi-meet jitsi-meet-web-config jitsi-meet-prosody jitsi-meet-web jicofo jitsi-videobridge
|
||||
apt-get purge jigasi jitsi-meet jitsi-meet-web-config jitsi-meet-prosody jitsi-meet-turnserver jitsi-meet-web jicofo jitsi-videobridge
|
||||
```
|
||||
|
||||
Sometimes the following packages will fail to uninstall properly:
|
||||
|
||||