Compare commits
391 Commits
3579
...
android-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd409e4570 | ||
|
|
344244fa46 | ||
|
|
eb58f0e9de | ||
|
|
f1fbd60bbf | ||
|
|
27a59529c8 | ||
|
|
98c6b825fb | ||
|
|
54efd35081 | ||
|
|
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 | ||
|
|
8be02f9ca1 | ||
|
|
3c25a4c08c | ||
|
|
5ade0cad8b | ||
|
|
0fa6ffc439 | ||
|
|
c2ed296178 | ||
|
|
febd12b871 | ||
|
|
0a06e256b7 | ||
|
|
f295f60bea | ||
|
|
4a8f787519 | ||
|
|
d85b869934 | ||
|
|
35130f0736 | ||
|
|
1feff9709c | ||
|
|
1010f53a84 | ||
|
|
f7a526f488 | ||
|
|
245eb89b85 | ||
|
|
99de9d0bfa | ||
|
|
98698ba89a | ||
|
|
19d1e3829d | ||
|
|
612586ed1f | ||
|
|
2609e43f29 | ||
|
|
c5cd4f534c | ||
|
|
6e10ca5dd2 | ||
|
|
0fff1c3534 | ||
|
|
42271b1b89 | ||
|
|
f270b50972 | ||
|
|
ab4b6be9d7 | ||
|
|
c45ee0230f | ||
|
|
d210f2f2e7 | ||
|
|
13d78d6b49 | ||
|
|
34a71042c6 | ||
|
|
47ecf7d035 | ||
|
|
bbe8c52778 | ||
|
|
2011421e9d | ||
|
|
ce55952ca9 | ||
|
|
f93482e815 | ||
|
|
761ac6a730 | ||
|
|
11d3a343e5 | ||
|
|
9666bf836e | ||
|
|
a6d3b09796 | ||
|
|
bb0036fdab | ||
|
|
8dc0f30a49 | ||
|
|
1aed08f460 | ||
|
|
afccf6f06d | ||
|
|
5cd351a46f | ||
|
|
51f257e894 | ||
|
|
ac06892bb4 | ||
|
|
fd8473cb52 | ||
|
|
3f40257f89 | ||
|
|
14509adff2 | ||
|
|
c472537ecf | ||
|
|
d40fce741a | ||
|
|
944e8f8353 | ||
|
|
7d972a50f2 | ||
|
|
92e7be34e3 | ||
|
|
f5dba929a5 | ||
|
|
64d2885233 | ||
|
|
4ce65ae7a7 | ||
|
|
07bf95f838 | ||
|
|
3469d5dc4f | ||
|
|
8c0f942ae1 | ||
|
|
b8aa74f212 | ||
|
|
2a7c6681ad | ||
|
|
324a9eba91 | ||
|
|
fb1ed22c6c | ||
|
|
00c8409e31 | ||
|
|
1b43c22940 | ||
|
|
efddb36164 | ||
|
|
385e1c1047 | ||
|
|
3cc181a2e5 | ||
|
|
bcc1be675f | ||
|
|
d1be5742ba | ||
|
|
1b27e331da | ||
|
|
c1f7bf75c1 | ||
|
|
382ec011eb | ||
|
|
8a3ddd8596 | ||
|
|
738a199b4c | ||
|
|
40a1af5302 | ||
|
|
be5dba7eea | ||
|
|
eb15f73e59 | ||
|
|
2f7b485b8f | ||
|
|
3a885c893a | ||
|
|
6c4901a826 | ||
|
|
91c1c91950 | ||
|
|
1091ac7e7d | ||
|
|
0c0bd001e5 | ||
|
|
256994e1f8 | ||
|
|
f6fb859531 | ||
|
|
7deb2006c3 | ||
|
|
2aea24ffad | ||
|
|
b5aae0b58d | ||
|
|
d436825a45 | ||
|
|
5276cb6bc8 | ||
|
|
f863733dd3 | ||
|
|
2ccd4968a4 | ||
|
|
c233433243 | ||
|
|
6861f463b3 | ||
|
|
ac065f0225 | ||
|
|
b4b33c94dd | ||
|
|
c8753230a4 | ||
|
|
dbf569e29e | ||
|
|
27205e3119 | ||
|
|
f2fdef8361 | ||
|
|
d4dd5ab46a | ||
|
|
902da8cc4f | ||
|
|
c0a5e4f203 | ||
|
|
55ff9dbe80 | ||
|
|
bd99108e8e | ||
|
|
0c042b4078 | ||
|
|
743bbcb846 | ||
|
|
4ef2f0211a | ||
|
|
86f765a01f | ||
|
|
9e8b8313e0 | ||
|
|
f4a8115b00 | ||
|
|
a93bd422d3 | ||
|
|
c1598b7376 | ||
|
|
bc403adb46 | ||
|
|
1941275f93 | ||
|
|
6ae9bbe0c5 | ||
|
|
2c70388a9e | ||
|
|
e0815de2ad | ||
|
|
02e058370e | ||
|
|
8a7b795d37 | ||
|
|
d24ca796ad | ||
|
|
af2c61fd96 | ||
|
|
5a934c071a | ||
|
|
bfe4237430 | ||
|
|
84c60a5fdf | ||
|
|
cfc7210ac8 | ||
|
|
57ac951192 | ||
|
|
36fee03d5e | ||
|
|
14b747e0a4 | ||
|
|
c113b2e765 | ||
|
|
560e65da37 | ||
|
|
861cf7d842 | ||
|
|
dd23ed09ad | ||
|
|
64897b9c91 | ||
|
|
0dc8c687f2 | ||
|
|
c65d29d1a7 | ||
|
|
6aa895b679 | ||
|
|
dae451e6fa | ||
|
|
3c1f056d2a | ||
|
|
663e0a6693 |
79
.flowconfig
@@ -2,22 +2,30 @@
|
||||
; We fork some components by platform
|
||||
.*/*[.]android.js
|
||||
|
||||
; Ignore "BUCK" generated dirs
|
||||
<PROJECT_ROOT>/\.buckd/
|
||||
|
||||
; Ignore unexpected extra "@providesModule"
|
||||
.*/node_modules/.*/node_modules/fbjs/.*
|
||||
node_modules/react-native/Libraries/react-native/React.js
|
||||
|
||||
; Ignore duplicate module providers
|
||||
; For RN Apps installed via npm, "Libraries" folder is inside
|
||||
; "node_modules/react-native" but in the source repo it is in the root
|
||||
.*/Libraries/react-native/React.js
|
||||
node_modules/react-native/Libraries/react-native/React.js
|
||||
|
||||
; Flow doesn't support platforms
|
||||
.*/Libraries/Utilities/LoadingView.js
|
||||
|
||||
; Ignore polyfills
|
||||
.*/Libraries/polyfills/.*
|
||||
node_modules/react-native/Libraries/polyfills/.*
|
||||
|
||||
; Ignore metro
|
||||
.*/node_modules/metro/.*
|
||||
; These should not be required directly
|
||||
; require from fbjs/lib instead: require('fbjs/lib/warning')
|
||||
node_modules/warning/.*
|
||||
|
||||
; Flow doesn't support platforms
|
||||
.*/Libraries/Utilities/HMRLoadingView.js
|
||||
|
||||
[untyped]
|
||||
.*/node_modules/@react-native-community/cli/.*/.*
|
||||
|
||||
; Ignore packages in node_modules which we (i.e. the jitsi-meet project) have
|
||||
; seen to cause errors and we have chosen not to fix.
|
||||
@@ -40,6 +48,18 @@ emoji=true
|
||||
esproposal.optional_chaining=enable
|
||||
esproposal.nullish_coalescing=enable
|
||||
|
||||
; We (i.e. the jitsi-meet project) are using the haste module system on Web as
|
||||
; well, not only on React Native. Unfortunately, Flow does not support .web.js
|
||||
; by default. Override Flow's defaults to include .web.js as well. Technically,
|
||||
; we have .native.js as well so the choice of .web.js may lead to errors.
|
||||
; Practically though, it is a potential future problem that we do not have at
|
||||
; the time of this writing.
|
||||
module.file_ext=.web.js
|
||||
; Flow's defaults:
|
||||
module.file_ext=.js
|
||||
module.file_ext=.json
|
||||
module.file_ext=.ios.js
|
||||
|
||||
module.system=haste
|
||||
module.system.haste.use_name_reducers=true
|
||||
# get basename
|
||||
@@ -52,8 +72,11 @@ module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
|
||||
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
|
||||
module.system.haste.paths.blacklist=.*/__tests__/.*
|
||||
module.system.haste.paths.blacklist=.*/__mocks__/.*
|
||||
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
|
||||
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
|
||||
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/RNTester/.*
|
||||
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/IntegrationTests/.*
|
||||
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/react-native/react-native-implementation.js
|
||||
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
|
||||
|
||||
munge_underscores=true
|
||||
|
||||
@@ -64,22 +87,32 @@ suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMeProps
|
||||
suppress_type=$FlowFixMeState
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
; We (i.e. the jitsi-meet project) are using the haste module system on Web as
|
||||
; well, not only on React Native. Unfortunately, Flow does not support .web.js
|
||||
; by default. Override Flow's defaults to include .web.js as well. Technically,
|
||||
; we have .native.js as well so the choice of .web.js may lead to errors.
|
||||
; Practically though, it is a potential future problem that we do not have at
|
||||
; the time of this writing.
|
||||
module.file_ext=.web.js
|
||||
; Flow's defaults:
|
||||
module.file_ext=.js
|
||||
module.file_ext=.jsx
|
||||
module.file_ext=.json
|
||||
[lints]
|
||||
sketchy-null-number=warn
|
||||
sketchy-null-mixed=warn
|
||||
sketchy-number=warn
|
||||
untyped-type-import=warn
|
||||
nonstrict-import=warn
|
||||
deprecated-type=warn
|
||||
unsafe-getters-setters=warn
|
||||
inexact-spread=warn
|
||||
unnecessary-invariant=warn
|
||||
signature-verification-failure=warn
|
||||
deprecated-utility=error
|
||||
|
||||
[strict]
|
||||
deprecated-type
|
||||
nonstrict-import
|
||||
sketchy-null
|
||||
unclear-type
|
||||
unsafe-getters-setters
|
||||
untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.92.0
|
||||
^0.104.0
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/4-security-issues.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Security issues
|
||||
about: Please email security@jitsi.org
|
||||
|
||||
---
|
||||
|
||||
We take security very seriously and develop all Jitsi projects to be secure and safe.
|
||||
|
||||
If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org.
|
||||
|
||||
We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.
|
||||
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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
osx_image: xcode10.2
|
||||
osx_image: xcode11.1
|
||||
language: objective-c
|
||||
script:
|
||||
- "./ios/travis-ci/build-ipa.sh"
|
||||
after_script:
|
||||
- sleep 10
|
||||
|
||||
12
Makefile
@@ -3,6 +3,7 @@ CLEANCSS = ./node_modules/.bin/cleancss
|
||||
DEPLOY_DIR = libs
|
||||
LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
|
||||
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
|
||||
RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/
|
||||
NODE_SASS = ./node_modules/.bin/node-sass
|
||||
NPM = npm
|
||||
OUTPUT_DIR = .
|
||||
@@ -20,7 +21,7 @@ compile:
|
||||
clean:
|
||||
rm -fr $(BUILD_DIR)
|
||||
|
||||
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
|
||||
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
|
||||
|
||||
deploy-init:
|
||||
rm -fr $(DEPLOY_DIR)
|
||||
@@ -47,6 +48,8 @@ deploy-appbundle:
|
||||
$(BUILD_DIR)/analytics-ga.min.map \
|
||||
$(BUILD_DIR)/video-blur-effect.min.js \
|
||||
$(BUILD_DIR)/video-blur-effect.min.map \
|
||||
$(BUILD_DIR)/rnnoise-processor.min.js \
|
||||
$(BUILD_DIR)/rnnoise-processor.min.map \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-lib-jitsi-meet:
|
||||
@@ -63,6 +66,11 @@ deploy-libflac:
|
||||
$(LIBFLAC_DIR)/libflac4-1.3.2.min.js.mem \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-rnnoise-binary:
|
||||
cp \
|
||||
$(RNNOISE_WASM_DIR)/rnnoise.wasm \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-css:
|
||||
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
|
||||
$(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
|
||||
@@ -71,7 +79,7 @@ deploy-css:
|
||||
deploy-local:
|
||||
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
|
||||
|
||||
dev: deploy-init deploy-css deploy-lib-jitsi-meet deploy-libflac
|
||||
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac
|
||||
$(WEBPACK_DEV_SERVER)
|
||||
|
||||
source-package:
|
||||
|
||||
101
README.md
@@ -27,86 +27,28 @@ 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)
|
||||
|
||||
## Building the sources
|
||||
[<img src="resources/img/appstore-badge.png" height="50">](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
|
||||
|
||||
Node.js >= 10 and npm >= 6 are required.
|
||||
You can also sign up for our open beta testing here:
|
||||
|
||||
On Debian/Ubuntu systems, the required packages can be installed with:
|
||||
```
|
||||
sudo apt-get install npm nodejs
|
||||
cd jitsi-meet
|
||||
npm install
|
||||
```
|
||||
* [Android](https://play.google.com/apps/testing/org.jitsi.meet)
|
||||
* [iOS](https://testflight.apple.com/join/isy6ja7S)
|
||||
|
||||
To build the Jitsi Meet application, just type
|
||||
```
|
||||
make
|
||||
```
|
||||
## Development
|
||||
|
||||
### Working with the library sources (lib-jitsi-meet)
|
||||
|
||||
By default the library is build from its git repository sources. The default dependency path in package.json is :
|
||||
```json
|
||||
"lib-jitsi-meet": "jitsi/lib-jitsi-meet",
|
||||
```
|
||||
|
||||
To work with local copy you must change the path to:
|
||||
```json
|
||||
"lib-jitsi-meet": "file:///Users/name/local-lib-jitsi-meet-copy",
|
||||
```
|
||||
|
||||
To make the project you must force it to take the sources as 'npm update':
|
||||
```
|
||||
npm install lib-jitsi-meet --force && make
|
||||
```
|
||||
|
||||
Or if you are making only changes to the library:
|
||||
```
|
||||
npm install lib-jitsi-meet --force && make deploy-lib-jitsi-meet
|
||||
```
|
||||
|
||||
Alternative way is to use [npm link](https://docs.npmjs.com/cli/link).
|
||||
It allows to link `lib-jitsi-meet` dependency to local source in few steps:
|
||||
|
||||
```bash
|
||||
cd lib-jitsi-meet
|
||||
|
||||
#### create global symlink for lib-jitsi-meet package
|
||||
npm link
|
||||
|
||||
cd ../jitsi-meet
|
||||
|
||||
#### create symlink from the local node_modules folder to the global lib-jitsi-meet symlink
|
||||
npm link lib-jitsi-meet
|
||||
```
|
||||
|
||||
After changes in local `lib-jitsi-meet` repository, you can rebuild it with `npm run install` and your `jitsi-meet` repository will use that modified library.
|
||||
Note: when using node version 4.x, the make file of jitsi-meet do npm update which will delete the link. It is no longer the case with version 6.x.
|
||||
|
||||
If you do not want to use local repository anymore you should run
|
||||
```bash
|
||||
cd jitsi-meet
|
||||
npm unlink lib-jitsi-meet
|
||||
npm install
|
||||
```
|
||||
### Running with webpack-dev-server for development
|
||||
|
||||
Use it at the CLI, type
|
||||
```
|
||||
make dev
|
||||
```
|
||||
|
||||
By default the backend deployment used is `beta.meet.jit.si`. You can point the Jitsi-Meet app at a different backend by using a proxy server. To do this, set the WEBPACK_DEV_SERVER_PROXY_TARGET variable:
|
||||
```
|
||||
export WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com
|
||||
make dev
|
||||
```
|
||||
|
||||
The app should be running at https://localhost:8080/
|
||||
For web development see [here](doc/development.md), and for mobile see [here](doc/mobile.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -118,7 +60,8 @@ see our [guidelines for contributing](CONTRIBUTING.md).
|
||||
Jitsi Meet provides a very flexible way of embedding in external applications by using the [Jitsi Meet API](doc/api.md).
|
||||
|
||||
## Security
|
||||
WebRTC does not provide a way of conducting multi-party conversations with end-to-end encryption.
|
||||
|
||||
WebRTC does not (yet) provide a way of conducting multi-party conversations with end-to-end encryption.
|
||||
Unless you consistently compare DTLS fingerprints with your peers vocally, the same goes for one-to-one calls.
|
||||
As a result, your stream is encrypted on the network but decrypted on the machine that hosts the bridge when using Jitsi Meet.
|
||||
|
||||
@@ -130,9 +73,13 @@ Jitsi Meet in terms of security.
|
||||
The [meet.jit.si](https://meet.jit.si) service is maintained by the Jitsi team
|
||||
at [8x8](https://8x8.com).
|
||||
|
||||
## Mobile app
|
||||
Jitsi Meet is also available as a React Native app for Android and iOS.
|
||||
Instructions on how to build it can be found [here](doc/mobile.md).
|
||||
## Security issues
|
||||
|
||||
We take security very seriously and develop all Jitsi projects to be secure and safe.
|
||||
|
||||
If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org.
|
||||
|
||||
**We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.**
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
@@ -32,14 +32,23 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules-debug.pro'
|
||||
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}"
|
||||
@@ -70,8 +79,10 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "com.android.support:support-v4:${rootProject.ext.supportLibVersion}"
|
||||
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
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'
|
||||
@@ -85,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
@@ -1,5 +0,0 @@
|
||||
-include proguard-rules.pro
|
||||
|
||||
# Disabling obfuscation is useful if you collect stack traces from production crashes
|
||||
# (unless you are using a system that supports de-obfuscate the stack traces).
|
||||
-dontobfuscate
|
||||
14
android/app/proguard-rules.pro
vendored
@@ -9,13 +9,6 @@
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# React Native
|
||||
|
||||
# Keep our interfaces so they can be used by other ProGuard rules.
|
||||
@@ -90,3 +83,10 @@
|
||||
-dontwarn javax.servlet.**
|
||||
|
||||
# ^^^ We added the above when we switched minifyEnabled on.
|
||||
|
||||
# Rule to avoid build errors related to SVGs.
|
||||
-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,6 +1,5 @@
|
||||
/*
|
||||
* Copyright @ 2018-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.
|
||||
@@ -21,14 +20,13 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jitsi.meet.sdk.JitsiMeet;
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivity;
|
||||
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions;
|
||||
import org.jitsi.meet.sdk.JitsiMeetUserInfo;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.MalformedURLException;
|
||||
@@ -92,6 +90,7 @@ public class MainActivity extends JitsiMeetActivity {
|
||||
= new JitsiMeetConferenceOptions.Builder()
|
||||
.setWelcomePageEnabled(true)
|
||||
.setServerURL(buildURL("https://meet.jit.si"))
|
||||
.setFeatureFlag("call-integration.enabled", false)
|
||||
.build();
|
||||
JitsiMeet.setDefaultConferenceOptions(defaultOptions);
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -25,8 +25,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
// React Native (JS, Obj-C sources, Android binaries) is installed from
|
||||
// npm.
|
||||
// React Native (JS, Obj-C sources, Android binaries) is installed from npm.
|
||||
maven { url "$rootDir/../node_modules/react-native/android" }
|
||||
}
|
||||
|
||||
@@ -74,6 +73,13 @@ allprojects {
|
||||
def versionQualifierNumber = (int)(((new Date().getTime()/1000) - 1546297200) / 10)
|
||||
|
||||
afterEvaluate { project ->
|
||||
if (project.plugins.hasPlugin('android') || project.plugins.hasPlugin('android-library')) {
|
||||
project.android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
}
|
||||
}
|
||||
|
||||
if (project.name.startsWith('react-native-')) {
|
||||
def npmManifest = project.file('../package.json')
|
||||
def json = new JsonSlurper().parseText(npmManifest.text)
|
||||
@@ -83,17 +89,6 @@ allprojects {
|
||||
|
||||
project.version = "${json.version}-jitsi-${versionQualifierNumber}"
|
||||
|
||||
project.android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
if (rootProject.ext.has('buildToolsVersion')) {
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
task androidSourcesJar(type: Jar) {
|
||||
classifier = 'sources'
|
||||
from android.sourceSets.main.java.source
|
||||
@@ -168,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 |
@@ -17,5 +17,8 @@
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
appVersion=19.3.0
|
||||
sdkVersion=2.2.2
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
appVersion=20.0.1
|
||||
sdkVersion=2.6.1
|
||||
|
||||
@@ -9,7 +9,8 @@ THE_MVN_REPO=${MVN_REPO:-${1:-$DEFAULT_MVN_REPO}}
|
||||
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)
|
||||
RN_VERSION=$(jq -r '.version' ${THIS_DIR}/../../node_modules/react-native/package.json)
|
||||
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
|
||||
@@ -37,14 +38,22 @@ if [[ $MVN_HTTP == 1 ]]; then
|
||||
-DgeneratePom=false \
|
||||
-DpomFile=react-native-${RN_VERSION}.pom || true
|
||||
popd
|
||||
# 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=hermes-release.aar \
|
||||
-Dpackaging=aar \
|
||||
-DgroupId=com.facebook \
|
||||
-DartifactId=hermes \
|
||||
-Dversion=${HERMES_VERSION} \
|
||||
-DgeneratePom=true || true
|
||||
popd
|
||||
else
|
||||
# Check if an SDK with that same version has already been released
|
||||
if [[ -d ${MVN_REPO}/org/jitsi/react/jitsi-meet-sdk/${SDK_VERSION} ]]; then
|
||||
echo "There is already a release with that version in the Maven repo!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First push React Native, if necessary
|
||||
# Push React Native, if necessary
|
||||
if [[ ! -d ${MVN_REPO}/com/facebook/react/react-native/${RN_VERSION} ]]; then
|
||||
echo "Pushing React Native ${RN_VERSION} to the Maven repo"
|
||||
pushd ${THIS_DIR}/../../node_modules/react-native/android/com/facebook/react/react-native/${RN_VERSION}
|
||||
@@ -57,6 +66,28 @@ else
|
||||
-DpomFile=react-native-${RN_VERSION}.pom
|
||||
popd
|
||||
fi
|
||||
|
||||
# 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=hermes-release.aar \
|
||||
-Dpackaging=aar \
|
||||
-DgroupId=com.facebook \
|
||||
-DartifactId=hermes \
|
||||
-Dversion=${HERMES_VERSION} \
|
||||
-DgeneratePom=true
|
||||
popd
|
||||
fi
|
||||
|
||||
# Check if an SDK with that same version has already been released
|
||||
if [[ -d ${MVN_REPO}/org/jitsi/react/jitsi-meet-sdk/${SDK_VERSION} ]]; then
|
||||
echo "There is already a release with that version in the Maven repo!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Now build and publish the Jitsi Meet SDK and its dependencies
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
@@ -31,34 +33,48 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/libc++_shared.so'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation "com.android.support:support-v4:${rootProject.ext.supportLibVersion}"
|
||||
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
|
||||
implementation 'androidx.legacy:legacy-support-v4: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:+'
|
||||
|
||||
// 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'
|
||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
||||
|
||||
if (!rootProject.ext.libreBuild) {
|
||||
implementation 'com.amplitude:android-sdk:2.14.1'
|
||||
implementation(project(":react-native-google-signin")) {
|
||||
exclude group: 'com.google.android.gms'
|
||||
exclude group: 'com.android.support'
|
||||
exclude group: 'androidx'
|
||||
}
|
||||
}
|
||||
|
||||
implementation project(':react-native-background-timer')
|
||||
implementation project(':react-native-calendar-events')
|
||||
implementation project(':react-native-community-async-storage')
|
||||
implementation project(':react-native-community_netinfo')
|
||||
implementation project(':react-native-immersive')
|
||||
implementation project(':react-native-keep-awake')
|
||||
implementation project(':react-native-linear-gradient')
|
||||
implementation project(':react-native-sound')
|
||||
implementation project(':react-native-vector-icons')
|
||||
implementation project(':react-native-svg')
|
||||
implementation project(':react-native-webrtc')
|
||||
implementation project(':react-native-webview')
|
||||
|
||||
@@ -136,13 +152,6 @@ android.libraryVariants.all { def variant ->
|
||||
mergeAssetsTask.doLast {
|
||||
def assetsDir = mergeAssetsTask.outputDir
|
||||
|
||||
// Bundle fonts
|
||||
//
|
||||
copy {
|
||||
from("${projectDir}/../../fonts/jitsi.ttf")
|
||||
into("${assetsDir}/fonts")
|
||||
}
|
||||
|
||||
// Bundle sounds
|
||||
//
|
||||
copy {
|
||||
@@ -215,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;
|
||||
@@ -24,6 +28,7 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
import com.amplitude.api.Amplitude;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -48,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,7 +103,7 @@ class AmplitudeModule
|
||||
JSONObject eventProps = new JSONObject(eventPropsString);
|
||||
Amplitude.getInstance(instanceName).logEvent(eventType, eventProps);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
JitsiMeetLogger.e(e, "Error logging event");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,17 +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 android.support.annotation.RequiresApi;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
@@ -37,6 +28,8 @@ import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
@@ -60,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";
|
||||
|
||||
/**
|
||||
@@ -74,174 +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:
|
||||
Log.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;
|
||||
Log.d(TAG, "Available audio devices: " +
|
||||
availableDevices.toString());
|
||||
|
||||
// Reset user selection
|
||||
userSelectedDevice = null;
|
||||
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* {@link Runnable} for running update operation on the main thread.
|
||||
*/
|
||||
private final Runnable updateAudioRouteRunner
|
||||
= new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
};
|
||||
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Audio mode currently in use.
|
||||
@@ -251,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.
|
||||
@@ -271,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.
|
||||
@@ -295,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -360,7 +176,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
data.pushMap(deviceInfo);
|
||||
}
|
||||
ReactInstanceManagerHolder.emitEvent(DEVICE_CHANGE_EVENT, data);
|
||||
Log.i(TAG, "Updating audio device list");
|
||||
JitsiMeetLogger.i(TAG + " Updating audio device list");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -376,133 +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);
|
||||
Log.d(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: {
|
||||
Log.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: {
|
||||
Log.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);
|
||||
}
|
||||
|
||||
@@ -517,13 +236,13 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
@Override
|
||||
public void run() {
|
||||
if (!availableDevices.contains(device)) {
|
||||
Log.d(TAG, "Audio device not available: " + device);
|
||||
JitsiMeetLogger.w(TAG + " Audio device not available: " + device);
|
||||
userSelectedDevice = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode != -1) {
|
||||
Log.d(TAG, "User selected device set to: " + device);
|
||||
JitsiMeetLogger.i(TAG + " User selected device set to: " + device);
|
||||
userSelectedDevice = device;
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
@@ -531,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.
|
||||
*
|
||||
@@ -594,79 +273,28 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
success = updateAudioRoute(mode);
|
||||
} catch (Throwable e) {
|
||||
success = false;
|
||||
Log.e(
|
||||
TAG,
|
||||
"Failed to update audio route for mode: " + mode,
|
||||
e);
|
||||
JitsiMeetLogger.e(e, TAG + " Failed to update audio route for mode: " + mode);
|
||||
}
|
||||
if (success) {
|
||||
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) {
|
||||
Log.d(TAG, "Audio devices added");
|
||||
onAudioDeviceChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(
|
||||
AudioDeviceInfo[] removedDevices) {
|
||||
Log.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) {
|
||||
Log.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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -677,16 +305,13 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
* {@code false}, otherwise.
|
||||
*/
|
||||
private boolean updateAudioRoute(int mode) {
|
||||
Log.d(TAG, "Update audio route for mode: " + 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;
|
||||
|
||||
@@ -694,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) {
|
||||
Log.d(TAG, "Audio focus request failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
|
||||
boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
|
||||
|
||||
@@ -722,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;
|
||||
}
|
||||
|
||||
@@ -734,15 +344,99 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||
}
|
||||
|
||||
selectedDevice = audioDevice;
|
||||
Log.d(TAG, "Selected audio device: " + 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ package org.jitsi.meet.sdk;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
@@ -111,8 +111,7 @@ public abstract class BaseReactView<ListenerT>
|
||||
|
||||
setBackgroundColor(BACKGROUND_COLOR);
|
||||
|
||||
ReactInstanceManagerHolder.initReactInstanceManager(
|
||||
((Activity) context).getApplication());
|
||||
ReactInstanceManagerHolder.initReactInstanceManager((Activity)context);
|
||||
|
||||
// Hook this BaseReactView into ExternalAPI.
|
||||
externalAPIScope = UUID.randomUUID().toString();
|
||||
|
||||
@@ -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.
|
||||
@@ -24,7 +24,8 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.util.Log;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
/**
|
||||
* Helper class to detect and handle Bluetooth device changes. It monitors
|
||||
@@ -32,68 +33,43 @@ import android.util.Log;
|
||||
* 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()) {
|
||||
Log.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) {
|
||||
Log.w(AudioModeModule.TAG, "Device doesn't support Bluetooth");
|
||||
JitsiMeetLogger.w(TAG + " Device doesn't support Bluetooth");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -103,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();
|
||||
@@ -119,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) {
|
||||
@@ -148,9 +108,7 @@ class BluetoothHeadsetMonitor {
|
||||
switch (state) {
|
||||
case BluetoothHeadset.STATE_CONNECTED:
|
||||
case BluetoothHeadset.STATE_DISCONNECTED:
|
||||
Log.d(
|
||||
AudioModeModule.TAG,
|
||||
"BT headset connection state changed: " + state);
|
||||
JitsiMeetLogger.d(TAG + " BT headset connection state changed: " + state);
|
||||
updateDevices();
|
||||
break;
|
||||
}
|
||||
@@ -158,15 +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:
|
||||
Log.d(
|
||||
AudioModeModule.TAG,
|
||||
"BT SCO connection state changed: " + state);
|
||||
JitsiMeetLogger.d(TAG + " BT SCO connection state changed: " + state);
|
||||
updateDevices();
|
||||
break;
|
||||
}
|
||||
@@ -174,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.telecom.CallAudioState;
|
||||
import android.telecom.Connection;
|
||||
import android.telecom.ConnectionRequest;
|
||||
@@ -14,12 +13,14 @@ import android.telecom.PhoneAccount;
|
||||
import android.telecom.PhoneAccountHandle;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.telecom.VideoProfile;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -122,16 +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 {
|
||||
Log.e(TAG, String.format(
|
||||
"setConnectionActive - no connection for UUID: %s",
|
||||
callUUID));
|
||||
JitsiMeetLogger.w("%s setConnectionActive - no connection for UUID: %s", TAG, callUUID);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +164,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
connection.setDisconnected(cause);
|
||||
connection.destroy();
|
||||
} else {
|
||||
Log.e(TAG, "endCall no connection for UUID: " + callUUID);
|
||||
JitsiMeetLogger.e(TAG + " endCall no connection for UUID: " + callUUID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,15 +196,14 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
boolean hasVideo
|
||||
= callState.getBoolean(ConnectionImpl.KEY_HAS_VIDEO);
|
||||
|
||||
Log.d(TAG, String.format(
|
||||
"updateCall: %s hasVideo: %s", callUUID, hasVideo));
|
||||
JitsiMeetLogger.i(" %s updateCall: %s hasVideo: %s", TAG, callUUID, hasVideo);
|
||||
connection.setVideoState(
|
||||
hasVideo
|
||||
? VideoProfile.STATE_BIDIRECTIONAL
|
||||
: VideoProfile.STATE_AUDIO_ONLY);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "updateCall no connection for UUID: " + callUUID);
|
||||
JitsiMeetLogger.e(TAG + " updateCall no connection for UUID: " + callUUID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,13 +239,11 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
= unregisterStartCallPromise(connection.getCallUUID());
|
||||
|
||||
if (startCallPromise != null) {
|
||||
Log.d(TAG,
|
||||
"onCreateOutgoingConnection " + connection.getCallUUID());
|
||||
JitsiMeetLogger.d(TAG + " onCreateOutgoingConnection " + connection.getCallUUID());
|
||||
startCallPromise.resolve(null);
|
||||
} else {
|
||||
Log.e(TAG, String.format(
|
||||
"onCreateOutgoingConnection: no start call Promise for %s",
|
||||
connection.getCallUUID()));
|
||||
JitsiMeetLogger.e(
|
||||
TAG + " onCreateOutgoingConnection: no start call Promise for " + connection.getCallUUID());
|
||||
}
|
||||
|
||||
return connection;
|
||||
@@ -268,7 +267,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
PhoneAccountHandle theAccountHandle = request.getAccountHandle();
|
||||
String callUUID = theAccountHandle.getId();
|
||||
|
||||
Log.e(TAG, "onCreateOutgoingConnectionFailed " + callUUID);
|
||||
JitsiMeetLogger.e(TAG + " onCreateOutgoingConnectionFailed " + callUUID);
|
||||
|
||||
if (callUUID != null) {
|
||||
Promise startCallPromise = unregisterStartCallPromise(callUUID);
|
||||
@@ -278,12 +277,10 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
"CREATE_OUTGOING_CALL_FAILED",
|
||||
"The request has been denied by the system");
|
||||
} else {
|
||||
Log.e(TAG, String.format(
|
||||
"startCallFailed - no start call Promise for UUID: %s",
|
||||
callUUID));
|
||||
JitsiMeetLogger.e(TAG + " startCallFailed - no start call Promise for UUID: " + callUUID);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "onCreateOutgoingConnectionFailed - no call UUID");
|
||||
JitsiMeetLogger.e(TAG + " onCreateOutgoingConnectionFailed - no call UUID");
|
||||
}
|
||||
|
||||
unregisterPhoneAccount(theAccountHandle);
|
||||
@@ -295,10 +292,10 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
if (phoneAccountHandle != null) {
|
||||
telecom.unregisterPhoneAccount(phoneAccountHandle);
|
||||
} else {
|
||||
Log.e(TAG, "unregisterPhoneAccount - account handle is null");
|
||||
JitsiMeetLogger.e(TAG + " unregisterPhoneAccount - account handle is null");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "unregisterPhoneAccount - telecom is null");
|
||||
JitsiMeetLogger.e(TAG + " unregisterPhoneAccount - telecom is null");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +354,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
*/
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
Log.d(TAG, "onDisconnect " + getCallUUID());
|
||||
JitsiMeetLogger.i(TAG + " onDisconnect " + getCallUUID());
|
||||
WritableNativeMap data = new WritableNativeMap();
|
||||
data.putString("callUUID", getCallUUID());
|
||||
ReactInstanceManagerHolder.emitEvent(
|
||||
@@ -377,7 +374,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
*/
|
||||
@Override
|
||||
public void onAbort() {
|
||||
Log.d(TAG, "onAbort " + getCallUUID());
|
||||
JitsiMeetLogger.i(TAG + " onAbort " + getCallUUID());
|
||||
WritableNativeMap data = new WritableNativeMap();
|
||||
data.putString("callUUID", getCallUUID());
|
||||
ReactInstanceManagerHolder.emitEvent(
|
||||
@@ -395,9 +392,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
// What ?! Android will still call this method even if we do not add
|
||||
// the HOLD capability, so do the same thing as on abort.
|
||||
// TODO implement HOLD
|
||||
Log.d(TAG, String.format(
|
||||
"onHold %s - HOLD is not supported, aborting the call...",
|
||||
getCallUUID()));
|
||||
JitsiMeetLogger.w(TAG + " onHold %s - HOLD is not supported, aborting the call...", getCallUUID());
|
||||
this.onAbort();
|
||||
}
|
||||
|
||||
@@ -410,12 +405,10 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
*/
|
||||
@Override
|
||||
public void onCallAudioStateChanged(CallAudioState state) {
|
||||
Log.d(TAG, "onCallAudioStateChanged: " + state);
|
||||
AudioModeModule audioModeModule
|
||||
= ReactInstanceManagerHolder
|
||||
.getNativeModule(AudioModeModule.class);
|
||||
if (audioModeModule != null) {
|
||||
audioModeModule.onCallAudioStateChange(state);
|
||||
JitsiMeetLogger.d(TAG + " onCallAudioStateChanged: " + state);
|
||||
RNConnectionService module = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
|
||||
if (module != null) {
|
||||
module.onCallAudioStateChange(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,10 +419,8 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
*/
|
||||
@Override
|
||||
public void onStateChanged(int state) {
|
||||
Log.d(TAG,
|
||||
String.format("onStateChanged: %s %s",
|
||||
Connection.stateToString(state),
|
||||
getCallUUID()));
|
||||
JitsiMeetLogger.d(
|
||||
"%s onStateChanged: %s %s", TAG, Connection.stateToString(state), getCallUUID());
|
||||
|
||||
if (state == STATE_DISCONNECTED) {
|
||||
removeConnection(this);
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
/**
|
||||
* Module implementing an API for sending events from JavaScript to native code.
|
||||
*/
|
||||
@@ -76,11 +76,11 @@ class ExternalAPIModule
|
||||
BaseReactView view = BaseReactView.findViewByExternalAPIScope(scope);
|
||||
|
||||
if (view != null) {
|
||||
Log.d(TAG, "Sending event: " + name + " with data: " + data);
|
||||
JitsiMeetLogger.d(TAG + " Sending event: " + name + " with data: " + data);
|
||||
try {
|
||||
view.onExternalAPIEvent(name, data);
|
||||
} catch(Exception e) {
|
||||
Log.e(TAG, "onExternalAPIEvent: error sending event", e);
|
||||
JitsiMeetLogger.e(e, TAG + " onExternalAPIEvent: error sending event");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright @ 2019-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 com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.squareup.duktape.Duktape;
|
||||
|
||||
@ReactModule(name = JavaScriptSandboxModule.NAME)
|
||||
class JavaScriptSandboxModule extends ReactContextBaseJavaModule {
|
||||
public static final String NAME = "JavaScriptSandbox";
|
||||
|
||||
public JavaScriptSandboxModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given code in a Duktape VM.
|
||||
* @param code - The code that needs to evaluated.
|
||||
* @param promise - Resolved with the output in case of success or rejected with an exception
|
||||
* in case of failure.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void evaluate(String code, Promise promise) {
|
||||
Duktape vm = Duktape.create();
|
||||
try {
|
||||
Object res = vm.evaluate(code);
|
||||
promise.resolve(res.toString());
|
||||
} catch (Throwable tr) {
|
||||
promise.reject(tr);
|
||||
} finally {
|
||||
vm.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ public class JitsiMeet {
|
||||
}
|
||||
|
||||
public static void setDefaultConferenceOptions(JitsiMeetConferenceOptions options) {
|
||||
if (options != null && options.getRoom() != null) {
|
||||
throw new RuntimeException("'room' must be null in the default conference options");
|
||||
}
|
||||
defaultConferenceOptions = options;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.facebook.react.modules.core.PermissionListener;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -208,19 +209,19 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
|
||||
@Override
|
||||
public void onConferenceJoined(Map<String, Object> data) {
|
||||
Log.d(TAG, "Conference joined: " + data);
|
||||
JitsiMeetLogger.i("Conference joined: " + data);
|
||||
// Launch the service for the ongoing notification.
|
||||
JitsiMeetOngoingConferenceService.launch(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConferenceTerminated(Map<String, Object> data) {
|
||||
Log.d(TAG, "Conference terminated: " + data);
|
||||
JitsiMeetLogger.i("Conference terminated: " + data);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConferenceWillJoin(Map<String, Object> data) {
|
||||
Log.d(TAG, "Conference will join: " + data);
|
||||
JitsiMeetLogger.i("Conference will join: " + data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.os.Build;
|
||||
import com.calendarevents.CalendarEventsPackage;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.core.PermissionListener;
|
||||
|
||||
/**
|
||||
@@ -117,7 +118,13 @@ public class JitsiMeetActivityDelegate {
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
reactInstanceManager.onHostPause(activity);
|
||||
// Try to avoid a crash because some devices trip on this assert:
|
||||
// https://github.com/facebook/react-native/blob/df4e67fe75d781d1eb264128cadf079989542755/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java#L512
|
||||
// Why this happens is a mystery wrapped in an enigma.
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext != null && activity == reactContext.getCurrentActivity()) {
|
||||
reactInstanceManager.onHostPause(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import com.facebook.react.modules.core.PermissionAwareActivity;
|
||||
|
||||
|
||||
@@ -72,6 +72,46 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
*/
|
||||
private JitsiMeetUserInfo userInfo;
|
||||
|
||||
public URL getServerURL() {
|
||||
return serverURL;
|
||||
}
|
||||
|
||||
public String getRoom() {
|
||||
return room;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public Bundle getColorScheme() {
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
public Bundle getFeatureFlags() {
|
||||
return featureFlags;
|
||||
}
|
||||
|
||||
public boolean getAudioMuted() {
|
||||
return audioMuted;
|
||||
}
|
||||
|
||||
public boolean getAudioOnly() {
|
||||
return audioOnly;
|
||||
}
|
||||
|
||||
public boolean getVideoMuted() {
|
||||
return videoMuted;
|
||||
}
|
||||
|
||||
public JitsiMeetUserInfo getUserInfo() {
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used to build the immutable {@link JitsiMeetConferenceOptions} object.
|
||||
*/
|
||||
@@ -309,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ package org.jitsi.meet.sdk;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -23,7 +23,8 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
|
||||
/**
|
||||
@@ -55,7 +56,7 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
componentName = context.startService(intent);
|
||||
}
|
||||
if (componentName == null) {
|
||||
Log.w(TAG, "Ongoing conference service not started");
|
||||
JitsiMeetLogger.w(TAG + " Ongoing conference service not started");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,24 +87,24 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
final String action = intent.getAction();
|
||||
if (action.equals(Actions.START)) {
|
||||
if (Actions.START.equals(action)) {
|
||||
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
|
||||
if (notification == null) {
|
||||
stopSelf();
|
||||
Log.w(TAG, "Couldn't start service, notification is null");
|
||||
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
|
||||
} else {
|
||||
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
|
||||
Log.i(TAG, "Service started");
|
||||
JitsiMeetLogger.i(TAG + " Service started");
|
||||
}
|
||||
} else if (action.equals(Actions.HANGUP)) {
|
||||
Log.i(TAG, "Hangup requested");
|
||||
} else if (Actions.HANGUP.equals(action)) {
|
||||
JitsiMeetLogger.i(TAG + " Hangup requested");
|
||||
// Abort all ongoing calls
|
||||
if (AudioModeModule.useConnectionService()) {
|
||||
ConnectionService.abortConnections();
|
||||
}
|
||||
stopSelf();
|
||||
} else {
|
||||
Log.w(TAG, "Unknown action received: " + action);
|
||||
JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@@ -114,7 +115,7 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
public void onCurrentConferenceChanged(String conferenceUrl) {
|
||||
if (conferenceUrl == null) {
|
||||
stopSelf();
|
||||
Log.i(TAG, "Service stopped");
|
||||
JitsiMeetLogger.i(TAG + "Service stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.util.Log;
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
|
||||
@@ -37,7 +37,7 @@ class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandl
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Log.e(this.getClass().getSimpleName(), "FATAL ERROR", e);
|
||||
JitsiMeetLogger.e(e, this.getClass().getSimpleName() + " FATAL ERROR");
|
||||
|
||||
// Abort all ConnectionService ongoing calls
|
||||
if (AudioModeModule.useConnectionService()) {
|
||||
|
||||
@@ -19,12 +19,13 @@ package org.jitsi.meet.sdk;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -39,12 +40,6 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
|
||||
private static final Map<String, Method> LISTENER_METHODS
|
||||
= ListenerUtils.mapListenerMethods(JitsiMeetViewListener.class);
|
||||
|
||||
/**
|
||||
* The {@link Log} tag which identifies the source of the log messages of
|
||||
* {@code JitsiMeetView}.
|
||||
*/
|
||||
private static final String TAG = JitsiMeetView.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The URL of the current conference.
|
||||
*/
|
||||
@@ -137,7 +132,7 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
|
||||
try {
|
||||
pipModule.enterPictureInPicture();
|
||||
} catch (RuntimeException re) {
|
||||
Log.e(TAG, "failed to enter PiP mode", re);
|
||||
JitsiMeetLogger.e(re, "Failed to enter PiP mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright @ 2019-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 com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Module implementing a "bridge" between the JS loggers and the native one.
|
||||
*/
|
||||
@ReactModule(name = LogBridgeModule.NAME)
|
||||
class LogBridgeModule extends ReactContextBaseJavaModule {
|
||||
public static final String NAME = "LogBridge";
|
||||
|
||||
public LogBridgeModule(@Nonnull ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void trace(final String message) {
|
||||
JitsiMeetLogger.v(message);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void debug(final String message) {
|
||||
JitsiMeetLogger.d(message);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void info(final String message) {
|
||||
JitsiMeetLogger.i(message);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void log(final String message) {
|
||||
JitsiMeetLogger.i(message);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void warn(final String message) {
|
||||
JitsiMeetLogger.w(message);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void error(final String message) {
|
||||
JitsiMeetLogger.e(message);
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,9 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@@ -50,7 +51,7 @@ class OngoingNotification {
|
||||
|
||||
Context context = ReactInstanceManagerHolder.getCurrentActivity();
|
||||
if (context == null) {
|
||||
Log.w(TAG, "Cannot create notification channel: no current context");
|
||||
JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ class OngoingNotification {
|
||||
static Notification buildOngoingConferenceNotification() {
|
||||
Context context = ReactInstanceManagerHolder.getCurrentActivity();
|
||||
if (context == null) {
|
||||
Log.w(TAG, "Cannot create notification: no current context");
|
||||
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
@@ -29,6 +28,8 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
@ReactModule(name = PictureInPictureModule.NAME)
|
||||
class PictureInPictureModule
|
||||
extends ReactContextBaseJavaModule {
|
||||
@@ -70,7 +71,7 @@ class PictureInPictureModule
|
||||
throw new IllegalStateException("No current Activity!");
|
||||
}
|
||||
|
||||
Log.d(TAG, "Entering Picture-in-Picture");
|
||||
JitsiMeetLogger.i(TAG + " Entering Picture-in-Picture");
|
||||
|
||||
PictureInPictureParams.Builder builder
|
||||
= new PictureInPictureParams.Builder()
|
||||
|
||||
@@ -5,13 +5,12 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.telecom.DisconnectCause;
|
||||
import android.telecom.PhoneAccount;
|
||||
import android.telecom.PhoneAccountHandle;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.telecom.VideoProfile;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
@@ -20,6 +19,8 @@ import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
/**
|
||||
* The react-native side of Jitsi Meet's {@link ConnectionService}. Exposes
|
||||
* the Java Script API.
|
||||
@@ -28,13 +29,18 @@ import com.facebook.react.module.annotations.ReactModule;
|
||||
*/
|
||||
@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
|
||||
*
|
||||
@@ -74,18 +80,26 @@ class RNConnectionService
|
||||
String handle,
|
||||
boolean hasVideo,
|
||||
Promise promise) {
|
||||
Log.d(TAG,
|
||||
String.format("startCall UUID=%s, h=%s, v=%s",
|
||||
JitsiMeetLogger.d("%s startCall UUID=%s, h=%s, v=%s",
|
||||
TAG,
|
||||
callUUID,
|
||||
handle,
|
||||
hasVideo));
|
||||
hasVideo);
|
||||
|
||||
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(
|
||||
@@ -99,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +140,7 @@ class RNConnectionService
|
||||
*/
|
||||
@ReactMethod
|
||||
public void reportCallFailed(String callUUID) {
|
||||
Log.d(TAG, "reportCallFailed " + callUUID);
|
||||
JitsiMeetLogger.d(TAG + " reportCallFailed " + callUUID);
|
||||
ConnectionService.setConnectionDisconnected(
|
||||
callUUID,
|
||||
new DisconnectCause(DisconnectCause.ERROR));
|
||||
@@ -131,7 +153,7 @@ class RNConnectionService
|
||||
*/
|
||||
@ReactMethod
|
||||
public void endCall(String callUUID) {
|
||||
Log.d(TAG, "endCall " + callUUID);
|
||||
JitsiMeetLogger.d(TAG + " endCall " + callUUID);
|
||||
ConnectionService.setConnectionDisconnected(
|
||||
callUUID,
|
||||
new DisconnectCause(DisconnectCause.LOCAL));
|
||||
@@ -143,9 +165,13 @@ class RNConnectionService
|
||||
* @param callUUID - the call's UUID.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void reportConnectedOutgoingCall(String callUUID) {
|
||||
Log.d(TAG, "reportConnectedOutgoingCall " + callUUID);
|
||||
ConnectionService.setConnectionActive(callUUID);
|
||||
public void reportConnectedOutgoingCall(String callUUID, Promise promise) {
|
||||
JitsiMeetLogger.d(TAG + " reportConnectedOutgoingCall " + callUUID);
|
||||
if (ConnectionService.setConnectionActive(callUUID)) {
|
||||
promise.resolve(null);
|
||||
} else {
|
||||
promise.reject("CONNECTION_NOT_FOUND_ERROR", "Connection wasn't found.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,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.
|
||||
@@ -18,9 +17,11 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.os.Build;
|
||||
|
||||
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;
|
||||
@@ -30,16 +31,20 @@ import com.facebook.react.common.LifecycleState;
|
||||
import com.facebook.react.devsupport.DevInternalSettings;
|
||||
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.webrtc.DefaultVideoDecoderFactory;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.SoftwareVideoDecoderFactory;
|
||||
import org.webrtc.SoftwareVideoEncoderFactory;
|
||||
import org.webrtc.VideoDecoderFactory;
|
||||
import org.webrtc.VideoEncoderFactory;
|
||||
import org.webrtc.audio.AudioDeviceModule;
|
||||
import org.webrtc.audio.JavaAudioDeviceModule;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
@@ -66,7 +71,9 @@ class ReactInstanceManagerHolder {
|
||||
new AudioModeModule(reactContext),
|
||||
new DropboxModule(reactContext),
|
||||
new ExternalAPIModule(reactContext),
|
||||
new JavaScriptSandboxModule(reactContext),
|
||||
new LocaleDetector(reactContext),
|
||||
new LogBridgeModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
new ProximityModule(reactContext),
|
||||
new WiFiStatsModule(reactContext),
|
||||
@@ -82,10 +89,22 @@ 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 = new SoftwareVideoDecoderFactory();
|
||||
|
||||
// Initialize EGL context required for HW acceleration. We are only going to use it for
|
||||
// decoding.
|
||||
// NOTE: We are explicitly skipping Samsung devices because we have observed a high crash
|
||||
// count on them.
|
||||
if (!Build.MANUFACTURER.toLowerCase().contains("samsung")) {
|
||||
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
|
||||
if (eglContext != null) {
|
||||
videoDecoderFactory = new DefaultVideoDecoderFactory(eglContext);
|
||||
}
|
||||
}
|
||||
|
||||
options.setVideoDecoderFactory(videoDecoderFactory);
|
||||
options.setVideoEncoderFactory(videoEncoderFactory);
|
||||
|
||||
@@ -175,22 +194,25 @@ class ReactInstanceManagerHolder {
|
||||
* time. All {@code ReactRootView} instances will be tied to the one and
|
||||
* only {@code ReactInstanceManager}.
|
||||
*
|
||||
* @param application {@code Application} instance which is running.
|
||||
* @param activity {@code Activity} current running Activity.
|
||||
*/
|
||||
static void initReactInstanceManager(Application application) {
|
||||
static void initReactInstanceManager(Activity activity) {
|
||||
if (reactInstanceManager != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SoLoader.init(activity, /* native exopackage */ false);
|
||||
|
||||
List<ReactPackage> packages
|
||||
= new ArrayList<>(Arrays.asList(
|
||||
new com.BV.LinearGradient.LinearGradientPackage(),
|
||||
new com.calendarevents.CalendarEventsPackage(),
|
||||
new com.corbt.keepawake.KCKeepAwakePackage(),
|
||||
new com.facebook.react.shell.MainReactPackage(),
|
||||
new com.oblador.vectoricons.VectorIconsPackage(),
|
||||
new com.horcrux.svg.SvgPackage(),
|
||||
new com.ocetnik.timer.BackgroundTimerPackage(),
|
||||
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
|
||||
new com.reactnativecommunity.netinfo.NetInfoPackage(),
|
||||
new com.reactnativecommunity.webview.RNCWebViewPackage(),
|
||||
new com.rnimmersive.RNImmersivePackage(),
|
||||
new com.zmxv.RNSound.RNSoundPackage(),
|
||||
@@ -213,11 +235,16 @@ class ReactInstanceManagerHolder {
|
||||
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
|
||||
}
|
||||
|
||||
// Use the Hermes JavaScript engine.
|
||||
HermesExecutorFactory jsFactory = new HermesExecutorFactory();
|
||||
|
||||
reactInstanceManager
|
||||
= ReactInstanceManager.builder()
|
||||
.setApplication(application)
|
||||
.setApplication(activity.getApplication())
|
||||
.setCurrentActivity(activity)
|
||||
.setBundleAssetName("index.android.bundle")
|
||||
.setJSMainModulePath("index.android")
|
||||
.setJavaScriptExecutorFactory(jsFactory)
|
||||
.addPackages(packages)
|
||||
.setUseDeveloperSupport(BuildConfig.DEBUG)
|
||||
.setInitialLifecycleState(LifecycleState.RESUMED)
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jitsi.meet.sdk;
|
||||
import android.content.Context;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
@@ -27,6 +26,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -184,17 +184,15 @@ class WiFiStatsModule
|
||||
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
Log.wtf(TAG,
|
||||
"Unable to NetworkInterface.getNetworkInterfaces()"
|
||||
);
|
||||
JitsiMeetLogger.e(e, TAG + " Unable to NetworkInterface.getNetworkInterfaces()");
|
||||
}
|
||||
|
||||
result.put("addresses", addresses);
|
||||
promise.resolve(result.toString());
|
||||
|
||||
Log.d(TAG, "WiFi stats: " + result.toString());
|
||||
JitsiMeetLogger.d(TAG + " WiFi stats: " + result.toString());
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "Failed to obtain wifi stats", e);
|
||||
JitsiMeetLogger.e(e, TAG + " Failed to obtain wifi stats");
|
||||
promise.reject(
|
||||
new Exception("Failed to obtain wifi stats"));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package org.jitsi.meet.sdk.incoming_call;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class IncomingCallInfo {
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.jitsi.meet.sdk.incoming_call;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright @ 2019-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.log;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Base class for all custom log handlers. Implementations must inherit from this class and
|
||||
* implement a `doLog` method which does the actual logging, in addition with a `getTag` method
|
||||
* with which to tag all logs coming into this logger.
|
||||
*
|
||||
* See {@link JitsiMeetDefaultLogHandler} for an example.
|
||||
*/
|
||||
public abstract class JitsiMeetBaseLogHandler extends Timber.Tree {
|
||||
@Override
|
||||
protected void log(int priority, @Nullable String tag, @NotNull String msg, @Nullable Throwable t) {
|
||||
String errmsg = Log.getStackTraceString(t);
|
||||
if (errmsg.isEmpty()) {
|
||||
doLog(priority, getDefaultTag(), msg);
|
||||
} else {
|
||||
doLog(priority, getDefaultTag(), MessageFormat.format("{0}\n{1}", msg, errmsg));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void doLog(int priority, @NotNull String tag, @NotNull String msg);
|
||||
|
||||
protected abstract String getDefaultTag();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright @ 2019-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.log;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link JitsiMeetBaseLogHandler}. This is the main SDK logger, which
|
||||
* logs using the Android util.Log module.
|
||||
*/
|
||||
public class JitsiMeetDefaultLogHandler extends JitsiMeetBaseLogHandler {
|
||||
private static final String TAG = "JitsiMeetSDK";
|
||||
|
||||
@Override
|
||||
protected void doLog(int priority, @NotNull String tag, @NotNull String msg) {
|
||||
Log.println(priority, tag, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDefaultTag() {
|
||||
return TAG;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright @ 2019-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.log;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class JitsiMeetLogger {
|
||||
static {
|
||||
addHandler(new JitsiMeetDefaultLogHandler());
|
||||
}
|
||||
|
||||
public static void addHandler(JitsiMeetBaseLogHandler 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) {
|
||||
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) {
|
||||
Timber.v(message, args);
|
||||
}
|
||||
|
||||
public static void v(Throwable t, String message, Object... args) {
|
||||
Timber.v(t, message, args);
|
||||
}
|
||||
|
||||
public static void v(Throwable t) {
|
||||
Timber.v(t);
|
||||
}
|
||||
|
||||
public static void d(String message, Object... args) {
|
||||
Timber.d(message, args);
|
||||
}
|
||||
|
||||
public static void d(Throwable t, String message, Object... args) {
|
||||
Timber.d(t, message, args);
|
||||
}
|
||||
|
||||
public static void d(Throwable t) {
|
||||
Timber.d(t);
|
||||
}
|
||||
|
||||
public static void i(String message, Object... args) {
|
||||
Timber.i(message, args);
|
||||
}
|
||||
|
||||
public static void i(Throwable t, String message, Object... args) {
|
||||
Timber.i(t, message, args);
|
||||
}
|
||||
|
||||
public static void i(Throwable t) {
|
||||
Timber.i(t);
|
||||
}
|
||||
|
||||
public static void w(String message, Object... args) {
|
||||
Timber.w(message, args);
|
||||
}
|
||||
|
||||
public static void w(Throwable t, String message, Object... args) {
|
||||
Timber.w(t, message, args);
|
||||
}
|
||||
|
||||
public static void w(Throwable t) {
|
||||
Timber.w(t);
|
||||
}
|
||||
|
||||
public static void e(String message, Object... args) {
|
||||
Timber.e(message, args);
|
||||
}
|
||||
|
||||
public static void e(Throwable t, String message, Object... args) {
|
||||
Timber.e(t, message, args);
|
||||
}
|
||||
|
||||
public static void e(Throwable t) {
|
||||
Timber.e(t);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
package org.jitsi.meet.sdk.net;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ public class NAT64AddrInfoModule
|
||||
* The host for which the module wil try to resolve both IPv4 and IPv6
|
||||
* addresses in order to figure out the NAT64 prefix.
|
||||
*/
|
||||
private final static String HOST = "nat64.jitsi.net";
|
||||
private final static String HOST = "ipv4only.arpa";
|
||||
|
||||
/**
|
||||
* How long is the {@link NAT64AddrInfo} instance valid.
|
||||
@@ -97,7 +97,7 @@ public class NAT64AddrInfoModule
|
||||
try {
|
||||
info = NAT64AddrInfo.discover(host);
|
||||
} catch (UnknownHostException e) {
|
||||
Log.e(TAG, "NAT64AddrInfo.discover: " + host, e);
|
||||
JitsiMeetLogger.e(e, TAG + " NAT64AddrInfo.discover: " + host);
|
||||
}
|
||||
infoTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class NAT64AddrInfoModule
|
||||
try {
|
||||
result = info == null ? null : info.getIPv6Address(ipv4Address);
|
||||
} catch (IllegalArgumentException exc) {
|
||||
Log.e(TAG, "Failed to get IPv6 address for: " + ipv4Address, exc);
|
||||
JitsiMeetLogger.e(exc, TAG + " Failed to get IPv6 address for: " + ipv4Address);
|
||||
|
||||
// We don't want to reject. It's not a big deal if there's no IPv6
|
||||
// address resolved.
|
||||
|
||||
@@ -7,8 +7,10 @@ include ':react-native-calendar-events'
|
||||
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
|
||||
include ':react-native-community-async-storage'
|
||||
project(':react-native-community-async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android')
|
||||
include ':react-native-community_netinfo'
|
||||
project(':react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
|
||||
include ':react-native-google-signin'
|
||||
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android')
|
||||
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/google-signin/android')
|
||||
include ':react-native-immersive'
|
||||
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
|
||||
include ':react-native-keep-awake'
|
||||
@@ -17,8 +19,8 @@ include ':react-native-linear-gradient'
|
||||
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
|
||||
include ':react-native-sound'
|
||||
project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
|
||||
include ':react-native-vector-icons'
|
||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||
include ':react-native-svg'
|
||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
||||
include ':react-native-webrtc'
|
||||
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
|
||||
include ':react-native-webview'
|
||||
|
||||
401
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,
|
||||
@@ -104,7 +110,6 @@ import {
|
||||
trackRemoved
|
||||
} from './react/features/base/tracks';
|
||||
import { getJitsiMeetGlobalNS } from './react/features/base/util';
|
||||
import { addMessage } from './react/features/chat';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
import {
|
||||
@@ -114,8 +119,10 @@ import {
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
|
||||
import { suspendDetected } from './react/features/power-monitor';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import { isButtonEnabled } from './react/features/toolbox';
|
||||
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);
|
||||
|
||||
@@ -124,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
|
||||
@@ -244,8 +260,6 @@ class ConferenceConnector {
|
||||
this._handleConferenceJoined.bind(this));
|
||||
room.on(JitsiConferenceEvents.CONFERENCE_FAILED,
|
||||
this._onConferenceFailed.bind(this));
|
||||
room.on(JitsiConferenceEvents.CONFERENCE_ERROR,
|
||||
this._onConferenceError.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,31 +362,6 @@ class ConferenceConnector {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
_onConferenceError(err, ...params) {
|
||||
logger.error('CONFERENCE Error:', err, params);
|
||||
switch (err) {
|
||||
case JitsiConferenceErrors.CHAT_ERROR:
|
||||
logger.error('Chat error.', err);
|
||||
if (isButtonEnabled('chat') && !interfaceConfig.filmStripOnly) {
|
||||
const [ code, msg ] = params;
|
||||
|
||||
APP.store.dispatch(addMessage({
|
||||
hasRead: true,
|
||||
error: code,
|
||||
message: msg,
|
||||
messageType: 'error',
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.error('Unknown error.', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -466,6 +455,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
|
||||
@@ -497,14 +492,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) {
|
||||
@@ -751,9 +750,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);
|
||||
},
|
||||
|
||||
@@ -827,6 +825,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.
|
||||
@@ -841,6 +868,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
|
||||
@@ -1234,17 +1268,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;
|
||||
},
|
||||
@@ -1376,7 +1415,7 @@ export default {
|
||||
* in case it fails.
|
||||
* @private
|
||||
*/
|
||||
_turnScreenSharingOff(didHaveVideo, wasVideoMuted) {
|
||||
_turnScreenSharingOff(didHaveVideo) {
|
||||
this._untoggleScreenSharing = null;
|
||||
this.videoSwitchInProgress = true;
|
||||
const { receiver } = APP.remoteControl;
|
||||
@@ -1387,20 +1426,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);
|
||||
@@ -1412,7 +1458,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(
|
||||
@@ -1440,7 +1489,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.');
|
||||
}
|
||||
@@ -1454,10 +1503,20 @@ 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();
|
||||
return this._untoggleScreenSharing
|
||||
? this._untoggleScreenSharing()
|
||||
: Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1477,8 +1536,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 ])
|
||||
@@ -1529,8 +1587,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,
|
||||
() => {
|
||||
@@ -1555,6 +1612,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.
|
||||
@@ -1579,6 +1756,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');
|
||||
})
|
||||
@@ -1692,7 +1872,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,
|
||||
@@ -1822,6 +2005,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));
|
||||
@@ -2015,36 +2202,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)));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2198,12 +2414,6 @@ export default {
|
||||
|
||||
APP.keyboardshortcut.init();
|
||||
|
||||
if (config.requireDisplayName
|
||||
&& !APP.conference.getLocalDisplayName()
|
||||
&& !room.isHidden()) {
|
||||
APP.UI.promptDisplayName();
|
||||
}
|
||||
|
||||
APP.store.dispatch(conferenceJoined(room));
|
||||
|
||||
const displayName
|
||||
@@ -2280,6 +2490,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()
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
67
config.js
@@ -1,16 +1,6 @@
|
||||
/* eslint-disable no-unused-vars, no-var */
|
||||
|
||||
var config = {
|
||||
// Configuration
|
||||
//
|
||||
|
||||
// Alternative location for the configuration.
|
||||
// configLocation: './config.json',
|
||||
|
||||
// Custom function which given the URL path should return a room name.
|
||||
// getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; },
|
||||
|
||||
|
||||
// Connection
|
||||
//
|
||||
|
||||
@@ -40,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',
|
||||
|
||||
@@ -60,6 +53,10 @@ var config = {
|
||||
|
||||
// Enables the test specific features consumed by jitsi-meet-torture
|
||||
// testMode: false
|
||||
|
||||
// Disables the auto-play behavior of *all* newly created video element.
|
||||
// This is useful when the client runs on a host with limited resources.
|
||||
// noAutoPlayVideo: false
|
||||
},
|
||||
|
||||
// Disables ICE/UDP by filtering out local and remote UDP candidates in
|
||||
@@ -79,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,
|
||||
@@ -123,10 +131,6 @@ var config = {
|
||||
// are requested again.
|
||||
// enableLayerSuspension: false,
|
||||
|
||||
// Suspend sending video if bandwidth estimation is too low. This may cause
|
||||
// problems with audio playback. Disabled until these are fixed.
|
||||
disableSuspendVideo: true,
|
||||
|
||||
// Every participant after the Nth will start video muted.
|
||||
// startVideoMuted: 10,
|
||||
|
||||
@@ -201,6 +205,9 @@ var config = {
|
||||
// subtitles and buttons can be configured)
|
||||
// transcribingEnabled: false,
|
||||
|
||||
// Enables automatic turning on captions when recording is started
|
||||
// autoCaptionOnRecord: false,
|
||||
|
||||
// Misc
|
||||
|
||||
// Default value for the channel "last N" attribute. -1 for unlimited.
|
||||
@@ -299,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
|
||||
//
|
||||
@@ -333,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' }
|
||||
@@ -381,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
|
||||
//
|
||||
|
||||
@@ -426,6 +447,16 @@ var config = {
|
||||
// the menu has option to flip the locally seen video for local presentations
|
||||
// disableLocalVideoFlip: false
|
||||
|
||||
// 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
|
||||
/**
|
||||
_immediateReloadThreshold
|
||||
|
||||
@@ -75,7 +75,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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,15 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-icon svg {
|
||||
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
|
||||
@@ -157,6 +166,7 @@ form {
|
||||
::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
width: 7px;
|
||||
height: $scrollHeight;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
|
||||
@@ -80,6 +80,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
#chat-recipient {
|
||||
align-items: center;
|
||||
background-color: $chatPrivateMessageBackgroundColor;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: 100;
|
||||
padding: 10px;
|
||||
|
||||
span {
|
||||
color: white;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
div {
|
||||
svg {
|
||||
cursor: pointer;
|
||||
fill: white
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background-color: $chatHeaderBackgroundColor;
|
||||
height: 70px;
|
||||
@@ -111,6 +133,7 @@
|
||||
#chat-input {
|
||||
border-top: 1px solid $chatInputSeparatorColor;
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
|
||||
* {
|
||||
background-color: transparent;
|
||||
@@ -131,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%;
|
||||
@@ -162,6 +184,7 @@
|
||||
.display-name {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -175,7 +198,6 @@
|
||||
color: white;
|
||||
margin-top: 3px;
|
||||
max-width: 100%;
|
||||
padding-bottom: 3px;
|
||||
position: relative;
|
||||
|
||||
&.localuser {
|
||||
@@ -183,6 +205,10 @@
|
||||
border-radius: 6px 0px 6px 6px;
|
||||
}
|
||||
|
||||
.usermessage {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-radius: 0px;
|
||||
|
||||
@@ -196,6 +222,17 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.privatemessagenotice {
|
||||
font-size: 11px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.messagecontent {
|
||||
margin: 5px 10px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.smiley {
|
||||
@@ -228,6 +265,7 @@
|
||||
.smileys-panel {
|
||||
bottom: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0, 0, 0, .6) !important;
|
||||
height: auto;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
@@ -270,10 +308,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#usermsg::-webkit-input-placeholder {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#usermsg::-webkit-scrollbar-track-piece {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
@@ -288,6 +322,10 @@
|
||||
.chatmessage {
|
||||
background-color: $chatLocalMessageBackgroundColor;
|
||||
border-radius: 6px 0px 6px 6px;
|
||||
|
||||
&.privatemessage {
|
||||
background-color: $chatPrivateMessageBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
@@ -301,8 +339,9 @@
|
||||
|
||||
&.error {
|
||||
.chatmessage {
|
||||
background-color: $defaultWarningColor;
|
||||
border-radius: 0px;
|
||||
color: red;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
@@ -312,6 +351,25 @@
|
||||
|
||||
.chatmessage-wrapper {
|
||||
max-width: 100%;
|
||||
|
||||
.replywrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.messageactions {
|
||||
align-self: stretch;
|
||||
border-left: 1px solid $chatActionsSeparatorColor;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
|
||||
.toolbox-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chatmessage {
|
||||
@@ -320,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;
|
||||
}
|
||||
}
|
||||
226
css/_font.scss
@@ -1,226 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'jitsi';
|
||||
src: url('../fonts/jitsi.eot?icrce1');
|
||||
src: url('../fonts/jitsi.eot?icrce1#iefix') format('embedded-opentype'),
|
||||
url('../fonts/jitsi.ttf?icrce1') format('truetype'),
|
||||
url('../fonts/jitsi.woff?icrce1') format('woff'),
|
||||
url('../fonts/jitsi.svg?icrce1#jitsi') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
font-family: 'jitsi';
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1.22em;
|
||||
font-size: 1.22em;
|
||||
cursor: default;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-blur-background:before {
|
||||
content: "\e90f";
|
||||
color: #a4b8d1;
|
||||
}
|
||||
.icon-speaker:before {
|
||||
content: "\e92d";
|
||||
}
|
||||
.icon-rec:before {
|
||||
content: "\e92b";
|
||||
}
|
||||
.icon-camera-take-picture:before {
|
||||
content: "\e92a";
|
||||
}
|
||||
.icon-AUD:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-HD:before {
|
||||
content: "\e927";
|
||||
}
|
||||
.icon-LD:before {
|
||||
content: "\e928";
|
||||
}
|
||||
.icon-SD:before {
|
||||
content: "\e929";
|
||||
}
|
||||
.icon-gsm-bars:before {
|
||||
content: "\e926";
|
||||
}
|
||||
.icon-info:before {
|
||||
content: "\e922";
|
||||
}
|
||||
.icon-mic-camera-combined:before {
|
||||
content: "\e903";
|
||||
}
|
||||
.icon-feedback:before {
|
||||
content: "\e91d";
|
||||
}
|
||||
.icon-hangup:before {
|
||||
content: "\e905";
|
||||
}
|
||||
.icon-chat:before {
|
||||
content: "\e906";
|
||||
}
|
||||
.icon-share-doc:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.icon-kick:before {
|
||||
content: "\e904";
|
||||
}
|
||||
.icon-menu-up:before {
|
||||
content: "\e91f";
|
||||
}
|
||||
.icon-menu-down:before {
|
||||
content: "\e920";
|
||||
}
|
||||
.icon-full-screen:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-exit-full-screen:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon-security:before {
|
||||
content: "\e90d";
|
||||
}
|
||||
.icon-security-locked:before {
|
||||
content: "\e90e";
|
||||
}
|
||||
.icon-microphone:before {
|
||||
content: "\e910";
|
||||
}
|
||||
.icon-mic-disabled:before {
|
||||
content: "\e912";
|
||||
}
|
||||
.icon-raised-hand:before {
|
||||
content: "\e91e";
|
||||
}
|
||||
.icon-link:before {
|
||||
content: "\e913";
|
||||
}
|
||||
.icon-shared-video:before {
|
||||
content: "\e914";
|
||||
}
|
||||
.icon-settings:before {
|
||||
content: "\e915";
|
||||
}
|
||||
.icon-star:before {
|
||||
content: "\e916";
|
||||
}
|
||||
.icon-switch-camera:before {
|
||||
content: "\e921";
|
||||
}
|
||||
.icon-share-desktop:before {
|
||||
content: "\e917";
|
||||
}
|
||||
.icon-camera:before {
|
||||
content: "\e918";
|
||||
}
|
||||
.icon-camera-disabled:before {
|
||||
content: "\e919";
|
||||
}
|
||||
.icon-volume:before {
|
||||
content: "\e91a";
|
||||
}
|
||||
.icon-presentation:before {
|
||||
content: "\e603";
|
||||
}
|
||||
.icon-visibility:before {
|
||||
content: "\e923";
|
||||
}
|
||||
.icon-visibility-off:before {
|
||||
content: "\e924";
|
||||
}
|
||||
.icon-enlarge:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-signal_cellular_0:before {
|
||||
content: "\e901";
|
||||
}
|
||||
.icon-signal_cellular_1:before {
|
||||
content: "\e902";
|
||||
}
|
||||
.icon-signal_cellular_2:before {
|
||||
content: "\e907";
|
||||
}
|
||||
.icon-phone:before {
|
||||
content: "\e0cd";
|
||||
}
|
||||
.icon-radio_button_unchecked:before {
|
||||
content: "\e836";
|
||||
}
|
||||
.icon-radio_button_checked:before {
|
||||
content: "\e837";
|
||||
}
|
||||
.icon-search:before {
|
||||
content: "\e8b6";
|
||||
}
|
||||
.icon-chat-unread:before {
|
||||
content: "\e0b7";
|
||||
}
|
||||
.icon-closed_caption:before {
|
||||
content: "\e930";
|
||||
}
|
||||
.icon-tiles-many:before {
|
||||
content: "\e92e";
|
||||
}
|
||||
.icon-close:before {
|
||||
content: "\e5cd";
|
||||
}
|
||||
.icon-open_in_new:before {
|
||||
content: "\e89e";
|
||||
}
|
||||
.icon-restore:before {
|
||||
content: "\e8b3";
|
||||
}
|
||||
.icon-navigate_next:before {
|
||||
content: "\e409";
|
||||
}
|
||||
.icon-menu:before {
|
||||
content: "\e5d2";
|
||||
}
|
||||
.icon-arrow_back:before {
|
||||
content: "\e5c4";
|
||||
}
|
||||
.icon-public:before {
|
||||
content: "\e80b";
|
||||
}
|
||||
.icon-event_note:before {
|
||||
content: "\e616";
|
||||
}
|
||||
.icon-bluetooth:before {
|
||||
content: "\e1aa";
|
||||
}
|
||||
.icon-headset:before {
|
||||
content: "\e310";
|
||||
}
|
||||
.icon-phone-talk:before {
|
||||
content: "\e61d";
|
||||
}
|
||||
.icon-thumb-menu:before {
|
||||
content: "\e5d4";
|
||||
}
|
||||
.icon-ninja:before {
|
||||
content: "\e909";
|
||||
}
|
||||
.icon-invite:before {
|
||||
content: "\e145";
|
||||
}
|
||||
.icon-add:before {
|
||||
content: "\e146";
|
||||
}
|
||||
.icon-play:before {
|
||||
content: "\f04b";
|
||||
}
|
||||
.icon-stop:before {
|
||||
content: "\f04d";
|
||||
}
|
||||
.icon-dominant-speaker:before {
|
||||
content: "\f0a1";
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
min-width: 75px;
|
||||
text-align: left;
|
||||
padding: 0px;
|
||||
width: 150px;
|
||||
width: 180px;
|
||||
white-space: nowrap;
|
||||
|
||||
&__item {
|
||||
@@ -87,6 +87,7 @@
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
|
||||
> * {
|
||||
@include absoluteAligning();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,62 @@
|
||||
|
||||
.button-group-center {
|
||||
justify-content: center;
|
||||
.toolbox-icon {
|
||||
margin: 0px 4px;
|
||||
|
||||
.toolbox-button {
|
||||
|
||||
.toolbox-icon {
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #d1dbe8;
|
||||
margin: 0px 4px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
|
||||
&:hover {
|
||||
background-color: #daebfa;
|
||||
border: 1px solid #daebfa;
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: #2a3a4b;
|
||||
border: 1px solid #5e6d7a;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #5e6d7a;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, .disabled & {
|
||||
cursor: initial;
|
||||
color: #fff;
|
||||
background-color: #a4b8d1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #5e6d7a;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
.toolbox-icon {
|
||||
background-color: $hangupColor;
|
||||
border: 1px solid $hangupColor;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&:hover {
|
||||
background-color: $hangupColor;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,75 +136,6 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
i {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: $newToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
i.toggled:hover {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
background-color: #e12d2d;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&:hover {
|
||||
background-color: #e54b4b;
|
||||
}
|
||||
}
|
||||
i.disabled, .disabled i {
|
||||
cursor: initial !important;
|
||||
color: #fff !important;
|
||||
background-color: #a4b8d1 !important;
|
||||
}
|
||||
|
||||
.icon-mic-disabled, .icon-microphone, .icon-camera-disabled, .icon-camera {
|
||||
background-color: #fff;
|
||||
color: #5e6d7a;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #d1dbe8;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
|
||||
&:hover {
|
||||
background-color: #daebfa;
|
||||
border: 1px solid #daebfa;
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: #2a3a4b;
|
||||
color: #fff;
|
||||
border: 1px solid #5e6d7a;
|
||||
|
||||
&:hover {
|
||||
background-color: #5e6d7a;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, .disabled & {
|
||||
cursor: initial;
|
||||
color: #fff;
|
||||
background-color: #a4b8d1;
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-menu {
|
||||
font-size: 1.2em;
|
||||
list-style-type: none;
|
||||
@@ -191,14 +176,6 @@
|
||||
cursor: initial;
|
||||
color: #3b475c;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
i.toggled:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-tag {
|
||||
@@ -227,6 +204,10 @@
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #B8C7E0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-text {
|
||||
@@ -265,104 +246,30 @@
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
height: $newToolbarSize;
|
||||
display: flex;
|
||||
border-radius: 5px;
|
||||
flex-direction: column;
|
||||
font-size: 24px;
|
||||
height: $newToolbarSize;
|
||||
justify-content: center;
|
||||
width: $newToolbarSize;
|
||||
|
||||
&:hover, &.toggled {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: initial !important;
|
||||
background-color: #a4b8d1 !important;
|
||||
|
||||
svg {
|
||||
fill: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
color: $hangupColor;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
font-size: $newToolbarHangupFontSize;
|
||||
}
|
||||
|
||||
.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.
|
||||
*/
|
||||
|
||||
@@ -28,11 +28,15 @@ $defaultColor: #F1F1F1;
|
||||
$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);
|
||||
@@ -85,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;
|
||||
|
||||
/**
|
||||
@@ -99,6 +105,7 @@ $defaultWatermarkLink: '../images/watermark.png';
|
||||
$popoverMenuPadding: 13px;
|
||||
$happySoftwareBackground: transparent;
|
||||
$desktopAppDragBarHeight: 25px;
|
||||
$scrollHeight: 7px;
|
||||
|
||||
/**
|
||||
* Z-indexes. TODO: Replace this by a function.
|
||||
@@ -163,11 +170,104 @@ $watermarkHeight: 74px;
|
||||
*/
|
||||
$welcomePageDescriptionColor: #fff;
|
||||
$welcomePageFontFamily: inherit;
|
||||
$welcomePageHeaderBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
|
||||
$welcomePageBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
|
||||
$welcomePageTitleColor: #fff;
|
||||
|
||||
$welcomePageHeaderBackground: none;
|
||||
$welcomePageHeaderBackgroundSmall: none;
|
||||
$welcomePageHeaderBackgroundPosition: none;
|
||||
$welcomePageHeaderBackgroundRepeat: none;
|
||||
$welcomePageHeaderBackgroundSize: none;
|
||||
$welcomePageHeaderPaddingBottom: 0px;
|
||||
|
||||
$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;
|
||||
$welcomePageEnterRoomBorderRadius: 0px;
|
||||
|
||||
$welcomePageEnterRoomInputContainerPadding: 0 8px 5px 0px;
|
||||
$welcomePageEnterRoomInputContainerBorderWidth: 0px 0px 2px 0px;
|
||||
$welcomePageEnterRoomInputContainerBorderStyle: solid;
|
||||
$welcomePageEnterRoomInputContainerBorderImage: linear-gradient(to right, #dee1e6, #fff) 1;
|
||||
|
||||
$welcomePageEnterRoomTitleDisplay: inherit;
|
||||
|
||||
$welcomePageTabContainerDisplay: flex;
|
||||
$welcomePageTabContentDisplay: inherit;
|
||||
$welcomePageTabButtonsDisplay: flex;
|
||||
$welcomePageTabDisplay: block;
|
||||
|
||||
$welcomePageButtonWidth: 51px;
|
||||
$welcomePageButtonMinWidth: inherit;
|
||||
$welcomePageButtonFontSize: 14px;
|
||||
$welcomePageButtonHeight: 35px;
|
||||
$welcomePageButtonFontWeight: inherit;
|
||||
$welcomePageButtonBorderRadius: 4px;
|
||||
$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 {
|
||||
@@ -344,8 +340,11 @@
|
||||
/**
|
||||
* Toolbar icon internal i elements (font icons).
|
||||
*/
|
||||
.toolbar-icon>i {
|
||||
line-height: $thumbnailToolbarHeight;
|
||||
.toolbar-icon>div {
|
||||
height: $thumbnailToolbarHeight;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,6 +485,8 @@
|
||||
height: 300px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#mixedstream {
|
||||
@@ -521,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,6 +554,9 @@
|
||||
}
|
||||
|
||||
.sharedVideoAvatar {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
@@ -560,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;
|
||||
@@ -605,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;
|
||||
|
||||
@@ -4,7 +4,7 @@ body.welcome-page {
|
||||
}
|
||||
|
||||
.welcome {
|
||||
background-image: $welcomePageHeaderBackground;
|
||||
background-image: $welcomePageBackground;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: $welcomePageFontFamily;
|
||||
@@ -13,6 +13,11 @@ body.welcome-page {
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
background-image: $welcomePageHeaderBackground;
|
||||
background-position: $welcomePageHeaderBackgroundPosition;
|
||||
background-repeat: $welcomePageHeaderBackgroundRepeat;
|
||||
background-size: $welcomePageHeaderBackgroundSize;
|
||||
padding-bottom: $welcomePageHeaderPaddingBottom;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -24,8 +29,8 @@ body.welcome-page {
|
||||
.header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: $watermarkHeight + 35;
|
||||
margin-bottom: 35px;
|
||||
margin-top: $watermarkHeight + $welcomePageHeaderTextMarginTop;
|
||||
margin-bottom: $welcomePageHeaderTextMarginBottom;
|
||||
max-width: calc(100% - 40px);
|
||||
width: 650px;
|
||||
z-index: $zindex2;
|
||||
@@ -33,41 +38,45 @@ body.welcome-page {
|
||||
|
||||
.header-text-title {
|
||||
color: $welcomePageTitleColor;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.18;
|
||||
margin-bottom: 16px;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: calc(100% - 40px);
|
||||
width: 680px;
|
||||
width: $welcomePageEnterRoomWidth;
|
||||
z-index: $zindex2;
|
||||
background-color: #fff;
|
||||
padding: 25px 30px;
|
||||
padding: $welcomePageEnterRoomPadding;
|
||||
border-radius: $welcomePageEnterRoomBorderRadius;
|
||||
|
||||
.enter-room-input-container {
|
||||
width: 100%;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 5px;
|
||||
padding: $welcomePageEnterRoomInputContainerPadding;
|
||||
text-align: left;
|
||||
color: #253858;
|
||||
height: fit-content;
|
||||
border-width: 0px 0px 2px 0px;
|
||||
border-style: solid;
|
||||
border-image: linear-gradient(to right, #dee1e6, #fff) 1;
|
||||
border-width: $welcomePageEnterRoomInputContainerBorderWidth;
|
||||
border-style: $welcomePageEnterRoomInputContainerBorderStyle;
|
||||
border-image: $welcomePageEnterRoomInputContainerBorderImage;
|
||||
|
||||
.enter-room-title {
|
||||
display: $welcomePageEnterRoomTitleDisplay;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
@@ -94,10 +103,11 @@ body.welcome-page {
|
||||
min-height: 354px;
|
||||
width: 710px;
|
||||
background: #75A7E7;
|
||||
display: flex;
|
||||
display: $welcomePageTabContainerDisplay;
|
||||
flex-direction: column;
|
||||
|
||||
.tab-content{
|
||||
display: $welcomePageTabContentDisplay;
|
||||
margin: 5px 0px;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
@@ -111,13 +121,14 @@ body.welcome-page {
|
||||
.tab-buttons {
|
||||
font-size: 18px;
|
||||
color: #FFFFFF;
|
||||
display: flex;
|
||||
display: $welcomePageTabButtonsDisplay;
|
||||
flex-grow: 0;
|
||||
flex-direction: row;
|
||||
min-height: 54px;
|
||||
width: 100%;
|
||||
|
||||
.tab {
|
||||
display: $welcomePageTabDisplay;
|
||||
text-align: center;
|
||||
background: rgba(9,30,66,0.37);
|
||||
height: 55px;
|
||||
@@ -138,15 +149,17 @@ body.welcome-page {
|
||||
}
|
||||
|
||||
.welcome-page-button {
|
||||
width: 51px;
|
||||
height: 35px;
|
||||
font-size: 14px;
|
||||
width: $welcomePageButtonWidth;
|
||||
min-width: $welcomePageButtonMinWidth;
|
||||
height: $welcomePageButtonHeight;
|
||||
font-size: $welcomePageButtonFontSize;
|
||||
font-weight: $welcomePageButtonFontWeight;
|
||||
background: #0074E0;
|
||||
border-radius: 4px;
|
||||
border-radius: $welcomePageButtonBorderRadius;
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 35px;
|
||||
line-height: $welcomePageButtonLineHeight;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
1
css/_welcome_page_settings_toolbar.scss
Normal file
@@ -0,0 +1 @@
|
||||
/** Insert custom CSS for any additional content in the welcome page settings toolbar **/
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,12 +23,6 @@ $flagsImagePath: "../images/";
|
||||
@import "../node_modules/bc-css-flags/dist/css/bc-css-flags.scss";
|
||||
/* Flags END */
|
||||
|
||||
/* Fonts BEGIN */
|
||||
|
||||
@import 'font';
|
||||
|
||||
/* Fonts END */
|
||||
|
||||
/* Modules BEGIN */
|
||||
|
||||
@import 'aui_reset';
|
||||
@@ -38,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';
|
||||
@@ -50,6 +45,7 @@ $flagsImagePath: "../images/";
|
||||
@import 'videolayout_default';
|
||||
@import 'notice';
|
||||
@import 'subject';
|
||||
@import 'participants-count';
|
||||
@import 'popup_menu';
|
||||
@import 'recording';
|
||||
@import 'login_menu';
|
||||
@@ -57,6 +53,7 @@ $flagsImagePath: "../images/";
|
||||
@import 'ringing/ringing';
|
||||
@import 'welcome_page';
|
||||
@import 'welcome_page_content';
|
||||
@import 'welcome_page_settings_toolbar';
|
||||
@import 'toolbars';
|
||||
@import 'jquery.contextMenu';
|
||||
@import 'keyboard-shortcuts';
|
||||
@@ -87,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
|
||||
|
||||