Compare commits
642 Commits
dependabot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1486493a | ||
|
|
885cac991b | ||
|
|
462ee4e24b | ||
|
|
68d803a4e9 | ||
|
|
8874d10214 | ||
|
|
896b49cb95 | ||
|
|
9f5d38d0e7 | ||
|
|
b2f5e460c4 | ||
|
|
1547645e7e | ||
|
|
fcf7d62b4b | ||
|
|
1f4b6f34e8 | ||
|
|
df4adc52a3 | ||
|
|
6f5ba1d533 | ||
|
|
ae99a2afee | ||
|
|
17de091cfe | ||
|
|
c4673170b4 | ||
|
|
03c9b4178a | ||
|
|
d850e01f53 | ||
|
|
ec34374254 | ||
|
|
b2701311fb | ||
|
|
4617dfd797 | ||
|
|
5de74f2567 | ||
|
|
375ea757dd | ||
|
|
48341c21f6 | ||
|
|
bc38b18bc5 | ||
|
|
3bf155cf93 | ||
|
|
26c60fd1cc | ||
|
|
d74c2d03ea | ||
|
|
0894097546 | ||
|
|
649fc40666 | ||
|
|
ed51cf7e79 | ||
|
|
b04de2e76e | ||
|
|
41b836a05b | ||
|
|
969077e755 | ||
|
|
10c6e04db5 | ||
|
|
c4cdddd191 | ||
|
|
444188d653 | ||
|
|
f083ca0cec | ||
|
|
fa434d2097 | ||
|
|
9a3811d48e | ||
|
|
98fc2c2d5b | ||
|
|
a492515ce5 | ||
|
|
5e31998776 | ||
|
|
c43b3e86e3 | ||
|
|
755da724b3 | ||
|
|
942e5d1062 | ||
|
|
483fda785a | ||
|
|
617c642215 | ||
|
|
83e38ae88a | ||
|
|
ed64c3a305 | ||
|
|
f4264a2a8a | ||
|
|
5edb36670b | ||
|
|
7208cb3a65 | ||
|
|
6cfa44c4f1 | ||
|
|
465403f8cb | ||
|
|
b321d5bb69 | ||
|
|
81e7400807 | ||
|
|
73f84491a5 | ||
|
|
59e4a14d15 | ||
|
|
8dd058fd50 | ||
|
|
4a5afac382 | ||
|
|
8336b9e81c | ||
|
|
d82d8b061a | ||
|
|
21906c971e | ||
|
|
06f394232c | ||
|
|
9dc4236f1e | ||
|
|
de15d97839 | ||
|
|
14009f6041 | ||
|
|
5ba3e912c9 | ||
|
|
bcad387fe4 | ||
|
|
cb266952b7 | ||
|
|
28da654141 | ||
|
|
c2a87459cd | ||
|
|
ce7dd0aa6f | ||
|
|
703403f182 | ||
|
|
13cfff1aad | ||
|
|
961867941a | ||
|
|
3c02121ebe | ||
|
|
ff492a1f22 | ||
|
|
f300788c1e | ||
|
|
1b9162b6e5 | ||
|
|
b266916f7f | ||
|
|
e8a31c3985 | ||
|
|
72e2873e21 | ||
|
|
4324ac7c05 | ||
|
|
3590a8fec3 | ||
|
|
d48e0a8e79 | ||
|
|
52d60946bc | ||
|
|
798df6fdcc | ||
|
|
d229da9e47 | ||
|
|
ddc7226b64 | ||
|
|
47253bbda1 | ||
|
|
db8c6d5af9 | ||
|
|
86f71fc111 | ||
|
|
11dcafd904 | ||
|
|
e8798a9536 | ||
|
|
84b086d076 | ||
|
|
bfa690ae56 | ||
|
|
680494f30a | ||
|
|
b469cf5455 | ||
|
|
d9ebae88fa | ||
|
|
118d6d370e | ||
|
|
00a9a21de3 | ||
|
|
f2363eebd8 | ||
|
|
d9565c07bd | ||
|
|
ad80e2b3d3 | ||
|
|
92511e0535 | ||
|
|
3a5f0d28da | ||
|
|
7c0333bf19 | ||
|
|
c7f73e36eb | ||
|
|
2806cbcf8b | ||
|
|
1359ec77fe | ||
|
|
05697f7ab3 | ||
|
|
41dc440ef8 | ||
|
|
a32efa876f | ||
|
|
8f42f50a01 | ||
|
|
87ec92ecc8 | ||
|
|
435db94254 | ||
|
|
007297e4ff | ||
|
|
895cf0e72c | ||
|
|
3531932c88 | ||
|
|
a3810f4f51 | ||
|
|
aa08432ea8 | ||
|
|
7bc782b7c0 | ||
|
|
75fc732c69 | ||
|
|
0cfc342153 | ||
|
|
712377a1b5 | ||
|
|
4cffa3d8b9 | ||
|
|
d49d7ef943 | ||
|
|
72cad07118 | ||
|
|
352fa03b0a | ||
|
|
d0b2bab7b1 | ||
|
|
7c43d73066 | ||
|
|
9918b07f51 | ||
|
|
171a5104ae | ||
|
|
e59ed89a0b | ||
|
|
8cdc71e22f | ||
|
|
a0de27a78e | ||
|
|
9c89adbdee | ||
|
|
0cf5101931 | ||
|
|
7c17e19cbb | ||
|
|
f4c4ca8cec | ||
|
|
c22e957d4d | ||
|
|
3d3089f479 | ||
|
|
41fe3d718a | ||
|
|
3be9dd6741 | ||
|
|
cabb761024 | ||
|
|
70a9f1b2b0 | ||
|
|
8688211277 | ||
|
|
5a99839ed7 | ||
|
|
19a5c21162 | ||
|
|
bea3b8e70a | ||
|
|
0f075008a4 | ||
|
|
b1fb3406a0 | ||
|
|
f3e551fc4a | ||
|
|
ba80504c9e | ||
|
|
c50b4a6d2f | ||
|
|
fb1dc9d95a | ||
|
|
9d75a429a6 | ||
|
|
bb62986000 | ||
|
|
48eec08509 | ||
|
|
41bca24bfa | ||
|
|
9950a2ba21 | ||
|
|
4bf413cffc | ||
|
|
d68cb4933e | ||
|
|
17ed624e40 | ||
|
|
6c9a8a1bc2 | ||
|
|
de8639ad63 | ||
|
|
c576133b42 | ||
|
|
bb47c3696c | ||
|
|
1a3edb9a61 | ||
|
|
b7e715361e | ||
|
|
1d4bcdf54f | ||
|
|
6ac6e1f2de | ||
|
|
6b9e9da2f1 | ||
|
|
56dd99458e | ||
|
|
9cd2bb9bd7 | ||
|
|
95f4326f9a | ||
|
|
1ca0e8e4e1 | ||
|
|
25f9d29ffd | ||
|
|
45d2f7dd6e | ||
|
|
a666659ca0 | ||
|
|
6acb28a178 | ||
|
|
ade870d843 | ||
|
|
654063a33f | ||
|
|
11063be327 | ||
|
|
2ab28e3d41 | ||
|
|
780635dc9a | ||
|
|
6a5980e3da | ||
|
|
b187ee9777 | ||
|
|
6534a847f8 | ||
|
|
f98f853c76 | ||
|
|
206bb6e8e8 | ||
|
|
94a14e3ae2 | ||
|
|
432953323c | ||
|
|
eec1678585 | ||
|
|
f097866d38 | ||
|
|
9c119d6bd2 | ||
|
|
65e51decb2 | ||
|
|
431bad226d | ||
|
|
a0e156e42c | ||
|
|
0fd048d030 | ||
|
|
df6c5c6db2 | ||
|
|
4762dbc336 | ||
|
|
b2b66477d4 | ||
|
|
89fc82bfd7 | ||
|
|
8dc1dd329a | ||
|
|
1298e3160d | ||
|
|
d54e21d726 | ||
|
|
e00120b046 | ||
|
|
ec4e2a2f76 | ||
|
|
0a96ce9323 | ||
|
|
0b56a76f0c | ||
|
|
76ca051ae3 | ||
|
|
6e40f239fb | ||
|
|
5fec44d44e | ||
|
|
db20af15e6 | ||
|
|
1bbb9784cd | ||
|
|
90071667ac | ||
|
|
1c21097f59 | ||
|
|
fe97022182 | ||
|
|
0cdb46a79c | ||
|
|
7071385e22 | ||
|
|
5c6d8984c2 | ||
|
|
bd3a835ce9 | ||
|
|
418372367e | ||
|
|
8ff7b24aa5 | ||
|
|
747aee5291 | ||
|
|
270bb213ca | ||
|
|
d309778f76 | ||
|
|
d3086d0c09 | ||
|
|
0641639643 | ||
|
|
246be8bcf2 | ||
|
|
1b9396ca1b | ||
|
|
0f06c15a78 | ||
|
|
faaac23c66 | ||
|
|
0407725437 | ||
|
|
bab8d3eb2a | ||
|
|
1cef3c17a4 | ||
|
|
bd74184799 | ||
|
|
8416c3f764 | ||
|
|
bb70ddb3d9 | ||
|
|
9d17c14bcd | ||
|
|
776c45be3a | ||
|
|
f11f0933b4 | ||
|
|
d48e44ea55 | ||
|
|
b35f959394 | ||
|
|
b71991a3e4 | ||
|
|
c80c8d3ab2 | ||
|
|
4654c48f22 | ||
|
|
6da901e7bb | ||
|
|
d223acefcf | ||
|
|
41152de276 | ||
|
|
2a9f3a62fa | ||
|
|
677a9129a2 | ||
|
|
c65b5c8a18 | ||
|
|
0a49ca91d8 | ||
|
|
8f48e9f70c | ||
|
|
0471e43c2a | ||
|
|
6bc21cc27e | ||
|
|
9755cd6753 | ||
|
|
0c21cacded | ||
|
|
522e0338da | ||
|
|
f0a788e44f | ||
|
|
9c52ea2fa4 | ||
|
|
a6488fe711 | ||
|
|
d8fd8df182 | ||
|
|
d47c080aaf | ||
|
|
8c11071f2e | ||
|
|
1b4b14c1d7 | ||
|
|
bb5c7d4e39 | ||
|
|
4fef8d73d4 | ||
|
|
36665d54b1 | ||
|
|
2908391eee | ||
|
|
01df8eab17 | ||
|
|
2304bca8d4 | ||
|
|
45b7ef4284 | ||
|
|
7353b4a7c2 | ||
|
|
90446e43dc | ||
|
|
bc247b8090 | ||
|
|
f0b3c2e2c6 | ||
|
|
7821f3a75d | ||
|
|
259585929b | ||
|
|
c1e916adc2 | ||
|
|
6bb42d813f | ||
|
|
b30ea404ed | ||
|
|
09a791307d | ||
|
|
1310446c23 | ||
|
|
51910582d8 | ||
|
|
0499e9b121 | ||
|
|
b99b041662 | ||
|
|
be8e615b81 | ||
|
|
4d0e4c4520 | ||
|
|
aa1dfd4560 | ||
|
|
0ba70638e6 | ||
|
|
f6d1b6e86c | ||
|
|
1d49017f41 | ||
|
|
03962d21df | ||
|
|
5cf8a32190 | ||
|
|
a2098d1e85 | ||
|
|
3794237883 | ||
|
|
e6757f5987 | ||
|
|
ecb6e2d011 | ||
|
|
fb465fa09f | ||
|
|
5436087745 | ||
|
|
88b8ac1e9e | ||
|
|
6da05e19a7 | ||
|
|
b549fdba51 | ||
|
|
08cfbcce64 | ||
|
|
591a8845fa | ||
|
|
1efc9850a5 | ||
|
|
3b13910a06 | ||
|
|
e3cd657c97 | ||
|
|
8f0db18c4b | ||
|
|
cf498ee9d5 | ||
|
|
9a549cfa2a | ||
|
|
897c74c6ab | ||
|
|
6fd6fc241c | ||
|
|
fdbb9dd87b | ||
|
|
f8e904a08d | ||
|
|
679447a4df | ||
|
|
536c2122df | ||
|
|
059c983274 | ||
|
|
a5e829408b | ||
|
|
2e7a85efa9 | ||
|
|
8a236e1b67 | ||
|
|
5d9dbc114b | ||
|
|
488811f132 | ||
|
|
4c37d8cf7c | ||
|
|
82630f3508 | ||
|
|
dfb297a6d2 | ||
|
|
c8f53a48ce | ||
|
|
1b9277145c | ||
|
|
ee30c3ce95 | ||
|
|
03fb7c0a93 | ||
|
|
1762c43769 | ||
|
|
bcbf24b84d | ||
|
|
fc73aca4a2 | ||
|
|
f92ee9b886 | ||
|
|
68d855a245 | ||
|
|
72e7469012 | ||
|
|
622a2f6707 | ||
|
|
72697bafa5 | ||
|
|
4fb4878342 | ||
|
|
57e76fe69b | ||
|
|
e1f16a6179 | ||
|
|
9e9684c4db | ||
|
|
efada4c166 | ||
|
|
3fc0193260 | ||
|
|
61a3589dd7 | ||
|
|
8d1c2468f5 | ||
|
|
020413257f | ||
|
|
6c78abdcc0 | ||
|
|
e31a78d153 | ||
|
|
b1d0269211 | ||
|
|
00fcb0b115 | ||
|
|
4bf351b2df | ||
|
|
e9ecceeb77 | ||
|
|
413dec3e0f | ||
|
|
414c26c31b | ||
|
|
fce026766b | ||
|
|
fe3f90d266 | ||
|
|
5f6b404576 | ||
|
|
9b8348bc04 | ||
|
|
1403d062e9 | ||
|
|
7bf0e0036c | ||
|
|
76c957903f | ||
|
|
68ea8001f1 | ||
|
|
5a249fc3e1 | ||
|
|
9bac0f6490 | ||
|
|
fa664c97f1 | ||
|
|
8e218ade3c | ||
|
|
22af5c7df6 | ||
|
|
04b8b741e2 | ||
|
|
8af9f75a10 | ||
|
|
fabeaf1471 | ||
|
|
1ffd7ea9d6 | ||
|
|
3af490522e | ||
|
|
0280b64084 | ||
|
|
83aad06574 | ||
|
|
892c6efed2 | ||
|
|
5b888aafc0 | ||
|
|
d918e8059a | ||
|
|
82ddca6b50 | ||
|
|
6fb7d9583c | ||
|
|
181c5f0789 | ||
|
|
e486665efd | ||
|
|
b659400c88 | ||
|
|
98c7e3f751 | ||
|
|
5e1df8b511 | ||
|
|
16ec1f3920 | ||
|
|
b01e8f4d23 | ||
|
|
2413a0bb6d | ||
|
|
bf6091e997 | ||
|
|
9e0034dfac | ||
|
|
637142cec6 | ||
|
|
c304c9c761 | ||
|
|
4dd007395f | ||
|
|
7573656060 | ||
|
|
8407363aaf | ||
|
|
55fd64c254 | ||
|
|
d151834048 | ||
|
|
ce47224400 | ||
|
|
61fbf9850b | ||
|
|
ba1df4660c | ||
|
|
d44e24592d | ||
|
|
91aa127dad | ||
|
|
1be876678c | ||
|
|
388981be31 | ||
|
|
6497751375 | ||
|
|
5f0639c157 | ||
|
|
e435a1a937 | ||
|
|
932eda8115 | ||
|
|
5d91f4d343 | ||
|
|
703698b25f | ||
|
|
b884e924b6 | ||
|
|
ee8847c5cb | ||
|
|
99c787c4f5 | ||
|
|
2083c078fd | ||
|
|
bc42a72836 | ||
|
|
c3b7c6f4bb | ||
|
|
b913dbb3b8 | ||
|
|
f16eefa9df | ||
|
|
96132553ae | ||
|
|
709779b7fd | ||
|
|
11ac3d32eb | ||
|
|
c8bbcfed56 | ||
|
|
76a6f6c301 | ||
|
|
2de2920acf | ||
|
|
fd8be9f23f | ||
|
|
e66e5a23e1 | ||
|
|
335fd8e3c3 | ||
|
|
237ebe1d59 | ||
|
|
5e5e404d7d | ||
|
|
16e869c5da | ||
|
|
c628b2ab68 | ||
|
|
b7d9f822de | ||
|
|
fe606cbf15 | ||
|
|
7c71f41d95 | ||
|
|
9728d966ad | ||
|
|
ce610cab03 | ||
|
|
d8f14c6905 | ||
|
|
bdf4f07a28 | ||
|
|
206a51baf7 | ||
|
|
94b51b9971 | ||
|
|
8984cfdcad | ||
|
|
022d18578e | ||
|
|
9beb8b435a | ||
|
|
26b790bfd8 | ||
|
|
308315dee4 | ||
|
|
5fac396487 | ||
|
|
99e36178a5 | ||
|
|
8999076291 | ||
|
|
220c6d7d4a | ||
|
|
f989dc26d2 | ||
|
|
3107d504ce | ||
|
|
b8c0bb3283 | ||
|
|
4aa8739ee8 | ||
|
|
bcfcfdd704 | ||
|
|
a59872c9e9 | ||
|
|
cbf4bafa36 | ||
|
|
95584d7437 | ||
|
|
20e348ed60 | ||
|
|
fbcc460f10 | ||
|
|
e7e1a39e05 | ||
|
|
18fd65350c | ||
|
|
1551bf8244 | ||
|
|
882f9405db | ||
|
|
af3608c81c | ||
|
|
395d0c3ade | ||
|
|
f0bcb3ac03 | ||
|
|
3579b9dc6a | ||
|
|
dbb45eaffd | ||
|
|
fc7d7fef5a | ||
|
|
3f234276ee | ||
|
|
b10bc5f8c6 | ||
|
|
a9e727fd51 | ||
|
|
c9b350eddc | ||
|
|
21c297c73b | ||
|
|
7df7b31d97 | ||
|
|
d8d4870eb7 | ||
|
|
aa15e8b713 | ||
|
|
621df40a7f | ||
|
|
147f7078a0 | ||
|
|
55a67f191a | ||
|
|
31587d7181 | ||
|
|
d0f714a5e2 | ||
|
|
e4ed447009 | ||
|
|
57ea002953 | ||
|
|
1b5a230132 | ||
|
|
7348402364 | ||
|
|
915a4598b1 | ||
|
|
0b017b2abb | ||
|
|
c12dbf247c | ||
|
|
f087c172fc | ||
|
|
fe3e99e4e1 | ||
|
|
442b99771a | ||
|
|
5f9fb06c2a | ||
|
|
dfae82d4cf | ||
|
|
549a822491 | ||
|
|
b6f242107e | ||
|
|
9023f03cee | ||
|
|
500b19d1b9 | ||
|
|
421c5ea29c | ||
|
|
cea8ce0def | ||
|
|
daf1349e75 | ||
|
|
ffbc1f5e5d | ||
|
|
a02e23763a | ||
|
|
c275381cfd | ||
|
|
34eb27dfe3 | ||
|
|
d3702ab6ac | ||
|
|
dafba8cdee | ||
|
|
995ce8c00b | ||
|
|
1e15fea5e4 | ||
|
|
3139437bfe | ||
|
|
59112b79fa | ||
|
|
fd486270b9 | ||
|
|
042f9b5a89 | ||
|
|
28e96647fa | ||
|
|
35eacb45a1 | ||
|
|
c42108bd22 | ||
|
|
63588148a7 | ||
|
|
52d253a1db | ||
|
|
bf7d83134a | ||
|
|
77bba7e587 | ||
|
|
fb999df526 | ||
|
|
314a3d1898 | ||
|
|
35f1085870 | ||
|
|
cfdaabfc0b | ||
|
|
146aeed893 | ||
|
|
02b594e405 | ||
|
|
bd3504b818 | ||
|
|
743b05911c | ||
|
|
1b2af77196 | ||
|
|
1f13b11f88 | ||
|
|
1cba6761bb | ||
|
|
920bba1bf3 | ||
|
|
1d3cba9517 | ||
|
|
331a102a19 | ||
|
|
6fe461dc0c | ||
|
|
baf3da51c2 | ||
|
|
48084544ba | ||
|
|
6a02f96af2 | ||
|
|
3c72bc6c4d | ||
|
|
3027ab6c5b | ||
|
|
ffc6bb4c41 | ||
|
|
5df42d5344 | ||
|
|
4ad8c52c38 | ||
|
|
741954ac2b | ||
|
|
75d6576603 | ||
|
|
8a9e9e0d27 | ||
|
|
1065f32a3a | ||
|
|
223c7473e5 | ||
|
|
f951eddfe5 | ||
|
|
8dfb513bb0 | ||
|
|
ab7a453507 | ||
|
|
5b112286d9 | ||
|
|
c4c5fb9866 | ||
|
|
bd711b57f3 | ||
|
|
94f2e3e573 | ||
|
|
b7a94f4b03 | ||
|
|
21b94bc4fd | ||
|
|
745e04baa6 | ||
|
|
4b611893e9 | ||
|
|
b8fc003a4c | ||
|
|
ed579b4e72 | ||
|
|
d8ce736cc0 | ||
|
|
cb8f11449f | ||
|
|
efb691f481 | ||
|
|
a8c2459d5f | ||
|
|
0074b28d3a | ||
|
|
760cf604eb | ||
|
|
8ce155df6a | ||
|
|
cff617b0c3 | ||
|
|
127fbbd4e1 | ||
|
|
3adfa91c54 | ||
|
|
7f00318cbb | ||
|
|
5433f516a9 | ||
|
|
81fcae2d4e | ||
|
|
0c1e1a7134 | ||
|
|
518e1511d2 | ||
|
|
b92630ecd8 | ||
|
|
11137a2a8f | ||
|
|
9310ff3ea1 | ||
|
|
37428c91b6 | ||
|
|
9e04d59b61 | ||
|
|
c1f2971881 | ||
|
|
8c28228357 | ||
|
|
90fd0ef44e | ||
|
|
6137bdbbbc | ||
|
|
2acf636842 | ||
|
|
0ad51d6c58 | ||
|
|
6f97b9d8c2 | ||
|
|
a64cf1d577 | ||
|
|
e373d23cc9 | ||
|
|
72888e4ea9 | ||
|
|
2bf212ccfe | ||
|
|
5fba250b1d | ||
|
|
7315360fbc | ||
|
|
d965e72822 | ||
|
|
288a585809 | ||
|
|
272eb9002c | ||
|
|
dee470609c | ||
|
|
ad8f368a91 | ||
|
|
5d855a1338 | ||
|
|
3defad20cc | ||
|
|
9b4f330c4a | ||
|
|
7d10ec6097 | ||
|
|
8d443ac506 | ||
|
|
1521a766e5 | ||
|
|
a1edded5df | ||
|
|
fa507d492f | ||
|
|
0ae67e8a87 | ||
|
|
051b14c5b2 | ||
|
|
f610f37ac3 | ||
|
|
501881c602 | ||
|
|
ea8f0e2101 | ||
|
|
d3520e9098 | ||
|
|
b29b9102a9 | ||
|
|
64fbaa2a42 | ||
|
|
4c4380a87f | ||
|
|
d8452c42ad | ||
|
|
09ba39642e | ||
|
|
11c6f2f965 | ||
|
|
9a01c0e179 | ||
|
|
06fc88f2b0 | ||
|
|
f19a9129ba | ||
|
|
f785dfd7ad | ||
|
|
8185324a0a | ||
|
|
88214676a8 | ||
|
|
9f9eda5ce4 | ||
|
|
75bb35d00a | ||
|
|
d4006421e9 | ||
|
|
0fe47c4b13 | ||
|
|
5c97301c6d | ||
|
|
de5f67dd3c | ||
|
|
da108a4031 | ||
|
|
5fb3be503c | ||
|
|
6ea42e82c0 | ||
|
|
6e1d9c7ee7 | ||
|
|
4cdb761737 | ||
|
|
51e3abd483 |
File diff suppressed because it is too large
Load Diff
24
.github/workflows/openvidu-integration-tests.yml
vendored
24
.github/workflows/openvidu-integration-tests.yml
vendored
@ -14,21 +14,13 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout OpenVidu Local Deployment
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: OpenVidu/openvidu-local-deployment
|
||||
ref: development
|
||||
path: openvidu-local-deployment
|
||||
|
||||
- name: Configure OpenVidu Local Deployment
|
||||
working-directory: ./openvidu-local-deployment/community
|
||||
run: |
|
||||
./configure_lan_private_ip_linux.sh
|
||||
sed -i 's/interval: 10s/interval: 1s/' livekit.yaml
|
||||
sed -i '/interval: 1s/a \ fixer_interval: 10s' livekit.yaml
|
||||
docker compose pull
|
||||
|
||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
||||
with:
|
||||
ref-openvidu-local-deployment: development
|
||||
pre_startup_commands: |
|
||||
sed -i 's/interval: 10s/interval: 1s/' livekit.yaml
|
||||
sed -i '/interval: 1s/a \ fixer_interval: 10s' livekit.yaml
|
||||
- name: Install LiveKit CLI
|
||||
run: |
|
||||
curl -sSL https://get.livekit.io/cli | bash
|
||||
@ -58,3 +50,7 @@ jobs:
|
||||
name: openvidu-integration-tests-report
|
||||
path: ./openvidu/openvidu-test-integration/test-results.json
|
||||
retention-days: 7
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
uses: OpenVidu/actions/cleanup@main
|
||||
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -27,3 +27,9 @@ nbactions.xml
|
||||
*/.tscache/*
|
||||
|
||||
.factorypath
|
||||
.terraform
|
||||
.terraform.lock.hcl
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfstate.lock.info
|
||||
*.tfvars
|
||||
|
||||
@ -39,7 +39,7 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com
|
||||
|
||||
OpenVidu has been supported under project "CPP2021-008720 NewGenVidu: An elastic, user-friendly and privacy-friendly videoconferencing platform", funded by MCIN/AEI/10.13039/501100011033 and by the European Union-NextGenerationEU/PRTR.
|
||||
|
||||
<img height="75px" src="https://openvidu.io/img/logos/support.jpg">
|
||||
<img height="75px" src="https://docs.openvidu.io/en/stable/img/logos/support.jpg">
|
||||
|
||||
## Sponsors
|
||||
|
||||
|
||||
5
openvidu-components-angular/.gitignore
vendored
5
openvidu-components-angular/.gitignore
vendored
@ -10,9 +10,4 @@
|
||||
node_modules
|
||||
dist/
|
||||
docs/
|
||||
openvidu-webcomponent/
|
||||
coverage/**
|
||||
e2e/webcomponent-app/openvidu-webcomponent-*.css
|
||||
e2e/webcomponent-app/openvidu-webcomponent-*.js
|
||||
|
||||
e2e/assets/*
|
||||
@ -18,7 +18,8 @@
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": {
|
||||
"base": "dist/openvidu-components-testapp"
|
||||
"base": "dist/openvidu-components-testapp",
|
||||
"browser": ""
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"polyfills": ["zone.js"],
|
||||
@ -58,7 +59,7 @@
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
"maximumError": "12kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -148,84 +149,35 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"openvidu-webcomponent": {
|
||||
"projectType": "application",
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/openvidu-webcomponent-rc",
|
||||
"index": "src/index.html",
|
||||
"main": "src/app/openvidu-webcomponent/openvidu-webcomponent.main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "src/app/openvidu-webcomponent/tsconfig.openvidu-webcomponent.json",
|
||||
"aot": true,
|
||||
"assets": ["src/favicon.ico"],
|
||||
"styles": ["src/app/openvidu-webcomponent/openvidu-webcomponent.component.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1mb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"testing": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.testing.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "1mb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
232
openvidu-components-angular/e2e/README.md
Normal file
232
openvidu-components-angular/e2e/README.md
Normal file
@ -0,0 +1,232 @@
|
||||
# E2E Testing Documentation
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Technology Stack](#technology-stack)
|
||||
3. [Test Coverage](#test-coverage)
|
||||
4. [Test Types](#test-types)
|
||||
5. [Test Files Structure](#test-files-structure)
|
||||
6. [Running Tests](#running-tests)
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains end-to-end (E2E) tests for the OpenVidu Components Angular library. The test suite validates the complete functionality of the library components, including UI interactions, media handling, real-time communication features, and API directives.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
The E2E test suite is built using the following technologies:
|
||||
|
||||
- **Selenium WebDriver**: Browser automation framework for UI testing
|
||||
- **Jasmine**: Testing framework providing describe/it syntax and assertions
|
||||
- **TypeScript**: Programming language for type-safe test development
|
||||
- **ChromeDriver**: Chrome browser automation driver
|
||||
- **Fake Media Devices**: Simulated audio/video devices for testing without real hardware
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- `selenium-webdriver` (v4.39.0): Core automation library
|
||||
- `jasmine` (v5.3.1): Test runner and assertion framework
|
||||
- `chromedriver` (v143.0.0): Chrome browser driver
|
||||
- `@types/selenium-webdriver`: TypeScript type definitions
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The test suite provides comprehensive coverage across the following functional areas:
|
||||
|
||||
### Core Features
|
||||
- **API Directives**: Component configuration and display options (41 tests)
|
||||
- **Events**: Component lifecycle and interaction events (24 tests)
|
||||
- **Stream Management**: Video/audio stream handling and display (32 tests)
|
||||
- **Media Devices**: Device selection, permissions, and virtual devices (7 tests)
|
||||
- **Panels**: UI navigation and panel management (6 tests)
|
||||
- **Toolbar**: Media control buttons and functionality (2 tests)
|
||||
- **Chat**: Messaging functionality and UI (3 tests)
|
||||
- **Screen Sharing**: Screen share capabilities and behavior (8 tests)
|
||||
- **Virtual Backgrounds**: Background effects and manipulation (5 tests)
|
||||
|
||||
### Nested Components Testing
|
||||
- **Structural Directives**: Custom component templates and layouts (30 tests)
|
||||
- **Attribute Directives**: Component visibility and behavior controls (16 tests)
|
||||
- **Events**: Nested component event handling (10 tests)
|
||||
|
||||
### Internal Functionality
|
||||
- **Internal Directives**: Library-specific directive behavior (5 tests)
|
||||
|
||||
### Disabled Tests
|
||||
- **Captions**: Captions feature tests (currently commented out, awaiting implementation)
|
||||
|
||||
## Test Types
|
||||
|
||||
### 1. UI Interaction Tests
|
||||
Tests that validate user interface elements and their interactions:
|
||||
- Button visibility and functionality
|
||||
- Panel opening/closing
|
||||
- Component rendering
|
||||
- Layout behavior
|
||||
- Visual element presence
|
||||
|
||||
**Example**: Testing microphone mute/unmute button functionality
|
||||
|
||||
### 2. Media Device Tests
|
||||
Tests focused on audio/video device handling:
|
||||
- Device selection and switching
|
||||
- Virtual device integration
|
||||
- Permission handling
|
||||
- Track management
|
||||
- Media stream validation
|
||||
|
||||
**Example**: Testing video device replacement with custom virtual devices
|
||||
|
||||
### 3. API Directive Tests
|
||||
Tests verifying component configuration through Angular directives:
|
||||
- Component display settings (minimal UI, language, prejoin)
|
||||
- Feature toggles (buttons, panels, toolbar elements)
|
||||
- Media settings (video/audio enabled/disabled)
|
||||
- UI customization options
|
||||
|
||||
**Example**: Testing hiding toolbar buttons via directives
|
||||
|
||||
### 4. Event Tests
|
||||
Tests validating event emission and handling:
|
||||
- Component lifecycle events
|
||||
- User interaction events
|
||||
- Media state change events
|
||||
- Panel state change events
|
||||
- Recording/broadcasting events
|
||||
|
||||
**Example**: Testing onVideoEnabledChanged event emission
|
||||
|
||||
### 5. Multi-Participant Tests
|
||||
Tests simulating multiple participants:
|
||||
- Message exchange between participants
|
||||
- Remote participant display
|
||||
- Screen sharing with multiple users
|
||||
- Participant panel functionality
|
||||
|
||||
**Example**: Testing chat message reception between two participants
|
||||
|
||||
### 6. Structural Customization Tests
|
||||
Tests for component template customization:
|
||||
- Custom toolbar templates
|
||||
- Custom panel templates
|
||||
- Custom layout templates
|
||||
- Custom stream templates
|
||||
- Additional component injection
|
||||
|
||||
**Example**: Testing custom toolbar rendering with additional buttons
|
||||
|
||||
### 7. Screen Sharing Tests
|
||||
Tests specific to screen sharing features:
|
||||
- Screen share toggle
|
||||
- Pin/unpin behavior
|
||||
- Multiple simultaneous screen shares
|
||||
- Screen share with audio/video states
|
||||
|
||||
**Example**: Testing screen share video pinning behavior
|
||||
|
||||
### 8. Virtual Background Tests
|
||||
Tests for background effects:
|
||||
- Background panel interaction
|
||||
- Effect application
|
||||
- Background state management
|
||||
- Prejoin and in-room background handling
|
||||
|
||||
**Example**: Testing background effect application in prejoin
|
||||
|
||||
## Test Files Structure
|
||||
|
||||
```
|
||||
e2e/
|
||||
├── api-directives.test.ts # API directive configuration tests (41 tests)
|
||||
├── events.test.ts # Component event emission tests (24 tests)
|
||||
├── stream.test.ts # Video/audio stream tests (32 tests)
|
||||
├── media-devices.test.ts # Device handling tests (7 tests)
|
||||
├── panels.test.ts # Panel navigation tests (6 tests)
|
||||
├── toolbar.test.ts # Toolbar functionality tests (2 tests)
|
||||
├── chat.test.ts # Chat feature tests (3 tests)
|
||||
├── screensharing.test.ts # Screen sharing tests (8 tests)
|
||||
├── virtual-backgrounds.test.ts # Virtual backgrounds tests (5 tests)
|
||||
├── internal-directives.test.ts # Internal directive tests (5 tests)
|
||||
├── captions.test.ts # Captions tests (currently disabled)
|
||||
├── config.ts # Test configuration
|
||||
├── selenium.conf.ts # Selenium browser configuration
|
||||
├── utils.po.test.ts # Page Object utilities
|
||||
└── nested-components/
|
||||
├── structural-directives.test.ts # Template customization tests (30 tests)
|
||||
├── attribute-directives.test.ts # Visibility directive tests (16 tests)
|
||||
└── events.test.ts # Nested event tests (10 tests)
|
||||
```
|
||||
|
||||
### Support Files
|
||||
|
||||
- **config.ts**: Global test configuration and timeout settings
|
||||
- **selenium.conf.ts**: Browser capabilities, Chrome options, and test environment setup
|
||||
- **utils.po.test.ts**: Page Object Model implementation with reusable helper methods
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Individual Test Suites
|
||||
|
||||
Execute specific test files using npm scripts:
|
||||
|
||||
```bash
|
||||
# API directives tests
|
||||
npm run e2e:lib-directives
|
||||
|
||||
# Event tests
|
||||
npm run e2e:lib-events
|
||||
|
||||
# Chat tests
|
||||
npm run e2e:lib-chat
|
||||
|
||||
# Media devices tests
|
||||
npm run e2e:lib-media-devices
|
||||
|
||||
# Panel tests
|
||||
npm run e2e:lib-panels
|
||||
|
||||
# Screen sharing tests
|
||||
npm run e2e:lib-screensharing
|
||||
|
||||
# Stream tests
|
||||
npm run e2e:lib-stream
|
||||
|
||||
# Toolbar tests
|
||||
npm run e2e:lib-toolbar
|
||||
|
||||
# Virtual backgrounds tests
|
||||
npm run e2e:lib-virtual-backgrounds
|
||||
|
||||
# Internal directives tests
|
||||
npm run e2e:lib-internal-directives
|
||||
|
||||
# All nested component tests
|
||||
npm run e2e:nested-all
|
||||
|
||||
# Nested events tests
|
||||
npm run e2e:nested-events
|
||||
|
||||
# Nested structural directives tests
|
||||
npm run e2e:nested-structural-directives
|
||||
|
||||
# Nested attribute directives tests
|
||||
npm run e2e:nested-attribute-directives
|
||||
```
|
||||
|
||||
### Test Execution Process
|
||||
|
||||
1. Tests are compiled from TypeScript to JavaScript using `tsc --project ./e2e`
|
||||
2. Jasmine executes the compiled tests from `./e2e/dist/` directory
|
||||
3. Selenium WebDriver launches Chrome browser instances
|
||||
4. Tests interact with the application running at `http://localhost:4200`
|
||||
5. Test results are reported in the console
|
||||
|
||||
### Environment Modes
|
||||
|
||||
Tests support two execution modes:
|
||||
|
||||
- **DEV Mode**: Local development with visible browser
|
||||
- **CI Mode**: Continuous integration with headless browser and additional Chrome flags
|
||||
|
||||
Mode is controlled via `LAUNCH_MODE` environment variable.
|
||||
@ -1,29 +1,35 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
let url = '';
|
||||
|
||||
describe('Testing API Directives', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
url = `${TestAppConfig.appUrl}&roomName=API_DIRECTIVES_${Math.floor(Math.random() * 1000)}`;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
|
||||
try {
|
||||
if (await utils.isPresent('#session-container')) {
|
||||
await utils.leaveRoom();
|
||||
}
|
||||
} catch (error) {}
|
||||
await browser.sleep(500);
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
@ -83,7 +89,7 @@ describe('Testing API Directives', () => {
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('#lang-btn-compact');
|
||||
await utils.waitForElement('.language-selector');
|
||||
|
||||
const element = await utils.waitForElement('#join-button');
|
||||
expect(await element.getText()).toEqual('Unirme ahora');
|
||||
@ -102,20 +108,20 @@ describe('Testing API Directives', () => {
|
||||
const panelTitle = await utils.waitForElement('.panel-title');
|
||||
expect(await panelTitle.getText()).toEqual('Configuración');
|
||||
|
||||
const element = await utils.waitForElement('#lang-selected-name');
|
||||
expect(await element.getAttribute('innerText')).toEqual('Español');
|
||||
const element = await utils.waitForElement('.lang-name');
|
||||
expect(await element.getAttribute('innerText')).toEqual('Español expand_more');
|
||||
});
|
||||
|
||||
it('should override the LANG OPTIONS', async () => {
|
||||
await browser.get(`${url}&prejoin=true&langOptions=true`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
await utils.waitForElement('#lang-btn-compact');
|
||||
await utils.clickOn('#lang-btn-compact');
|
||||
await utils.waitForElement('.language-selector');
|
||||
await utils.clickOn('.language-selector');
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
|
||||
|
||||
await utils.clickOn('.lang-menu-opt');
|
||||
await utils.clickOn('.language-option');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.clickOn('#join-button');
|
||||
@ -130,12 +136,12 @@ describe('Testing API Directives', () => {
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.waitForElement('.lang-button');
|
||||
await utils.clickOn('.lang-button');
|
||||
await utils.waitForElement('.full-lang-button');
|
||||
await utils.clickOn('.full-lang-button');
|
||||
|
||||
await browser.sleep(500);
|
||||
|
||||
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
|
||||
});
|
||||
|
||||
it('should show the PREJOIN page', async () => {
|
||||
@ -168,7 +174,7 @@ describe('Testing API Directives', () => {
|
||||
});
|
||||
|
||||
it('should show the token error WITH prejoin page', async () => {
|
||||
const fixedUrl = `${url}&roomName=TEST_TOKEN&participantName=PNAME`;
|
||||
const fixedUrl = `${TestAppConfig.appUrl}&roomName=TEST_TOKEN&participantName=PNAME`;
|
||||
await browser.get(`${fixedUrl}`);
|
||||
|
||||
// Checking if prejoin page exist
|
||||
@ -198,7 +204,7 @@ describe('Testing API Directives', () => {
|
||||
});
|
||||
|
||||
it('should show the token error WITHOUT prejoin page', async () => {
|
||||
const fixedUrl = `${url}&roomName=TOKEN_ERROR&prejoin=false&participantName=PNAME`;
|
||||
const fixedUrl = `${TestAppConfig.appUrl}&roomName=TOKEN_ERROR&prejoin=false&participantName=PNAME`;
|
||||
await browser.get(`${fixedUrl}`);
|
||||
|
||||
// Checking if session container is present
|
||||
@ -232,11 +238,11 @@ describe('Testing API Directives', () => {
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.waitForElement('#video-poster');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(0);
|
||||
|
||||
await utils.waitForElement('#videocam_off');
|
||||
expect(await utils.isPresent('#videocam_off')).toBeTrue();
|
||||
|
||||
await utils.waitForElement('#video-poster');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should run the app with VIDEO DISABLED and WITHOUT PREJOIN page', async () => {
|
||||
@ -281,6 +287,7 @@ describe('Testing API Directives', () => {
|
||||
it('should run the app with AUDIO DISABLED and WITHOUT PREJOIN page', async () => {
|
||||
await browser.get(`${url}&prejoin=false&audioEnabled=false`);
|
||||
|
||||
await browser.sleep(1000);
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
// Checking if video is displayed
|
||||
@ -292,6 +299,30 @@ describe('Testing API Directives', () => {
|
||||
expect(await utils.isPresent('#mic_off')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should run the app without camera button', async () => {
|
||||
await browser.get(`${url}&prejoin=false&cameraBtn=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
// Checking if toolbar is present
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Checking if camera button is not present
|
||||
expect(await utils.isPresent('#camera-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should run the app without microphone button', async () => {
|
||||
await browser.get(`${url}&prejoin=falseµphoneBtn=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
// Checking if toolbar is present
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Checking if microphone button is not present
|
||||
expect(await utils.isPresent('#microphone-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the SCREENSHARE button', async () => {
|
||||
await browser.get(`${url}&prejoin=false&screenshareBtn=false`);
|
||||
|
||||
@ -508,7 +539,7 @@ describe('Testing API Directives', () => {
|
||||
|
||||
it('should HIDE the MUTE button in participants panel', async () => {
|
||||
const roomName = 'e2etest';
|
||||
const fixedUrl = `${url}&prejoin=false&participantMuteBtn=false&roomName=${roomName}`;
|
||||
const fixedUrl = `${TestAppConfig.appUrl}&prejoin=false&participantMuteBtn=false&roomName=${roomName}`;
|
||||
await browser.get(fixedUrl);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
@ -527,8 +558,9 @@ describe('Testing API Directives', () => {
|
||||
expect(await utils.isPresent('#remote-participant-item')).toBeFalse();
|
||||
|
||||
// Starting new browser for adding a new participant
|
||||
const newTabScript = `window.open("${fixedUrl}")`;
|
||||
const newTabScript = `window.open("${fixedUrl}&participantName=SecondParticipant")`;
|
||||
await browser.executeScript(newTabScript);
|
||||
await browser.sleep(10000);
|
||||
|
||||
// Go to first tab
|
||||
const tabs = await browser.getAllWindowHandles();
|
||||
BIN
openvidu-components-angular/e2e/assets/audio_test.wav
Normal file
BIN
openvidu-components-angular/e2e/assets/audio_test.wav
Normal file
Binary file not shown.
@ -1,9 +1,8 @@
|
||||
import { Builder, Key, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
//TODO: Uncomment when captions are implemented
|
||||
// describe('Testing captions features', () => {
|
||||
@ -11,10 +10,10 @@ const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
// let utils: OpenViduComponentsPO;
|
||||
// async function createChromeBrowser(): Promise<WebDriver> {
|
||||
// return await new Builder()
|
||||
// .forBrowser(WebComponentConfig.browserName)
|
||||
// .withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
// .setChromeOptions(WebComponentConfig.browserOptions)
|
||||
// .usingServer(WebComponentConfig.seleniumAddress)
|
||||
// .forBrowser(TestAppConfig.browserName)
|
||||
// .withCapabilities(TestAppConfig.browserCapabilities)
|
||||
// .setChromeOptions(TestAppConfig.browserOptions)
|
||||
// .usingServer(TestAppConfig.seleniumAddress)
|
||||
// .build();
|
||||
// }
|
||||
|
||||
@ -177,4 +176,4 @@ const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
// expect(await button.getText()).toEqual('settingsEspañol');
|
||||
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
@ -1,9 +1,8 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Testing CHAT features', () => {
|
||||
let browser: WebDriver;
|
||||
@ -11,10 +10,10 @@ describe('Testing CHAT features', () => {
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -24,6 +23,10 @@ describe('Testing CHAT features', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
// leaving room if connected
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export const LAUNCH_MODE = process.env.LAUNCH_MODE || 'DEV';
|
||||
export const OPENVIDU_CALL_SERVER = process.env.OPENVIDU_CALL_SERVER || 'http://localhost:6080';
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
|
||||
|
||||
@ -1,20 +1,19 @@
|
||||
import { Builder, Key, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Testing videoconference EVENTS', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
const isHeadless: boolean = (WebComponentConfig.browserOptions as any).options_.args.includes('--headless');
|
||||
const isHeadless: boolean = (TestAppConfig.browserOptions as any).options_.args.includes('--headless');
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -24,6 +23,10 @@ describe('Testing videoconference EVENTS', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
// leaving room if connected
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
@ -57,23 +60,6 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent('#onTokenRequested')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onRoomDisconnected event', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Clicking to leave button
|
||||
const leaveButton = await utils.waitForElement('#leave-btn');
|
||||
expect(await utils.isPresent('#leave-btn')).toBeTrue();
|
||||
await leaveButton.click();
|
||||
|
||||
// Checking if onRoomDisconnected has been received
|
||||
await utils.waitForElement('#onRoomDisconnected');
|
||||
expect(await utils.isPresent('#onRoomDisconnected')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onVideoEnabledChanged event when clicking on the prejoin', async () => {
|
||||
await browser.get(url);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
@ -106,35 +92,12 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onVideoEnabledChanged event when clicking on the settings panel', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
await utils.togglePanel('settings');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.clickOn('#video-opt');
|
||||
|
||||
await utils.waitForElement('ov-video-devices-select');
|
||||
await utils.clickOn('ov-video-devices-select #camera-button');
|
||||
// Checking if onVideoEnabledChanged has been received
|
||||
await utils.waitForElement('#onVideoEnabledChanged-false');
|
||||
expect(await utils.isPresent('#onVideoEnabledChanged-false')).toBeTrue();
|
||||
|
||||
await utils.clickOn('ov-video-devices-select #camera-button');
|
||||
await utils.waitForElement('#onVideoEnabledChanged-true');
|
||||
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onVideoDeviceChanged event on prejoin', async () => {
|
||||
await browser.get(`${url}&fakeDevices=true`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('#video-devices-form');
|
||||
await utils.clickOn('#video-devices-form');
|
||||
await utils.waitForElement('#video-dropdown');
|
||||
await utils.clickOn('#video-dropdown');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_video_1');
|
||||
await utils.clickOn('#option-custom_fake_video_1');
|
||||
@ -156,8 +119,8 @@ describe('Testing videoconference EVENTS', () => {
|
||||
await utils.clickOn('#video-opt');
|
||||
|
||||
await utils.waitForElement('ov-video-devices-select');
|
||||
await utils.waitForElement('#video-devices-form');
|
||||
await utils.clickOn('#video-devices-form');
|
||||
await utils.waitForElement('#video-dropdown');
|
||||
await utils.clickOn('#video-dropdown');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_video_1');
|
||||
await utils.clickOn('#option-custom_fake_video_1');
|
||||
@ -198,35 +161,12 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onAudioEnabledChanged event when clicking on the settings panel', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
await utils.togglePanel('settings');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.clickOn('#audio-opt');
|
||||
|
||||
await utils.waitForElement('ov-audio-devices-select');
|
||||
await utils.clickOn('ov-audio-devices-select #microphone-button');
|
||||
// Checking if onAudioEnabledChanged has been received
|
||||
await utils.waitForElement('#onAudioEnabledChanged-false');
|
||||
expect(await utils.isPresent('#onAudioEnabledChanged-false')).toBeTrue();
|
||||
|
||||
await utils.clickOn('ov-audio-devices-select #microphone-button');
|
||||
await utils.waitForElement('#onAudioEnabledChanged-true');
|
||||
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onAudioDeviceChanged event on prejoin', async () => {
|
||||
await browser.get(`${url}&fakeDevices=true`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('#audio-devices-form');
|
||||
await utils.clickOn('#audio-devices-form');
|
||||
await utils.waitForElement('#audio-dropdown');
|
||||
await utils.clickOn('#audio-dropdown');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_audio_1');
|
||||
await utils.clickOn('#option-custom_fake_audio_1');
|
||||
@ -248,8 +188,8 @@ describe('Testing videoconference EVENTS', () => {
|
||||
await utils.clickOn('#audio-opt');
|
||||
|
||||
await utils.waitForElement('ov-audio-devices-select');
|
||||
await utils.waitForElement('#audio-devices-form');
|
||||
await utils.clickOn('#audio-devices-form');
|
||||
await utils.waitForElement('#audio-dropdown');
|
||||
await utils.clickOn('#audio-dropdown');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_audio_1');
|
||||
await utils.clickOn('#option-custom_fake_audio_1');
|
||||
@ -262,8 +202,8 @@ describe('Testing videoconference EVENTS', () => {
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('#lang-btn-compact');
|
||||
await utils.clickOn('#lang-btn-compact');
|
||||
await utils.waitForElement('.language-selector');
|
||||
await utils.clickOn('.language-selector');
|
||||
|
||||
await browser.sleep(500);
|
||||
await utils.clickOn('#lang-opt-es');
|
||||
@ -283,8 +223,8 @@ describe('Testing videoconference EVENTS', () => {
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.waitForElement('.lang-button');
|
||||
await utils.clickOn('.lang-button');
|
||||
await utils.waitForElement('.full-lang-button');
|
||||
await utils.clickOn('.full-lang-button');
|
||||
|
||||
await browser.sleep(500);
|
||||
await utils.clickOn('#lang-opt-es');
|
||||
@ -412,7 +352,7 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent('#onSettingsPanelStatusChanged-false')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onRecordingStartRequested event when clicking toolbar button', async () => {
|
||||
it('should receive the onRecordingStartRequested and onRecordingStopRequested event when clicking toolbar button', async () => {
|
||||
const roomName = 'recordingToolbarEvent';
|
||||
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
|
||||
|
||||
@ -424,9 +364,15 @@ describe('Testing videoconference EVENTS', () => {
|
||||
// Checking if onRecordingStartRequested has been received
|
||||
await utils.waitForElement(`#onRecordingStartRequested-${roomName}`);
|
||||
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
|
||||
});
|
||||
|
||||
xit('should receive the onRecordingStopRequested event when clicking toolbar button', async () => {});
|
||||
await utils.waitForElement('.activity-status.started');
|
||||
|
||||
await utils.toggleRecordingFromToolbar();
|
||||
|
||||
// Checking if onRecordingStopRequested has been received
|
||||
await utils.waitForElement(`#onRecordingStopRequested-${roomName}`);
|
||||
expect(await utils.isPresent(`#onRecordingStopRequested-${roomName}`)).toBeTrue();
|
||||
});
|
||||
|
||||
xit('should receive the onBroadcastingStopRequested event when clicking toolbar button', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
@ -460,7 +406,7 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent('#onBroadcastingStopRequested')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onRecordingStartRequested when clicking from activities panel', async () => {
|
||||
it('should receive the onRecordingStartRequested and onRecordingStopRequested when clicking from activities panel', async () => {
|
||||
const roomName = 'recordingActivitiesEvent';
|
||||
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
|
||||
|
||||
@ -486,8 +432,6 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
|
||||
});
|
||||
|
||||
xit('should receive the onRecordingStopRequested when clicking from activities panel', async () => {});
|
||||
|
||||
xit('should receive the onRecordingDeleteRequested event', async () => {
|
||||
let element;
|
||||
const roomName = 'deleteRecordingEvent';
|
||||
@ -613,7 +557,7 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent('#onReadyToJoin')).toBeFalse();
|
||||
});
|
||||
|
||||
// * PUBLISHER EVENTS
|
||||
// PARTICIPANT EVENTS
|
||||
|
||||
it('should receive onParticipantCreated event from LOCAL participant', async () => {
|
||||
const participantName = 'TEST_USER';
|
||||
@ -622,22 +566,39 @@ describe('Testing videoconference EVENTS', () => {
|
||||
expect(await utils.isPresent(`#${participantName}-onParticipantCreated`)).toBeTrue();
|
||||
});
|
||||
|
||||
// * ROOM EVENTS
|
||||
|
||||
it('should receive roomDisconnected event from LOCAL participant', async () => {
|
||||
const participantName = 'TEST_USER';
|
||||
let element;
|
||||
await browser.get(`${url}&prejoin=false&participantName=${participantName}`);
|
||||
it('should receive the onParticipantLeft event', async () => {
|
||||
await browser.get(`${url}&prejoin=false&redirectToHome=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Checking if leave button is not present
|
||||
element = await utils.waitForElement('#leave-btn');
|
||||
await element.click();
|
||||
// Clicking to leave button
|
||||
const leaveButton = await utils.waitForElement('#leave-btn');
|
||||
expect(await utils.isPresent('#leave-btn')).toBeTrue();
|
||||
await leaveButton.click();
|
||||
|
||||
await utils.waitForElement(`#roomDisconnected`);
|
||||
expect(await utils.isPresent(`#roomDisconnected`)).toBeTrue();
|
||||
await utils.waitForElement('#events');
|
||||
// Checking if onParticipantLeft has been received
|
||||
await utils.waitForElement('#onParticipantLeft');
|
||||
expect(await utils.isPresent('#onParticipantLeft')).toBeTrue();
|
||||
});
|
||||
|
||||
// * ROOM EVENTS
|
||||
|
||||
//TODO: Implement a mechanism to emulate network disconnection
|
||||
// it('should receive the onRoomDisconnected event', async () => {
|
||||
// await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
// await utils.checkSessionIsPresent();
|
||||
|
||||
// await utils.checkToolbarIsPresent();
|
||||
|
||||
// // Emulate network disconnection
|
||||
// await utils.forceCloseWebsocket();
|
||||
|
||||
// // Checking if onRoomDisconnected has been received
|
||||
// await utils.waitForElement('#onRoomDisconnected');
|
||||
// expect(await utils.isPresent('#onRoomDisconnected')).toBeTrue();
|
||||
// });
|
||||
});
|
||||
97
openvidu-components-angular/e2e/internal-directives.test.ts
Normal file
97
openvidu-components-angular/e2e/internal-directives.test.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
|
||||
let url = '';
|
||||
|
||||
describe('Testing Internal Directives', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
url = `${TestAppConfig.appUrl}&roomName=INTERNAL_DIRECTIVES_${Math.floor(Math.random() * 1000)}`;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
} catch (error) {}
|
||||
await browser.sleep(500);
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should show/hide toolbar view recording button with toolbarViewRecordingsButton directive', async () => {
|
||||
await browser.get(`${url}&prejoin=false&toolbarViewRecordingsButton=true`);
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.toggleToolbarMoreOptions();
|
||||
expect(await utils.isPresent('#view-recordings-btn')).toBeTrue();
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await browser.navigate().refresh();
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.toggleToolbarMoreOptions();
|
||||
expect(await utils.isPresent('#view-recordings-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should show/hide participant name in prejoin with prejoinDisplayParticipantName directive', async () => {
|
||||
await browser.get(`${url}&prejoin=true`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
expect(await utils.isPresent('.participant-name-container')).toBeTrue();
|
||||
await browser.get(`${url}&prejoin=true&prejoinDisplayParticipantName=false`);
|
||||
await browser.navigate().refresh();
|
||||
await utils.checkPrejoinIsPresent();
|
||||
expect(await utils.isPresent('.participant-name-container')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should show/hide view recordings button with recordingActivityViewRecordingsButton directive', async () => {
|
||||
await browser.get(`${url}&prejoin=false&recordingActivityViewRecordingsButton=true`);
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.togglePanel('activities');
|
||||
await utils.clickOn('#recording-activity');
|
||||
expect(await utils.isPresent('#view-recordings-btn')).toBeTrue();
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await browser.navigate().refresh();
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.togglePanel('activities');
|
||||
await utils.clickOn('#recording-activity');
|
||||
expect(await utils.isPresent('#view-recordings-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should show/hide start/stop recording buttons with recordingActivityStartStopRecordingButton directive', async () => {
|
||||
await browser.get(`${url}&prejoin=false&recordingActivityStartStopRecordingButton=false`);
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.togglePanel('activities');
|
||||
await utils.clickOn('#recording-activity');
|
||||
expect(await utils.isPresent('#start-recording-btn')).toBeFalse();
|
||||
|
||||
await browser.sleep(3000);
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await browser.navigate().refresh();
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.togglePanel('activities');
|
||||
await utils.clickOn('#recording-activity');
|
||||
expect(await utils.isPresent('#start-recording-btn')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should show/hide theme selector with showThemeSelector directive', async () => {
|
||||
await browser.get(`${url}&prejoin=false&showThemeSelector=true`);
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.togglePanel('settings');
|
||||
expect(await utils.isPresent('.theme-selector-container')).toBeTrue();
|
||||
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await browser.navigate().refresh();
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.togglePanel('settings');
|
||||
expect(await utils.isPresent('.theme-selector-container')).toBeFalse();
|
||||
});
|
||||
});
|
||||
@ -1,19 +1,18 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { getBrowserOptionsWithoutDevices, WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { getBrowserOptionsWithoutDevices, TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Testing replace track with emulated devices', () => {
|
||||
describe('Media Devices: Virtual Device Replacement and Permissions Handling', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -23,118 +22,99 @@ describe('Testing replace track with emulated devices', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should replace the video track in prejoin page', async () => {
|
||||
it('should allow selecting and replacing the video track with a custom virtual device in the prejoin page', async () => {
|
||||
const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;';
|
||||
|
||||
await browser.get(`${url}&fakeDevices=true`);
|
||||
|
||||
let videoDevices = await utils.waitForElement('#video-devices-form');
|
||||
|
||||
let videoDevices = await utils.waitForElement('#video-dropdown');
|
||||
await videoDevices.click();
|
||||
|
||||
let element = await utils.waitForElement('#option-custom_fake_video_1');
|
||||
|
||||
await element.click();
|
||||
|
||||
let videoLabel;
|
||||
|
||||
await browser.sleep(1000);
|
||||
videoLabel = await browser.executeScript<string>(script);
|
||||
expect(videoLabel).toEqual('custom_fake_video_1');
|
||||
|
||||
await videoDevices.click();
|
||||
|
||||
element = await utils.waitForElement('#option-fake_device_0');
|
||||
await element.click();
|
||||
|
||||
await browser.sleep(1000);
|
||||
videoLabel = await browser.executeScript<string>(script);
|
||||
expect(videoLabel).toEqual('fake_device_0');
|
||||
});
|
||||
|
||||
it('should replace the video track in videoconference page', async () => {
|
||||
it('should allow selecting and replacing the video track with a custom virtual device in the videoconference page', async () => {
|
||||
const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;';
|
||||
|
||||
await browser.get(`${url}&prejoin=false&fakeDevices=true`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
// Checking if toolbar is present
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
await utils.togglePanel('settings');
|
||||
|
||||
await utils.waitForElement('.settings-container');
|
||||
expect(await utils.isPresent('.settings-container')).toBeTrue();
|
||||
|
||||
await browser.sleep(500);
|
||||
await utils.clickOn('#video-opt');
|
||||
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
|
||||
|
||||
let videoDevices = await utils.waitForElement('#video-devices-form');
|
||||
|
||||
let videoDevices = await utils.waitForElement('#video-dropdown');
|
||||
await videoDevices.click();
|
||||
|
||||
let element = await utils.waitForElement('#option-custom_fake_video_1');
|
||||
|
||||
await element.click();
|
||||
|
||||
let videoLabel;
|
||||
await browser.sleep(1000);
|
||||
videoLabel = await browser.executeScript<string>(script);
|
||||
expect(videoLabel).toEqual('custom_fake_video_1');
|
||||
|
||||
await videoDevices.click();
|
||||
|
||||
element = await utils.waitForElement('#option-fake_device_0');
|
||||
await element.click();
|
||||
|
||||
await browser.sleep(1000);
|
||||
videoLabel = await browser.executeScript<string>(script);
|
||||
expect(videoLabel).toEqual('fake_device_0');
|
||||
});
|
||||
|
||||
// TODO: Uncommented when Livekit allows to replace the screen track
|
||||
// it('should replace the screen track', async () => {
|
||||
// const script = 'return document.getElementsByClassName("OV_video-element screen-type")[0].srcObject.getVideoTracks()[0].label;';
|
||||
it('should replace the screen track with a custom virtual device', async () => {
|
||||
const script = 'return document.getElementsByClassName("OV_video-element screen-type")[0].srcObject.getVideoTracks()[0].label;';
|
||||
|
||||
// await browser.get(`${url}&prejoin=false&fakeDevices=true`);
|
||||
await browser.get(`${url}&prejoin=false&fakeDevices=true`);
|
||||
|
||||
// await utils.checkLayoutPresent();
|
||||
// await utils.checkToolbarIsPresent();
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// await utils.clickOn('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
|
||||
// await browser.sleep(500);
|
||||
await browser.sleep(500);
|
||||
|
||||
// let screenLabel = await browser.executeScript<string>(script);
|
||||
// expect(screenLabel).not.toEqual('custom_fake_screen');
|
||||
let screenLabel = await browser.executeScript<string>(script);
|
||||
expect(screenLabel).not.toEqual('custom_fake_screen');
|
||||
|
||||
// await utils.clickOn('#video-settings-btn-SCREEN');
|
||||
// await browser.sleep(500);
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// await utils.waitForElement('.video-settings-menu');
|
||||
// const replaceBtn = await utils.waitForElement('#replace-screen-button');
|
||||
// await replaceBtn.sendKeys(Key.ENTER);
|
||||
await utils.waitForElement('#replace-screen-button');
|
||||
await utils.clickOn('#replace-screen-button');
|
||||
await browser.sleep(1000);
|
||||
|
||||
// await browser.sleep(1000);
|
||||
// screenLabel = await browser.executeScript<string>(script);
|
||||
// expect(screenLabel).to.be.toEqual('custom_fake_screen');
|
||||
// });
|
||||
screenLabel = await browser.executeScript<string>(script);
|
||||
expect(screenLabel).toEqual('custom_fake_screen');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Testing WITHOUT MEDIA DEVICES permissions', () => {
|
||||
describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(getBrowserOptionsWithoutDevices())
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -144,75 +124,54 @@ describe('Testing WITHOUT MEDIA DEVICES permissions', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should be able to ACCESS to PREJOIN page', async () => {
|
||||
it('should camera and microphone buttons be disabled in the prejoin page when permissions are denied', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
let button = await utils.waitForElement('#camera-button');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
|
||||
button = await utils.waitForElement('#microphone-button');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
await utils.waitForElement('#no-video-device-message');
|
||||
await utils.waitForElement('#no-audio-device-message');
|
||||
expect(await utils.isPresent('#backgrounds-button')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should be able to ACCESS to ROOM page', async () => {
|
||||
it('should camera and microphone buttons be disabled in the room page when permissions are denied', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.clickOn('#join-button');
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
let button = await utils.waitForElement('#camera-btn');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
|
||||
button = await utils.waitForElement('#mic-btn');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should be able to ACCESS to ROOM page without prejoin', async () => {
|
||||
it('should camera and microphone buttons be disabled in the room page without prejoin when permissions are denied', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
let button = await utils.waitForElement('#camera-btn');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
|
||||
button = await utils.waitForElement('#mic-btn');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should the settings buttons be disabled', async () => {
|
||||
it('should show an audio and video device warning in settings when permissions are denied', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Open more options menu
|
||||
await utils.togglePanel('settings');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('.settings-container');
|
||||
expect(await utils.isPresent('.settings-container')).toBeTrue();
|
||||
|
||||
await utils.clickOn('#video-opt');
|
||||
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
|
||||
|
||||
let button = await utils.waitForElement('#camera-button');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
|
||||
await utils.waitForElement('#no-video-device-message');
|
||||
await utils.clickOn('#audio-opt');
|
||||
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
|
||||
|
||||
button = await utils.waitForElement('#microphone-button');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
await utils.waitForElement('#no-audio-device-message');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,380 @@
|
||||
import { Builder, By, WebDriver } from 'selenium-webdriver';
|
||||
|
||||
import { NestedConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
|
||||
const url = NestedConfig.appUrl;
|
||||
|
||||
describe('OpenVidu Components ATTRIBUTE toolbar directives', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(NestedConfig.browserName)
|
||||
.withCapabilities(NestedConfig.browserCapabilities)
|
||||
.setChromeOptions(NestedConfig.browserOptions)
|
||||
.usingServer(NestedConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should HIDE the CHAT PANEL BUTTON', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#chatPanelButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Check if chat button does not exist
|
||||
expect(await utils.isPresent('chat-panel-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the PARTICIPANTS PANEL BUTTON', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#participantsPanelButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Check if participants button does not exist
|
||||
expect(await utils.isPresent('participants-panel-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the ACTIVITIES PANEL BUTTON', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#activitiesPanelButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Check if participants button does not exist
|
||||
expect(await utils.isPresent('activities-panel-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the DISPLAY LOGO', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#displayLogo-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
expect(await utils.isPresent('branding-logo')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the DISPLAY ROOM name', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#displayRoomName-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
expect(await utils.isPresent('session-name')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the FULLSCREEN button', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#fullscreenButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Open more options menu
|
||||
await utils.clickOn('#more-options-btn');
|
||||
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#more-options-menu');
|
||||
|
||||
// Checking if fullscreen button is not present
|
||||
expect(await utils.isPresent('#fullscreen-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the STREAMING button', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#broadcastingButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Open more options menu
|
||||
await utils.clickOn('#more-options-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#more-options-menu');
|
||||
|
||||
// Checking if fullscreen button is not present
|
||||
expect(await utils.isPresent('#broadcasting-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the LEAVE button', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#leaveButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
expect(await utils.isPresent('leave-btn')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the SCREENSHARE button', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
|
||||
await utils.clickOn('#screenshareButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
expect(await utils.isPresent('screenshare-btn')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenVidu Components ATTRIBUTE stream directives', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(NestedConfig.browserName)
|
||||
.withCapabilities(NestedConfig.browserCapabilities)
|
||||
.setChromeOptions(NestedConfig.browserOptions)
|
||||
.usingServer(NestedConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should HIDE the AUDIO detector', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovStream-checkbox');
|
||||
|
||||
await utils.clickOn('#displayAudioDetection-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.waitForElement('#session-container');
|
||||
await utils.waitForElement('#custom-stream');
|
||||
|
||||
expect(await utils.isPresent('audio-wave-container')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the PARTICIPANT NAME', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovStream-checkbox');
|
||||
|
||||
await utils.clickOn('#displayParticipantName-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.waitForElement('#session-container');
|
||||
await utils.waitForElement('#custom-stream');
|
||||
|
||||
expect(await utils.isPresent('participant-name-container')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the SETTINGS button', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovStream-checkbox');
|
||||
|
||||
await utils.clickOn('#settingsButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.waitForElement('#custom-stream');
|
||||
|
||||
expect(await utils.isPresent('settings-container')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenVidu Components ATTRIBUTE participant panels directives', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(NestedConfig.browserName)
|
||||
.withCapabilities(NestedConfig.browserCapabilities)
|
||||
.setChromeOptions(NestedConfig.browserOptions)
|
||||
.usingServer(NestedConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should HIDE the participant MUTE button', async () => {
|
||||
const fixedSession = `${url}?sessionId=fixedNameTesting`;
|
||||
await browser.get(`${fixedSession}`);
|
||||
|
||||
await utils.clickOn('#ovParticipantPanelItem-checkbox');
|
||||
|
||||
await utils.clickOn('#muteButton-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
await utils.clickOn('#participants-panel-btn');
|
||||
|
||||
await utils.waitForElement('#participants-container');
|
||||
|
||||
// Starting new browser for adding a new participant
|
||||
const newTabScript = `window.open("${fixedSession}")`;
|
||||
await browser.executeScript(newTabScript);
|
||||
|
||||
// Get tabs opened
|
||||
const tabs = await browser.getAllWindowHandles();
|
||||
// Focus on the last tab
|
||||
browser.switchTo().window(tabs[1]);
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
// Switch to first tab
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
|
||||
await utils.waitForElement('#remote-participant-item');
|
||||
|
||||
expect(await utils.isPresent('mute-btn')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenVidu Components ATTRIBUTE activity panel directives', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(NestedConfig.browserName)
|
||||
.withCapabilities(NestedConfig.browserCapabilities)
|
||||
.setChromeOptions(NestedConfig.browserOptions)
|
||||
.usingServer(NestedConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should HIDE the RECORDING activity', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovActivitiesPanel-checkbox');
|
||||
|
||||
await utils.clickOn('#recordingActivity-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
await utils.clickOn('#activities-panel-btn');
|
||||
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#custom-activities-panel');
|
||||
|
||||
expect(await utils.isPresent('ov-recording-activity')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should HIDE the STREAMING activity', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovActivitiesPanel-checkbox');
|
||||
|
||||
await utils.clickOn('#broadcastingActivity-checkbox');
|
||||
|
||||
await utils.clickOn('#apply-btn');
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
await utils.clickOn('#activities-panel-btn');
|
||||
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#custom-activities-panel');
|
||||
|
||||
await utils.waitForElement('ov-recording-activity');
|
||||
|
||||
expect(await utils.isPresent('ov-broadcasting-activity')).toBeFalse();
|
||||
});
|
||||
});
|
||||
@ -5,7 +5,7 @@ import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
|
||||
const url = NestedConfig.appUrl;
|
||||
|
||||
describe('Testing EVENTS', () => {
|
||||
describe('OpenVidu Components EVENTS', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
@ -24,10 +24,13 @@ describe('Testing EVENTS', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should receive the onRoomDisconnected event', async () => {
|
||||
it('should receive the onParticipantLeft event', async () => {
|
||||
await browser.get(`${url}`);
|
||||
|
||||
await utils.clickOn('#ovToolbar-checkbox');
|
||||
@ -40,8 +43,8 @@ describe('Testing EVENTS', () => {
|
||||
await utils.clickOn('#leave-btn');
|
||||
|
||||
// Checking if onLeaveButtonClicked has been received
|
||||
await utils.waitForElement('#onRoomDisconnected');
|
||||
expect(await utils.isPresent('#onRoomDisconnected')).toBeTrue();
|
||||
await utils.waitForElement('#onParticipantLeft');
|
||||
expect(await utils.isPresent('#onParticipantLeft')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onVideoEnabledChanged event', async () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +1,19 @@
|
||||
import { Builder, Key, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { getBrowserOptionsWithoutDevices, WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Testing panels', () => {
|
||||
describe('Panels: UI Navigation and Section Switching', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -24,201 +23,110 @@ describe('Testing panels', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* It only works with OpenVidu PRO because this is a PRO feature
|
||||
*/
|
||||
// it('should toggle BACKGROUND panel on prejoin page when VIDEO is MUTED', async () => {
|
||||
// let element;
|
||||
// await browser.get(`${url}`);
|
||||
// element = await utils.waitForElement('#pre-join-container');
|
||||
// expect(await utils.isPresent('#pre-join-container')).toBeTrue();
|
||||
|
||||
// const backgroundButton = await utils.waitForElement('#background-effects-btn');
|
||||
// expect(await utils.isPresent('#background-effects-btn')).toBeTrue();
|
||||
// expect(await backgroundButton.isEnabled()).toBeTrue();
|
||||
// await backgroundButton.click();
|
||||
// await browser.sleep(500);
|
||||
|
||||
// await utils.waitForElement('#background-effects-container');
|
||||
// expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
// element = await utils.waitForElement('#camera-button');
|
||||
// expect(await utils.isPresent('#camera-button')).toBeTrue();
|
||||
// expect(await element.isEnabled()).toBeTrue();
|
||||
// await element.click();
|
||||
|
||||
// await browser.sleep(500);
|
||||
// element = await utils.waitForElement('#video-poster');
|
||||
// expect(await utils.isPresent('#video-poster')).toBeTrue();
|
||||
|
||||
// expect(await backgroundButton.isDisplayed()).toBeTrue();
|
||||
// expect(await backgroundButton.isEnabled()).toBeFalse();
|
||||
|
||||
// expect(await utils.isPresent('#background-effects-container')).toBeFalse();
|
||||
// });
|
||||
|
||||
it('should toggle CHAT panel', async () => {
|
||||
it('should open and close the CHAT panel and verify its content', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
const chatButton = await utils.waitForElement('#chat-panel-btn');
|
||||
await chatButton.click();
|
||||
|
||||
await utils.waitForElement('.sidenav-menu');
|
||||
await utils.waitForElement('.input-container');
|
||||
expect(await utils.isPresent('.input-container')).toBeTrue();
|
||||
|
||||
await utils.waitForElement('.messages-container');
|
||||
expect(await utils.isPresent('.messages-container')).toBeTrue();
|
||||
|
||||
await chatButton.click();
|
||||
|
||||
expect(await utils.isPresent('.input-container')).toBeFalse();
|
||||
expect(await utils.isPresent('.messages-container')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should toggle PARTICIPANTS panel', async () => {
|
||||
it('should open and close the PARTICIPANTS panel and verify its content', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
const participantBtn = await utils.waitForElement('#participants-panel-btn');
|
||||
await participantBtn.click();
|
||||
|
||||
await utils.waitForElement('.sidenav-menu');
|
||||
|
||||
await utils.waitForElement('.local-participant-container');
|
||||
expect(await utils.isPresent('.local-participant-container')).toBeTrue();
|
||||
|
||||
await utils.waitForElement('ov-participant-panel-item');
|
||||
expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue();
|
||||
|
||||
await participantBtn.click();
|
||||
|
||||
expect(await utils.isPresent('.local-participant-container')).toBeFalse();
|
||||
expect(await utils.isPresent('ov-participant-panel-item')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should toggle ACTIVITIES panel', async () => {
|
||||
it('should open and close the ACTIVITIES panel and verify its content', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Get activities button and click into it
|
||||
const activitiesBtn = await utils.waitForElement('#activities-panel-btn');
|
||||
await activitiesBtn.click();
|
||||
|
||||
await utils.waitForElement('.sidenav-menu');
|
||||
await utils.waitForElement('#activities-container');
|
||||
expect(await utils.isPresent('#activities-container')).toBeTrue();
|
||||
|
||||
await utils.waitForElement('#recording-activity');
|
||||
expect(await utils.isPresent('#recording-activity')).toBeTrue();
|
||||
await activitiesBtn.click();
|
||||
|
||||
expect(await utils.isPresent('#activities-container')).toBeFalse();
|
||||
expect(await utils.isPresent('#recording-activity')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should toggle SETTINGS panel', async () => {
|
||||
it('should open the SETTINGS panel and verify its content', async () => {
|
||||
let element;
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Checking if toolbar is present
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
await utils.togglePanel('settings');
|
||||
|
||||
element = await utils.waitForElement('.sidenav-menu');
|
||||
expect(await utils.isPresent('#default-settings-panel')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should switching between PARTICIPANTS and CHAT panels', async () => {
|
||||
it('should switch between PARTICIPANTS and CHAT panels and verify correct content is shown', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Open chat panel
|
||||
const chatButton = await utils.waitForElement('#chat-panel-btn');
|
||||
await chatButton.click();
|
||||
|
||||
await utils.waitForElement('.sidenav-menu');
|
||||
expect(await utils.isPresent('.sidenav-menu')).toBeTrue();
|
||||
|
||||
await utils.waitForElement('.input-container');
|
||||
expect(await utils.isPresent('.input-container')).toBeTrue();
|
||||
|
||||
expect(await utils.isPresent('.messages-container')).toBeTrue();
|
||||
|
||||
// Open participants panel
|
||||
const participantBtn = await utils.waitForElement('#participants-panel-btn');
|
||||
await participantBtn.click();
|
||||
|
||||
await utils.waitForElement('.sidenav-menu');
|
||||
|
||||
expect(await utils.isPresent('.local-participant-container')).toBeTrue();
|
||||
|
||||
expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue();
|
||||
|
||||
// Switch to chat panel
|
||||
await chatButton.click();
|
||||
|
||||
await utils.waitForElement('.sidenav-menu');
|
||||
|
||||
expect(await utils.isPresent('.input-container')).toBeTrue();
|
||||
|
||||
expect(await utils.isPresent('.messages-container')).toBeTrue();
|
||||
|
||||
expect(await utils.isPresent('.local-participant-container')).toBeFalse();
|
||||
|
||||
expect(await utils.isPresent('ov-participant-panel-item')).toBeFalse();
|
||||
|
||||
// Close chat panel
|
||||
await chatButton.click();
|
||||
expect(await utils.getNumberOfElements('.input-container')).toEqual(0);
|
||||
expect(await utils.isPresent('messages-container')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should switching between sections in SETTINGS PANEL', async () => {
|
||||
it('should switch between sections in the SETTINGS panel and verify correct content is shown', async () => {
|
||||
let element;
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Checking if toolbar is present
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
||||
// Open more options menu
|
||||
await utils.togglePanel('settings');
|
||||
|
||||
await utils.waitForElement('.sidenav-menu');
|
||||
expect(await utils.isPresent('.sidenav-menu')).toBeTrue();
|
||||
|
||||
// Check if general section is shown
|
||||
await browser.sleep(500);
|
||||
element = await utils.waitForElement('#general-opt');
|
||||
await element.click();
|
||||
|
||||
expect(await utils.isPresent('ov-participant-name-input')).toBeTrue();
|
||||
|
||||
// Check if video section is shown
|
||||
element = await utils.waitForElement('#video-opt');
|
||||
await element.click();
|
||||
|
||||
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
|
||||
|
||||
// Check if audio section is shown
|
||||
element = await utils.waitForElement('#audio-opt');
|
||||
await element.click();
|
||||
|
||||
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
|
||||
});
|
||||
});
|
||||
355
openvidu-components-angular/e2e/screensharing.test.ts
Normal file
355
openvidu-components-angular/e2e/screensharing.test.ts
Normal file
@ -0,0 +1,355 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('E2E: Screensharing features', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should toggle screensharing on and off twice, updating video count', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Enable screensharing
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
// Disable screensharing
|
||||
await utils.disableScreenShare();
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
|
||||
// Enable again
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
// Disable again
|
||||
await utils.disableScreenShare();
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should show screenshare and muted camera (camera off, screenshare on)', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Mute camera
|
||||
await utils.waitForElement('#camera-btn');
|
||||
await utils.clickOn('#camera-btn');
|
||||
|
||||
// Enable screensharing
|
||||
const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
expect(await screenshareButton.isDisplayed()).toBeTrue();
|
||||
await screenshareButton.click();
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
// Disable screensharing
|
||||
await utils.disableScreenShare();
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should display screensharing with a single pinned video', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Enable screensharing
|
||||
const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
expect(await screenshareButton.isDisplayed()).toBeTrue();
|
||||
await screenshareButton.click();
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should replace pinned video when a second participant starts screensharing', async () => {
|
||||
const roomName = 'screensharingE2E';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// First participant screenshares
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Second participant joins and screenshares
|
||||
const newTabScript = `window.open("${fixedUrl}")`;
|
||||
await browser.executeScript(newTabScript);
|
||||
const tabs = await browser.getAllWindowHandles();
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Switch back to first tab and check
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should unpin screensharing and restore previous pinned video when disabled', async () => {
|
||||
const roomName = 'screensharingtwoE2E';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// First participant screenshares
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Second participant joins and screenshares
|
||||
const tabs = await utils.openTab(fixedUrl);
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Disable screensharing for second participant
|
||||
await utils.disableScreenShare();
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Switch back to first tab and check
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should correctly share screen with microphone muted and maintain proper track state', async () => {
|
||||
// Helper for inspecting stream tracks
|
||||
const getMediaTracks = (className: string) => {
|
||||
return `
|
||||
const tracks = document.getElementsByClassName('${className}')[0].srcObject.getTracks();
|
||||
return tracks.map(track => ({
|
||||
kind: track.kind,
|
||||
enabled: track.enabled,
|
||||
id: track.id,
|
||||
label: track.label
|
||||
}));`;
|
||||
};
|
||||
|
||||
// Setup: Navigate to room and skip prejoin
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Step 1: First mute the microphone
|
||||
const micButton = await utils.waitForElement('#mic-btn');
|
||||
await micButton.click();
|
||||
|
||||
// Step 2: Start screen sharing
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
|
||||
// Step 3: Verify both streams are present
|
||||
await utils.waitForElement('.screen-type');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
// Step 4: Verify screen share track properties
|
||||
const screenTracks: any[] = await browser.executeScript(getMediaTracks('screen-type'));
|
||||
expect(screenTracks.length).toEqual(1);
|
||||
expect(screenTracks[0].kind).toEqual('video');
|
||||
expect(screenTracks[0].enabled).toBeTrue();
|
||||
|
||||
// Step 5: Verify microphone status indicators for both streams
|
||||
// await utils.waitForElement('#status-mic');
|
||||
// const micStatusCount = await utils.getNumberOfElements('#status-mic');
|
||||
// expect(micStatusCount).toEqual(2);
|
||||
|
||||
// Step 6: Stop screen sharing and verify stream count
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
await utils.clickOn('#disable-screen-button');
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
});
|
||||
|
||||
// ==================== PIN/UNPIN TESTS ====================
|
||||
// These tests demonstrate bugs in the pin system:
|
||||
// 1. Multiple screens can be auto-pinned simultaneously
|
||||
// 2. Manual unpins can be overridden by auto-pin logic when participants join
|
||||
|
||||
it('should NOT have multiple screens pinned when both participants share screen', async () => {
|
||||
const roomName = 'pinBugCase1';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
|
||||
// Participant A joins and shares screen
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// Verify A's screen is pinned
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||
const pinnedCountA1 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab A] After A shares: ${pinnedCountA1} pinned stream(s)`);
|
||||
|
||||
// Participant B joins
|
||||
const tabs = await utils.openTab(fixedUrl);
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await utils.checkLayoutPresent();
|
||||
await browser.sleep(1000);
|
||||
|
||||
// B should see A's screen pinned
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3); // 2 cameras + 1 screen
|
||||
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||
const pinnedCountB1 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After B joins: ${pinnedCountB1} pinned stream(s)`);
|
||||
|
||||
// B shares screen
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// B should see only their own screen pinned (auto-pin + unpin previous)
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
|
||||
await utils.waitForElement('.OV_big');
|
||||
const pinnedCountB2 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After B shares: ${pinnedCountB2} pinned stream(s)`);
|
||||
expect(pinnedCountB2).toEqual(1); // Should be 1, but implementation might show different
|
||||
|
||||
// Switch to Tab A and check
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
|
||||
|
||||
// BUG: In A's view, BOTH screens are pinned
|
||||
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab A] After B shares: ${pinnedCountA2} pinned stream(s)`);
|
||||
|
||||
// EXPECTED: Only B's screen should be pinned (the most recent one)
|
||||
// ACTUAL: Both A's and B's screens are pinned
|
||||
expect(pinnedCountA2).toEqual(1, 'BUG DETECTED: Multiple screens are pinned. Expected only the most recent screen to be pinned.');
|
||||
});
|
||||
|
||||
it('should NOT re-pin manually unpinned screen when new participant joins', async () => {
|
||||
const roomName = 'pinBugCase2';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
|
||||
// Participant A joins and shares screen
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// Verify A's screen is auto-pinned
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||
|
||||
// Participant B joins and shares screen
|
||||
const tabs = await utils.openTab(fixedUrl);
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await utils.checkLayoutPresent();
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// B should see their own screen pinned
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
|
||||
await utils.waitForElement('.OV_big');
|
||||
let pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After B shares: ${pinnedCountB} pinned stream(s)`);
|
||||
|
||||
// B manually unpins their own screen
|
||||
const screenStreams = await utils.getScreenShareStreams();
|
||||
if (screenStreams.length > 0) {
|
||||
// Find B's own screen (it should be the pinned one)
|
||||
await utils.toggleStreamPin('.OV_big');
|
||||
await browser.sleep(1000);
|
||||
}
|
||||
|
||||
// Verify B's screen is now unpinned
|
||||
pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After manually unpinning B's screen: ${pinnedCountB} pinned stream(s)`);
|
||||
expect(pinnedCountB).toEqual(0, 'B should have no pinned streams after manual unpin');
|
||||
|
||||
// B manually pins A's screen
|
||||
const screenElements = await utils.getScreenShareStreams();
|
||||
if (screenElements.length >= 2) {
|
||||
// Pin the first screen that is not already pinned (should be A's screen)
|
||||
await utils.toggleStreamPin('.OV_stream.remote .screen-type');
|
||||
await utils.toggleStreamPin('#pin-btn');
|
||||
await browser.sleep(500);
|
||||
}
|
||||
|
||||
// Verify A's screen is now pinned in B's view
|
||||
pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After manually pinning A's screen: ${pinnedCountB} pinned stream(s)`);
|
||||
expect(pinnedCountB).toEqual(1, "Only A's screen should be pinned");
|
||||
|
||||
// Participant C joins the room
|
||||
const tab3 = await utils.openTab(fixedUrl);
|
||||
await browser.switchTo().window(tab3[2]);
|
||||
await utils.checkLayoutPresent();
|
||||
await browser.sleep(1500);
|
||||
|
||||
// Switch back to B's tab
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await browser.sleep(1000);
|
||||
|
||||
// B's screen should still be unpinned, but might get re-pinned automatically
|
||||
pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After C joins: ${pinnedCountB} pinned stream(s)`);
|
||||
|
||||
// EXPECTED: No screens should be pinned (B manually unpinned everything)
|
||||
// ACTUAL: B's screen gets re-pinned automatically
|
||||
expect(pinnedCountB).toEqual(1, 'BUG DETECTED: Only one screen should be pinned after C joins.');
|
||||
|
||||
// Switch back to A's tab to verify
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(500);
|
||||
|
||||
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab A] After C joins: ${pinnedCountA2} pinned stream(s)`);
|
||||
|
||||
// EXPECTED: Only A's screen should be pinned
|
||||
// ACTUAL: A's screen remains pinned
|
||||
expect(pinnedCountA2).toEqual(1, "BUG DETECTED: A's screen should remain pinned after C joins.");
|
||||
});
|
||||
});
|
||||
@ -10,12 +10,14 @@ interface BrowserConfig {
|
||||
browserName: string;
|
||||
}
|
||||
|
||||
const audioPath = LAUNCH_MODE === 'CI' ? `e2e-assets/audio_test.wav` : 'e2e/assets/audio_test.wav';
|
||||
|
||||
const chromeArguments = [
|
||||
'--window-size=1300,1000',
|
||||
'--headless',
|
||||
// '--headless',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--use-file-for-fake-audio-capture=e2e/assets/audio.wav'
|
||||
`--use-file-for-fake-audio-capture=${audioPath}`
|
||||
];
|
||||
const chromeArgumentsCI = [
|
||||
'--window-size=1300,1000',
|
||||
@ -29,7 +31,10 @@ const chromeArgumentsCI = [
|
||||
'--disable-background-networking',
|
||||
'--disable-default-apps',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
'--use-fake-device-for-media-stream'
|
||||
'--use-fake-device-for-media-stream',
|
||||
`--use-file-for-fake-audio-capture=${audioPath}`,
|
||||
'--autoplay-policy=no-user-gesture-required',
|
||||
'--allow-file-access-from-files'
|
||||
];
|
||||
const chromeArgumentsWithoutMediaDevices = ['--headless', '--window-size=1300,900', '--deny-permission-prompts'];
|
||||
const chromeArgumentsWithoutMediaDevicesCI = [
|
||||
@ -46,12 +51,13 @@ const chromeArgumentsWithoutMediaDevicesCI = [
|
||||
'--deny-permission-prompts'
|
||||
];
|
||||
|
||||
export const WebComponentConfig: BrowserConfig = {
|
||||
appUrl: 'http://localhost:8080/',
|
||||
export const TestAppConfig: BrowserConfig = {
|
||||
appUrl: 'http://localhost:4200/#/call?staticVideos=false',
|
||||
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
|
||||
browserName: 'chrome',
|
||||
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
|
||||
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
|
||||
browserOptions: new chrome.Options()
|
||||
.addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments)) as chrome.Options
|
||||
};
|
||||
|
||||
export const NestedConfig: BrowserConfig = {
|
||||
@ -59,13 +65,14 @@ export const NestedConfig: BrowserConfig = {
|
||||
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
|
||||
browserName: 'Chrome',
|
||||
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
|
||||
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
|
||||
browserOptions: new chrome.Options()
|
||||
.addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments)) as chrome.Options
|
||||
};
|
||||
|
||||
export function getBrowserOptionsWithoutDevices() {
|
||||
if (LAUNCH_MODE === 'CI') {
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI);
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI) as chrome.Options;
|
||||
} else {
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices);
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices) as chrome.Options;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import { Builder, ILocation, IRectangle, ISize, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { Builder, IRectangle, WebDriver } from 'selenium-webdriver';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
describe('Stream rendering and media toggling scenarios', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -23,10 +22,13 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should show 0 video element when a participant joins with video disabled', async () => {
|
||||
it('should not render any video element when joining with video disabled', async () => {
|
||||
await browser.get(`${url}&prejoin=true&videoEnabled=false`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
@ -39,7 +41,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should show a video element when a participant joins with audio muted', async () => {
|
||||
it('should render a video element but no audio when joining with audio muted', async () => {
|
||||
await browser.get(`${url}&prejoin=true&audioEnabled=false`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
@ -52,7 +54,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should show a video element when a participant joins', async () => {
|
||||
it('should render both video and audio elements when joining with both enabled', async () => {
|
||||
await browser.get(`${url}&prejoin=true&videoEnabled=true&audioEnabled=true`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
@ -65,7 +67,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should show a video element when a participant shares its screen with VIDEO and AUDIO MUTED', async () => {
|
||||
it('should add a screen share video/audio when sharing screen with both camera and mic muted', async () => {
|
||||
await browser.get(`${url}&prejoin=true&videoEnabled=false&audioEnabled=false`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
@ -79,7 +81,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1); //screen sharse video
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1); //screen share audio
|
||||
@ -90,7 +92,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should show a video element when a LOCAL participant shares its screen', async () => {
|
||||
it('should add a screen share video/audio when sharing screen with both camera and mic enabled', async () => {
|
||||
await browser.get(`${url}&prejoin=true&videoEnabled=true&audioEnabled=true`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
@ -104,7 +106,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(2); //screen share audio and local audio
|
||||
@ -115,9 +117,9 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1);
|
||||
});
|
||||
|
||||
/* ------------ Checking video elements with two participants ------------ */
|
||||
/* ------------ Checking video/audio elements with two participants ------------ */
|
||||
|
||||
it('should show zero video elements when two participants join with VIDEO and AUDIO MUTED', async () => {
|
||||
it('should not render any video/audio elements when two participants join with both video and audio muted', async () => {
|
||||
const roomName = `streams-${Date.now()}`;
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
|
||||
await browser.get(fixedUrl);
|
||||
@ -145,7 +147,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should show two video elements when a two participants join with audio muted', async () => {
|
||||
it('should render two video elements and no audio when two participants join with audio muted', async () => {
|
||||
const roomName = `streams-${Date.now()}`;
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=false`;
|
||||
await browser.get(fixedUrl);
|
||||
@ -158,6 +160,8 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(0);
|
||||
|
||||
const tabs = await utils.openTab(fixedUrl);
|
||||
await browser.sleep(1000);
|
||||
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
|
||||
await utils.waitForElement('.OV_stream.remote');
|
||||
@ -173,7 +177,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should show zero video elements when two participants join with video disabled', async () => {
|
||||
it('should not render any video elements but should render two audio elements when two participants join with video disabled', async () => {
|
||||
const roomName = `streams-${Date.now()}`;
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false`;
|
||||
await browser.get(fixedUrl);
|
||||
@ -186,6 +190,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1);
|
||||
|
||||
const tabs = await utils.openTab(fixedUrl);
|
||||
await browser.sleep(1000);
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
|
||||
await utils.waitForElement('.OV_stream.remote');
|
||||
@ -201,7 +206,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(2);
|
||||
});
|
||||
|
||||
it('should show 3 video elements when a participant shares its screen with AUDIO and VIDEO MUTED', async () => {
|
||||
it('should add a screen share video/audio when a participant with both video and audio muted shares their screen (two participants)', async () => {
|
||||
const roomName = `streams-${Date.now()}`;
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
|
||||
await browser.get(fixedUrl);
|
||||
@ -216,7 +221,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(3);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1); // screen share audios
|
||||
@ -240,7 +245,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(0);
|
||||
});
|
||||
|
||||
it('should show 3 video elements when a REMOTE participant shares its screen', async () => {
|
||||
it('should add a screen share video/audio when a remote participant with both video and audio enabled shares their screen', async () => {
|
||||
const roomName = `streams-${Date.now()}`;
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=true`;
|
||||
await browser.get(fixedUrl);
|
||||
@ -255,7 +260,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(3);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(3); // screen share audios and local audio and remote audio
|
||||
@ -279,7 +284,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(2);
|
||||
});
|
||||
|
||||
it('should show 4 video elements when a two participants share theirs screen', async () => {
|
||||
it('should add a screen share video/audio for both participants when both share their screen with video/audio muted', async () => {
|
||||
const roomName = `streams-${Date.now()}`;
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
|
||||
await browser.get(fixedUrl);
|
||||
@ -299,7 +304,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(4);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(2); // screen share audios
|
||||
@ -323,15 +328,15 @@ describe('Checking stream elements by disabling/enabling the media', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Testing stream features', () => {
|
||||
describe('Stream UI controls and interaction features', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -341,6 +346,9 @@ describe('Testing stream features', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
@ -658,7 +666,7 @@ describe('Testing stream features', () => {
|
||||
expect(streamProps.y).toEqual(0);
|
||||
});
|
||||
|
||||
xit('should show the audio detection elements when participant is speaking', async () => {
|
||||
it('should show the audio detection elements when participant is speaking', async () => {
|
||||
const roomName = 'speakingE2E';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
await browser.get(`${fixedUrl}&audioEnabled=false`);
|
||||
@ -668,25 +676,65 @@ describe('Testing stream features', () => {
|
||||
// Starting new browser for adding the second participant
|
||||
const newTabScript = `window.open("${fixedUrl}")`;
|
||||
await browser.executeScript(newTabScript);
|
||||
await browser.sleep(1000);
|
||||
const tabs = await browser.getAllWindowHandles();
|
||||
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
|
||||
await utils.waitForElement('.OV_stream.remote.speaking');
|
||||
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
|
||||
expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
|
||||
// Wait with retries for audio detection to appear (handles timing issues)
|
||||
const maxRetries = 5;
|
||||
const retryInterval = 1000;
|
||||
let audioDetected = false;
|
||||
|
||||
for (let i = 0; i < maxRetries && !audioDetected; i++) {
|
||||
await browser.sleep(retryInterval);
|
||||
const remoteSpeakingCount = await utils.getNumberOfElements('.OV_stream.remote.speaking');
|
||||
if (remoteSpeakingCount >= 1) {
|
||||
audioDetected = true;
|
||||
console.log(`[Audio Detection] Detected after ${i + 1} attempt(s)`);
|
||||
} else {
|
||||
console.log(`[Audio Detection] Attempt ${i + 1}/${maxRetries}: No audio detected yet`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure at least one remote speaker element is present (timing-sensitive)
|
||||
expect(audioDetected).toBeTrue();
|
||||
if (!audioDetected) {
|
||||
console.error('Audio detection indicator did not appear within timeout');
|
||||
}
|
||||
|
||||
// The local participant is muted; poll briefly to ensure the local stream is not
|
||||
// marked as speaking. This handles timing races where classes may be applied
|
||||
// or removed slightly later.
|
||||
const timeout = 2000;
|
||||
const interval = 200;
|
||||
const start = Date.now();
|
||||
let localNotSpeaking = false;
|
||||
while (Date.now() - start < timeout) {
|
||||
const localCount = await utils.getNumberOfElements('.OV_stream.local.speaking');
|
||||
if (localCount === 0) {
|
||||
localNotSpeaking = true;
|
||||
break;
|
||||
}
|
||||
await browser.sleep(interval);
|
||||
}
|
||||
expect(localNotSpeaking).toBeTrue();
|
||||
if (!localNotSpeaking) {
|
||||
console.error('Local stream should not be marked as speaking when muted');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Testing video is playing', () => {
|
||||
describe('Video playback reliability with different media states', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -696,6 +744,9 @@ describe('Testing video is playing', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Testing TOOLBAR features', () => {
|
||||
describe('Toolbar button functionality for local media control', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -23,10 +22,13 @@ describe('Testing TOOLBAR features', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should mute and unmute the local microphone', async () => {
|
||||
it('should toggle mute/unmute on the local microphone and update the icon accordingly', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
@ -43,7 +45,7 @@ describe('Testing TOOLBAR features', () => {
|
||||
expect(await utils.isPresent('#mic-btn #mic')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should mute and unmute the local camera', async () => {
|
||||
it('should toggle mute/unmute on the local camera and update the icon accordingly', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
@ -1,4 +1,8 @@
|
||||
import * as fs from 'fs';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
import { PNG } from 'pngjs';
|
||||
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
|
||||
type PNGWithMetadata = PNG & { data: Buffer };
|
||||
|
||||
export class OpenViduComponentsPO {
|
||||
private TIMEOUT = 10 * 1000;
|
||||
@ -136,6 +140,42 @@ export class OpenViduComponentsPO {
|
||||
await this.clickOn('#fullscreen-btn');
|
||||
}
|
||||
|
||||
async leaveRoom() {
|
||||
try {
|
||||
// Close any open panels or menus clicking on the body
|
||||
await this.clickOn('body');
|
||||
await this.browser.sleep(300);
|
||||
|
||||
// Verify that the leave button is present
|
||||
await this.waitForElement('#leave-btn');
|
||||
|
||||
// Click on the leave button
|
||||
await this.clickOn('#leave-btn');
|
||||
|
||||
// Verify that the session container is no longer present
|
||||
await this.browser.wait(
|
||||
async () => {
|
||||
return !(await this.isPresent('#session-container'));
|
||||
},
|
||||
this.TIMEOUT,
|
||||
'Session container should disappear after leaving room'
|
||||
);
|
||||
|
||||
// Wait for the prejoin container to be present again
|
||||
await this.browser.sleep(500);
|
||||
|
||||
// Verify that there are no video elements left in the DOM
|
||||
const videoCount = await this.getNumberOfElements('video');
|
||||
if (videoCount > 0) {
|
||||
console.warn(`Warning: ${videoCount} video elements still present after leaving room`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during leaveRoom:', error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async togglePanel(panelName: string) {
|
||||
switch (panelName) {
|
||||
case 'activities':
|
||||
@ -152,6 +192,16 @@ export class OpenViduComponentsPO {
|
||||
await this.waitForElement('#participants-panel-btn');
|
||||
await this.clickOn('#participants-panel-btn');
|
||||
break;
|
||||
case 'backgrounds':
|
||||
await this.waitForElement('#more-options-btn');
|
||||
await this.clickOn('#more-options-btn');
|
||||
|
||||
await this.browser.sleep(500);
|
||||
await this.waitForElement('#virtual-bg-btn');
|
||||
await this.clickOn('#virtual-bg-btn');
|
||||
|
||||
await this.browser.sleep(1000);
|
||||
break;
|
||||
|
||||
case 'settings':
|
||||
await this.toggleToolbarMoreOptions();
|
||||
@ -159,5 +209,120 @@ export class OpenViduComponentsPO {
|
||||
await this.clickOn('#toolbar-settings-btn');
|
||||
break;
|
||||
}
|
||||
|
||||
await this.browser.sleep(500);
|
||||
}
|
||||
|
||||
async applyBackground(bgId: string) {
|
||||
await this.waitForElement('ov-background-effects-panel');
|
||||
await this.browser.sleep(1000);
|
||||
await this.waitForElement(`#effect-${bgId}`);
|
||||
|
||||
await this.clickOn(`#effect-${bgId}`);
|
||||
await this.browser.sleep(2000);
|
||||
}
|
||||
|
||||
async applyVirtualBackgroundFromPrejoin(bgId: string): Promise<void> {
|
||||
await this.waitForElement('#backgrounds-button');
|
||||
await this.clickOn('#backgrounds-button');
|
||||
|
||||
await this.applyBackground(bgId);
|
||||
await this.clickOn('#backgrounds-button');
|
||||
}
|
||||
|
||||
async saveScreenshot(filename: string, element: WebElement) {
|
||||
const image = await element.takeScreenshot();
|
||||
fs.writeFileSync(filename, image, 'base64');
|
||||
}
|
||||
|
||||
async expectVirtualBackgroundApplied(
|
||||
img1Name: string,
|
||||
img2Name: string,
|
||||
{
|
||||
threshold = 0.4,
|
||||
minDiffPixels = 500,
|
||||
debug = false
|
||||
}: {
|
||||
threshold?: number;
|
||||
minDiffPixels?: number;
|
||||
debug?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const beforeImg = PNG.sync.read(fs.readFileSync(img1Name));
|
||||
const afterImg = PNG.sync.read(fs.readFileSync(img2Name));
|
||||
const { width, height } = beforeImg;
|
||||
const diff = new PNG({ width, height });
|
||||
|
||||
// const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {
|
||||
// threshold: 0.4
|
||||
// // alpha: 0.5,
|
||||
// // includeAA: false,
|
||||
// // diffColor: [255, 0, 0]
|
||||
// });
|
||||
|
||||
const numDiffPixels = pixelmatch(beforeImg.data, afterImg.data, diff.data, width, height, {
|
||||
threshold
|
||||
// includeAA: true
|
||||
});
|
||||
|
||||
if (numDiffPixels <= minDiffPixels) {
|
||||
// Sólo guardar los archivos de debug si falla la prueba
|
||||
if (debug) {
|
||||
fs.writeFileSync('before.png', PNG.sync.write(beforeImg));
|
||||
fs.writeFileSync('after.png', PNG.sync.write(afterImg));
|
||||
fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
||||
}
|
||||
}
|
||||
|
||||
expect(numDiffPixels).toBeGreaterThan(minDiffPixels, 'The virtual background was not applied correctly');
|
||||
|
||||
// fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
||||
// expect(numDiffPixels).to.be.greaterThan(500, 'The virtual background was not applied correctly');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins or unpins a stream by clicking on it
|
||||
* @param streamSelector CSS selector for the stream element (e.g., '.screen-type', '.camera-type')
|
||||
*/
|
||||
async toggleStreamPin(streamSelector: string): Promise<void> {
|
||||
const stream = await this.waitForElement(streamSelector);
|
||||
await stream.click();
|
||||
await this.clickOn('#pin-btn');
|
||||
await this.browser.sleep(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of pinned streams (elements with class .OV_big)
|
||||
*/
|
||||
async getNumberOfPinnedStreams(): Promise<number> {
|
||||
return await this.getNumberOfElements('.OV_big');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific stream is pinned
|
||||
* @param streamSelector CSS selector for the stream element
|
||||
*/
|
||||
async isStreamPinned(streamSelector: string): Promise<boolean> {
|
||||
try {
|
||||
const stream = await this.waitForElement(streamSelector);
|
||||
const classes = await stream.getAttribute('class');
|
||||
return classes.includes('OV_big');
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all screen share streams
|
||||
*/
|
||||
async getScreenShareStreams(): Promise<WebElement[]> {
|
||||
return await this.browser.findElements(By.css('.screen-type'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all camera streams
|
||||
*/
|
||||
async getCameraStreams(): Promise<WebElement[]> {
|
||||
return await this.browser.findElements(By.css('.camera-type'));
|
||||
}
|
||||
}
|
||||
|
||||
155
openvidu-components-angular/e2e/virtual-backgrounds.test.ts
Normal file
155
openvidu-components-angular/e2e/virtual-backgrounds.test.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Prejoin: Virtual Backgrounds', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should close BACKGROUNDS on prejoin page when VIDEO is disabled', async () => {
|
||||
let element;
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
const backgroundButton = await utils.waitForElement('#backgrounds-button');
|
||||
expect(await utils.isPresent('#backgrounds-button')).toBeTrue();
|
||||
expect(await backgroundButton.isEnabled()).toBeTrue();
|
||||
await utils.clickOn('#backgrounds-button');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
await utils.clickOn('#camera-button');
|
||||
|
||||
await browser.sleep(500);
|
||||
element = await utils.waitForElement('#video-poster');
|
||||
expect(await utils.isPresent('#video-poster')).toBeTrue();
|
||||
|
||||
expect(await backgroundButton.isDisplayed()).toBeTrue();
|
||||
expect(await backgroundButton.isEnabled()).toBeFalse();
|
||||
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
|
||||
});
|
||||
|
||||
it('should open and close BACKGROUNDS panel on prejoin page', async () => {
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
const backgroundButton = await utils.waitForElement('#backgrounds-button');
|
||||
expect(await utils.isPresent('#backgrounds-button')).toBeTrue();
|
||||
expect(await backgroundButton.isEnabled()).toBeTrue();
|
||||
await backgroundButton.click();
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
await utils.clickOn('#backgrounds-button');
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply a background effect on prejoin page', async () => {
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
let videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('before.png', videoElement);
|
||||
|
||||
await utils.applyVirtualBackgroundFromPrejoin('1');
|
||||
|
||||
await browser.sleep(1000);
|
||||
|
||||
videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('after.png', videoElement);
|
||||
|
||||
await utils.expectVirtualBackgroundApplied('before.png', 'after.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Room: Virtual Backgrounds', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should open and close BACKGROUNDS panel in the room', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.checkToolbarIsPresent();
|
||||
await utils.togglePanel('backgrounds');
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
await utils.togglePanel('backgrounds');
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply a background effect in the room', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
await utils.togglePanel('backgrounds');
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
let videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('before.png', videoElement);
|
||||
|
||||
await utils.applyBackground('1');
|
||||
|
||||
await browser.sleep(1000);
|
||||
|
||||
videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('after.png', videoElement);
|
||||
|
||||
await utils.expectVirtualBackgroundApplied('before.png', 'after.png');
|
||||
});
|
||||
});
|
||||
@ -1,277 +0,0 @@
|
||||
import monkeyPatchMediaDevices from './utils/media-devices.js';
|
||||
|
||||
var MINIMAL;
|
||||
var LANG;
|
||||
var CAPTIONS_LANG;
|
||||
var CUSTOM_LANG_OPTIONS;
|
||||
var CUSTOM_CAPTIONS_LANG_OPTIONS;
|
||||
var PREJOIN;
|
||||
var VIDEO_ENABLED;
|
||||
var AUDIO_ENABLED;
|
||||
|
||||
var SCREENSHARE_BUTTON;
|
||||
var FULLSCREEN_BUTTON;
|
||||
var ACTIVITIES_PANEL_BUTTON;
|
||||
var RECORDING_BUTTON;
|
||||
var BROADCASTING_BUTTON;
|
||||
var CHAT_PANEL_BUTTON;
|
||||
var DISPLAY_LOGO;
|
||||
var DISPLAY_ROOM_NAME;
|
||||
var DISPLAY_PARTICIPANT_NAME;
|
||||
var DISPLAY_AUDIO_DETECTION;
|
||||
var VIDEO_CONTROLS;
|
||||
var LEAVE_BUTTON;
|
||||
var PARTICIPANT_MUTE_BUTTON;
|
||||
var PARTICIPANTS_PANEL_BUTTON;
|
||||
var ACTIVITIES_RECORDING_ACTIVITY;
|
||||
var ACTIVITIES_BROADCASTING_ACTIVITY;
|
||||
var TOOLBAR_SETTINGS_BUTTON;
|
||||
var CAPTIONS_BUTTON;
|
||||
|
||||
var ROOM_NAME;
|
||||
var FAKE_DEVICES;
|
||||
var FAKE_RECORDINGS;
|
||||
|
||||
var PARTICIPANT_NAME;
|
||||
|
||||
var OPENVIDU_CALL_SERVER_URL;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
var url = new URL(window.location.href);
|
||||
|
||||
OPENVIDU_CALL_SERVER_URL = url.searchParams.get('OPENVIDU_CALL_SERVER_URL') || 'http://localhost:6080';
|
||||
|
||||
FAKE_DEVICES = url.searchParams.get('fakeDevices') === null ? false : url.searchParams.get('fakeDevices') === 'true';
|
||||
|
||||
FAKE_RECORDINGS = url.searchParams.get('fakeRecordings') === null ? false : url.searchParams.get('fakeRecordings') === 'true';
|
||||
|
||||
// Directives
|
||||
MINIMAL = url.searchParams.get('minimal') === null ? false : url.searchParams.get('minimal') === 'true';
|
||||
LANG = url.searchParams.get('lang') || 'en';
|
||||
CUSTOM_LANG_OPTIONS = url.searchParams.get('langOptions') === null ? false : url.searchParams.get('langOptions') === 'true';
|
||||
// CAPTIONS_LANG = url.searchParams.get('captionsLang') || 'en-US';
|
||||
// CUSTOM_CAPTIONS_LANG_OPTIONS = url.searchParams.get('captionsLangOptions') === null ? false : url.searchParams.get('captionsLangOptions') === 'true';
|
||||
PARTICIPANT_NAME =
|
||||
url.searchParams.get('participantName') === null
|
||||
? 'TEST_USER' + Math.random().toString(36).substr(2, 9)
|
||||
: url.searchParams.get('participantName');
|
||||
PREJOIN = url.searchParams.get('prejoin') === null ? true : url.searchParams.get('prejoin') === 'true';
|
||||
VIDEO_ENABLED = url.searchParams.get('videoEnabled') === null ? true : url.searchParams.get('videoEnabled') === 'true';
|
||||
AUDIO_ENABLED = url.searchParams.get('audioEnabled') === null ? true : url.searchParams.get('audioEnabled') === 'true';
|
||||
SCREENSHARE_BUTTON = url.searchParams.get('screenshareBtn') === null ? true : url.searchParams.get('screenshareBtn') === 'true';
|
||||
RECORDING_BUTTON =
|
||||
url.searchParams.get('toolbarRecordingButton') === null ? true : url.searchParams.get('toolbarRecordingButton') === 'true';
|
||||
FULLSCREEN_BUTTON = url.searchParams.get('fullscreenBtn') === null ? true : url.searchParams.get('fullscreenBtn') === 'true';
|
||||
BROADCASTING_BUTTON =
|
||||
url.searchParams.get('toolbarBroadcastingButton') === null ? true : url.searchParams.get('toolbarBroadcastingButton') === 'true';
|
||||
|
||||
TOOLBAR_SETTINGS_BUTTON =
|
||||
url.searchParams.get('toolbarSettingsBtn') === null ? true : url.searchParams.get('toolbarSettingsBtn') === 'true';
|
||||
CAPTIONS_BUTTON = url.searchParams.get('toolbarCaptionsBtn') === null ? true : url.searchParams.get('toolbarCaptionsBtn') === 'true';
|
||||
|
||||
LEAVE_BUTTON = url.searchParams.get('leaveBtn') === null ? true : url.searchParams.get('leaveBtn') === 'true';
|
||||
ACTIVITIES_PANEL_BUTTON =
|
||||
url.searchParams.get('activitiesPanelBtn') === null ? true : url.searchParams.get('activitiesPanelBtn') === 'true';
|
||||
CHAT_PANEL_BUTTON = url.searchParams.get('chatPanelBtn') === null ? true : url.searchParams.get('chatPanelBtn') === 'true';
|
||||
PARTICIPANTS_PANEL_BUTTON =
|
||||
url.searchParams.get('participantsPanelBtn') === null ? true : url.searchParams.get('participantsPanelBtn') === 'true';
|
||||
ACTIVITIES_BROADCASTING_ACTIVITY =
|
||||
url.searchParams.get('activitiesPanelBroadcastingActivity') === null
|
||||
? true
|
||||
: url.searchParams.get('activitiesPanelBroadcastingActivity') === 'true';
|
||||
ACTIVITIES_RECORDING_ACTIVITY =
|
||||
url.searchParams.get('activitiesPanelRecordingActivity') === null
|
||||
? true
|
||||
: url.searchParams.get('activitiesPanelRecordingActivity') === 'true';
|
||||
|
||||
DISPLAY_LOGO = url.searchParams.get('displayLogo') === null ? true : url.searchParams.get('displayLogo') === 'true';
|
||||
DISPLAY_ROOM_NAME = url.searchParams.get('displayRoomName') === null ? true : url.searchParams.get('displayRoomName') === 'true';
|
||||
DISPLAY_PARTICIPANT_NAME =
|
||||
url.searchParams.get('displayParticipantName') === null ? true : url.searchParams.get('displayParticipantName') === 'true';
|
||||
DISPLAY_AUDIO_DETECTION =
|
||||
url.searchParams.get('displayAudioDetection') === null ? true : url.searchParams.get('displayAudioDetection') === 'true';
|
||||
VIDEO_CONTROLS = url.searchParams.get('videoControls') === null ? true : url.searchParams.get('videoControls') === 'true';
|
||||
PARTICIPANT_MUTE_BUTTON =
|
||||
url.searchParams.get('participantMuteBtn') === null ? true : url.searchParams.get('participantMuteBtn') === 'true';
|
||||
|
||||
ROOM_NAME = url.searchParams.get('roomName') === null ? `E2ESession${Math.floor(Date.now())}` : url.searchParams.get('roomName');
|
||||
|
||||
var webComponent = document.querySelector('openvidu-webcomponent');
|
||||
|
||||
webComponent.addEventListener('onTokenRequested', (event) => {
|
||||
appendElement('onTokenRequested');
|
||||
console.log('Token ready', event.detail);
|
||||
joinSession(ROOM_NAME, event.detail);
|
||||
});
|
||||
webComponent.addEventListener('onReadyToJoin', (event) => appendElement('onReadyToJoin'));
|
||||
webComponent.addEventListener('onRoomDisconnected', (event) => appendElement('onRoomDisconnected'));
|
||||
webComponent.addEventListener('onVideoEnabledChanged', (event) => appendElement('onVideoEnabledChanged-' + event.detail));
|
||||
webComponent.addEventListener('onVideoDeviceChanged', (event) => appendElement('onVideoDeviceChanged'));
|
||||
webComponent.addEventListener('onAudioEnabledChanged', (eSESSIONvent) => appendElement('onAudioEnabledChanged-' + event.detail));
|
||||
webComponent.addEventListener('onAudioDeviceChanged', (event) => appendElement('onAudioDeviceChanged'));
|
||||
webComponent.addEventListener('onScreenShareEnabledChanged', (event) => appendElement('onScreenShareEnabledChanged'));
|
||||
webComponent.addEventListener('onParticipantsPanelStatusChanged', (event) =>
|
||||
appendElement('onParticipantsPanelStatusChanged-' + event.detail.isOpened)
|
||||
);
|
||||
webComponent.addEventListener('onLangChanged', (event) => appendElement('onLangChanged-' + event.detail.lang));
|
||||
webComponent.addEventListener('onChatPanelStatusChanged', (event) =>
|
||||
appendElement('onChatPanelStatusChanged-' + event.detail.isOpened)
|
||||
);
|
||||
webComponent.addEventListener('onActivitiesPanelStatusChanged', (event) =>
|
||||
appendElement('onActivitiesPanelStatusChanged-' + event.detail.isOpened)
|
||||
);
|
||||
webComponent.addEventListener('onSettingsPanelStatusChanged', (event) =>
|
||||
appendElement('onSettingsPanelStatusChanged-' + event.detail.isOpened)
|
||||
);
|
||||
webComponent.addEventListener('onFullscreenEnabledChanged', (event) => appendElement('onFullscreenEnabledChanged-' + event.detail));
|
||||
|
||||
webComponent.addEventListener('onRecordingStartRequested', async (event) => {
|
||||
appendElement('onRecordingStartRequested-' + event.detail.roomName);
|
||||
// Can't test the recording
|
||||
// RECORDING_ID = await startRecording(SESSION_NAME);
|
||||
});
|
||||
// Can't test the recording
|
||||
// webComponent.addEventListener('onRecordingStopRequested', async (event) => {
|
||||
// appendElement('onRecordingStopRequested-' + event.detail.roomName);
|
||||
// await stopRecording(RECORDING_ID);
|
||||
// });
|
||||
|
||||
webComponent.addEventListener('onRecordingStopRequested', async (event) => {
|
||||
appendElement('onRecordingStopRequested-' + event.detail.roomName);
|
||||
});
|
||||
|
||||
// Can't test the recording
|
||||
// webComponent.addEventListener('onActivitiesPanelStopRecordingClicked', async (event) => {
|
||||
// appendElement('onActivitiesPanelStopRecordingClicked');
|
||||
// await stopRecording(RECORDING_ID);
|
||||
// });
|
||||
|
||||
webComponent.addEventListener('onRecordingDeleteRequested', (event) => {
|
||||
const { roomName, recordingId } = event.detail;
|
||||
appendElement(`onRecordingDeleteRequested-${roomName}-${recordingId}`);
|
||||
});
|
||||
|
||||
webComponent.addEventListener('onBroadcastingStartRequested', async (event) => {
|
||||
const { roomName, broadcastUrl } = event.detail;
|
||||
appendElement(`onBroadcastingStartRequested-${roomName}-${broadcastUrl}`);
|
||||
});
|
||||
|
||||
webComponent.addEventListener('onActivitiesPanelStopBroadcastingClicked', async (event) => {
|
||||
appendElement('onActivitiesPanelStopBroadcastingClicked');
|
||||
});
|
||||
|
||||
webComponent.addEventListener('onRoomCreated', (event) => {
|
||||
var room = event.detail;
|
||||
appendElement('onRoomCreated');
|
||||
|
||||
room.on('disconnected', (e) => {
|
||||
appendElement('roomDisconnected');
|
||||
});
|
||||
});
|
||||
|
||||
webComponent.addEventListener('onParticipantCreated', (event) => {
|
||||
var participant = event.detail;
|
||||
appendElement(`${participant.name}-onParticipantCreated`);
|
||||
});
|
||||
|
||||
setWebcomponentAttributes();
|
||||
});
|
||||
|
||||
function setWebcomponentAttributes() {
|
||||
var webComponent = document.querySelector('openvidu-webcomponent');
|
||||
webComponent.participantName = PARTICIPANT_NAME;
|
||||
|
||||
webComponent.minimal = MINIMAL;
|
||||
webComponent.lang = LANG;
|
||||
if (CUSTOM_LANG_OPTIONS) {
|
||||
webComponent.langOptions = [
|
||||
{ name: 'Esp', lang: 'es' },
|
||||
{ name: 'Eng', lang: 'en' }
|
||||
];
|
||||
}
|
||||
// TODO: Uncomment when the captions are implemented
|
||||
// webComponent.captionsLang = CAPTIONS_LANG;
|
||||
// if (CUSTOM_CAPTIONS_LANG_OPTIONS) {
|
||||
// webComponent.captionsLangOptions = [
|
||||
// { name: 'Esp', lang: 'es-ES' },
|
||||
// { name: 'Eng', lang: 'en-US' }
|
||||
// ];
|
||||
// }
|
||||
if (FAKE_DEVICES) {
|
||||
console.warn('Using fake devices');
|
||||
monkeyPatchMediaDevices();
|
||||
}
|
||||
if (FAKE_RECORDINGS) {
|
||||
console.warn('Using fake recordings');
|
||||
webComponent.recordingActivityRecordingsList = [{ status: 'ready', filename: 'fakeRecording' }];
|
||||
}
|
||||
|
||||
webComponent.prejoin = PREJOIN;
|
||||
webComponent.videoEnabled = VIDEO_ENABLED;
|
||||
webComponent.audioEnabled = AUDIO_ENABLED;
|
||||
webComponent.toolbarScreenshareButton = SCREENSHARE_BUTTON;
|
||||
|
||||
webComponent.toolbarFullscreenButton = FULLSCREEN_BUTTON;
|
||||
webComponent.toolbarSettingsButton = TOOLBAR_SETTINGS_BUTTON;
|
||||
// webComponent.toolbarCaptionsButton = CAPTIONS_BUTTON;
|
||||
webComponent.toolbarLeaveButton = LEAVE_BUTTON;
|
||||
webComponent.toolbarRecordingButton = RECORDING_BUTTON;
|
||||
webComponent.toolbarBroadcastingButton = BROADCASTING_BUTTON;
|
||||
webComponent.toolbarActivitiesPanelButton = ACTIVITIES_PANEL_BUTTON;
|
||||
webComponent.toolbarChatPanelButton = CHAT_PANEL_BUTTON;
|
||||
webComponent.toolbarParticipantsPanelButton = PARTICIPANTS_PANEL_BUTTON;
|
||||
webComponent.toolbarDisplayLogo = DISPLAY_LOGO;
|
||||
webComponent.toolbarDisplayRoomName = DISPLAY_ROOM_NAME;
|
||||
webComponent.streamDisplayParticipantName = DISPLAY_PARTICIPANT_NAME;
|
||||
webComponent.streamDisplayAudioDetection = DISPLAY_AUDIO_DETECTION;
|
||||
webComponent.streamVideoControls = VIDEO_CONTROLS;
|
||||
webComponent.participantPanelItemMuteButton = PARTICIPANT_MUTE_BUTTON;
|
||||
|
||||
webComponent.activitiesPanelRecordingActivity = ACTIVITIES_RECORDING_ACTIVITY;
|
||||
webComponent.activitiesPanelBroadcastingActivity = ACTIVITIES_BROADCASTING_ACTIVITY;
|
||||
}
|
||||
|
||||
function appendElement(id) {
|
||||
var eventsDiv = document.getElementById('events');
|
||||
eventsDiv.setAttribute('style', 'position: absolute;');
|
||||
var element = document.createElement('div');
|
||||
element.setAttribute('id', id);
|
||||
element.setAttribute('style', 'height: 1px;');
|
||||
eventsDiv.appendChild(element);
|
||||
}
|
||||
|
||||
async function joinSession(roomName, participantName) {
|
||||
var webComponent = document.querySelector('openvidu-webcomponent');
|
||||
console.log('Joining session', roomName, participantName);
|
||||
try {
|
||||
webComponent.token = await getToken(roomName, participantName);
|
||||
} catch (error) {
|
||||
webComponent.tokenError = error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getToken(roomName, participantName) {
|
||||
try {
|
||||
const response = await fetch(OPENVIDU_CALL_SERVER_URL + '/call/api/rooms', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
// 'Authorization': 'Basic ' + btoa('OPENVIDUAPP:' + OPENVIDU_SECRET),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
participantName,
|
||||
roomName
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch token');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.token;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>openvidu-web-component</title>
|
||||
<script type="module" src="utils/filter-stream.js"></script>
|
||||
<!-- <script type="module" src="utils/shader-renderer.js"></script> -->
|
||||
<script type="module" src="utils/media-devices.js"></script>
|
||||
<script type="module" src="app.js"></script>
|
||||
<script src="openvidu-webcomponent-dev.js"></script>
|
||||
<link rel="stylesheet" href="openvidu-webcomponent-dev.css" />
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--ov-background-color: #303030;
|
||||
--ov-secondary-action-color: #3e3f3f;
|
||||
--ov-accent-action-color: #598eff;
|
||||
--ov-error-color: #eb5144;
|
||||
--ov-accent-action-color: #ffae35;
|
||||
--ov-light-color: #e6e6e6;
|
||||
|
||||
--ov-secondary-action-color: #3a3d3d;
|
||||
--ov-text-primary-color: #ffffff;
|
||||
|
||||
--ov-text-primary-color: #1d1d1d;
|
||||
--ov-surface-color: #ffffff;
|
||||
|
||||
--ov-toolbar-buttons-radius: 50%;
|
||||
--ov-leave-button-radius: 10px;
|
||||
--ov-video-radius: 5px;
|
||||
--ov-surface-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="events"></div>
|
||||
<!-- OpenVidu Web Component -->
|
||||
<openvidu-webcomponent></openvidu-webcomponent>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,310 +0,0 @@
|
||||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { OPENVIDU_CALL_SERVER } from '../config';
|
||||
import { WebComponentConfig } from '../selenium.conf';
|
||||
import { OpenViduComponentsPO } from '../utils.po.test';
|
||||
|
||||
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
|
||||
|
||||
describe('Testing screenshare features', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(WebComponentConfig.browserName)
|
||||
.withCapabilities(WebComponentConfig.browserCapabilities)
|
||||
.setChromeOptions(WebComponentConfig.browserOptions)
|
||||
.usingServer(WebComponentConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should toggle screensharing twice', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Clicking to screensharing button
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
// expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
|
||||
|
||||
await utils.disableScreenShare();
|
||||
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
|
||||
// toggle screenshare again
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
await utils.disableScreenShare();
|
||||
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should show screen and muted camera', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
await utils.waitForElement('#camera-btn');
|
||||
await utils.clickOn('#camera-btn');
|
||||
|
||||
// Clicking to screensharing button
|
||||
const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
expect(await screenshareButton.isDisplayed()).toBeTrue();
|
||||
await screenshareButton.click();
|
||||
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
await utils.disableScreenShare();
|
||||
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should screensharing with PINNED video', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Clicking to screensharing button
|
||||
const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
expect(await screenshareButton.isDisplayed()).toBeTrue();
|
||||
await screenshareButton.click();
|
||||
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should screensharing with PINNED video and replace the existing one', async () => {
|
||||
const roomName = 'screensharingE2E';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Clicking to screensharing button
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Starting new browser for adding the second participant
|
||||
const newTabScript = `window.open("${fixedUrl}")`;
|
||||
await browser.executeScript(newTabScript);
|
||||
const tabs = await browser.getAllWindowHandles();
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Clicking to screensharing button
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Go to first tab
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should disabled a screensharing and pinned the previous one', async () => {
|
||||
const roomName = 'screensharingtwoE2E';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Clicking to screensharing button
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Starting new browser for adding the second participant
|
||||
const tabs = await utils.openTab(fixedUrl);
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
// Clicking to screensharing button
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
// Disable screensharing
|
||||
await utils.disableScreenShare();
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
|
||||
// Go to first tab
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3);
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
|
||||
});
|
||||
|
||||
// it('should screensharing with audio muted', async () => {
|
||||
// let isAudioEnabled;
|
||||
// const getAudioScript = (className: string) => {
|
||||
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
|
||||
// };
|
||||
// await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
// await utils.checkLayoutPresent();
|
||||
|
||||
// const micButton = await utils.waitForElement('#mic-btn');
|
||||
// await micButton.click();
|
||||
|
||||
// // Clicking to screensharing button
|
||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
|
||||
// await screenshareButton.click();
|
||||
|
||||
// await utils.waitForElement('.screen-type');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
|
||||
// expect(isAudioEnabled).toBeFalse();
|
||||
|
||||
// await utils.waitForElement('#status-mic');
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(2);
|
||||
|
||||
// // Clicking to screensharing button
|
||||
// await screenshareButton.click();
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
|
||||
// });
|
||||
|
||||
// it('should show and hide CAMERA stream when muting video with screensharing', async () => {
|
||||
// await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
// await utils.checkLayoutPresent();
|
||||
|
||||
// // Clicking to screensharing button
|
||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
// expect(await screenshareButton.isDisplayed()).toBeTrue();
|
||||
// await screenshareButton.click();
|
||||
|
||||
// await utils.waitForElement('.OV_big');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
|
||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
||||
// await muteVideoButton.click();
|
||||
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
// });
|
||||
|
||||
// it('should screenshare has audio active when camera is muted', async () => {
|
||||
// let isAudioEnabled;
|
||||
// const audioEnableScript = 'return document.getElementsByTagName("video")[0].srcObject.getAudioTracks()[0].enabled;';
|
||||
|
||||
// await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
// await utils.checkLayoutPresent();
|
||||
|
||||
// // Clicking to screensharing button
|
||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
|
||||
// await screenshareButton.click();
|
||||
|
||||
// await utils.waitForElement('.OV_big');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
|
||||
// // Muting camera video
|
||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
||||
// await muteVideoButton.click();
|
||||
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
|
||||
// await browser.sleep(500);
|
||||
// expect(await utils.isPresent('#status-mic')).toBeFalse();
|
||||
|
||||
// // Checking if audio is muted after join the room
|
||||
// isAudioEnabled = await browser.executeScript(audioEnableScript);
|
||||
// expect(isAudioEnabled).toBeTrue();
|
||||
|
||||
// // Unmuting camera
|
||||
// await muteVideoButton.click();
|
||||
// await browser.sleep(1000);
|
||||
|
||||
// await utils.waitForElement('.camera-type');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
// });
|
||||
|
||||
// it('should camera come back with audio muted when screensharing', async () => {
|
||||
// let element, isAudioEnabled;
|
||||
|
||||
// const getAudioScript = (className: string) => {
|
||||
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
|
||||
// };
|
||||
|
||||
// await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
// await utils.checkLayoutPresent();
|
||||
|
||||
// // Clicking to screensharing button
|
||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
// await screenshareButton.click();
|
||||
|
||||
// await utils.waitForElement('.screen-type');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
|
||||
// // Mute camera
|
||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
||||
// await muteVideoButton.click();
|
||||
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
// expect(await utils.isPresent('#status-mic')).toBeFalse();
|
||||
|
||||
// // Checking if audio is muted after join the room
|
||||
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
|
||||
// expect(isAudioEnabled).toBeTrue();
|
||||
|
||||
// // Mute audio
|
||||
// const muteAudioButton = await utils.waitForElement('#mic-btn');
|
||||
// await muteAudioButton.click();
|
||||
|
||||
// await utils.waitForElement('#status-mic');
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
|
||||
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
|
||||
// expect(isAudioEnabled).toBeFalse();
|
||||
|
||||
// // Unmute camera
|
||||
// await muteVideoButton.click();
|
||||
|
||||
// await utils.waitForElement('.camera-type');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(2);
|
||||
|
||||
// isAudioEnabled = await browser.executeScript(getAudioScript('camera-type'));
|
||||
// expect(isAudioEnabled).toBeFalse();
|
||||
// });
|
||||
});
|
||||
@ -1,60 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
const concat = require('concat');
|
||||
const VERSION = require('./package.json').version;
|
||||
const ovWebcomponentRCPath = './dist/openvidu-webcomponent-rc';
|
||||
const ovWebcomponentProdPath = './dist/openvidu-webcomponent';
|
||||
|
||||
module.exports.buildWebcomponent = async () => {
|
||||
console.log('Building OpenVidu Web Component (' + VERSION + ')');
|
||||
const tutorialWcPath = '../../openvidu-tutorials/openvidu-webcomponent/web';
|
||||
const e2eWcPath = './e2e/webcomponent-app';
|
||||
|
||||
|
||||
try {
|
||||
await buildElement();
|
||||
await copyFiles(tutorialWcPath);
|
||||
await copyFiles(e2eWcPath);
|
||||
await renameWebComponentTestName(e2eWcPath);
|
||||
|
||||
console.log(`OpenVidu Web Component (${VERSION}) built`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
async function buildElement() {
|
||||
const files = [`${ovWebcomponentRCPath}/runtime.js`, `${ovWebcomponentRCPath}/main.js`, `${ovWebcomponentRCPath}/polyfills.js`];
|
||||
|
||||
try {
|
||||
await fs.ensureDir('./dist/openvidu-webcomponent');
|
||||
await concat(files, `${ovWebcomponentProdPath}/openvidu-webcomponent-${VERSION}.js`);
|
||||
await fs.copy(`${ovWebcomponentRCPath}/styles.css`, `${ovWebcomponentProdPath}/openvidu-webcomponent-${VERSION}.css`);
|
||||
// await fs.copy(
|
||||
// "./dist/openvidu-webcomponent/assets",
|
||||
// "./openvidu-webcomponent/assets"
|
||||
// );
|
||||
} catch (err) {
|
||||
console.error('Error executing build function in webcomponent-builds.js');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function renameWebComponentTestName(dir) {
|
||||
fs.renameSync(`${dir}/openvidu-webcomponent-${VERSION}.js`, `${dir}/openvidu-webcomponent-dev.js`);
|
||||
fs.renameSync(`${dir}/openvidu-webcomponent-${VERSION}.css`, `${dir}/openvidu-webcomponent-dev.css`);
|
||||
}
|
||||
|
||||
async function copyFiles(destination) {
|
||||
if (fs.existsSync(destination)) {
|
||||
try {
|
||||
console.log(`Copying openvidu-webcomponent files from: ${ovWebcomponentProdPath} to: ${destination}`);
|
||||
await fs.ensureDir(ovWebcomponentProdPath);
|
||||
await fs.copy(ovWebcomponentProdPath, destination);
|
||||
} catch (err) {
|
||||
console.error('Error executing copy function in webcomponent-builds.js');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.buildWebcomponent();
|
||||
42620
openvidu-components-angular/package-lock.json
generated
42620
openvidu-components-angular/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,110 +1,110 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@angular/animations": "18.2.5",
|
||||
"@angular/cdk": "18.2.5",
|
||||
"@angular/common": "18.2.5",
|
||||
"@angular/core": "18.2.5",
|
||||
"@angular/forms": "18.2.5",
|
||||
"@angular/material": "18.2.5",
|
||||
"@angular/platform-browser": "18.2.5",
|
||||
"@angular/platform-browser-dynamic": "18.2.5",
|
||||
"@angular/router": "18.2.5",
|
||||
"@livekit/track-processors": "0.3.2",
|
||||
"autolinker": "4.0.0",
|
||||
"livekit-client": "2.5.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.7.0",
|
||||
"zone.js": "^0.14.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "18.2.5",
|
||||
"@angular/cli": "18.2.5",
|
||||
"@angular/compiler": "18.2.5",
|
||||
"@angular/compiler-cli": "18.2.5",
|
||||
"@angular/elements": "18.2.5",
|
||||
"@compodoc/compodoc": "^1.1.25",
|
||||
"@types/dom-mediacapture-transform": "0.1.9",
|
||||
"@types/dom-webcodecs": "0.1.11",
|
||||
"@types/jasmine": "^5.1.4",
|
||||
"@types/node": "20.12.14",
|
||||
"@types/selenium-webdriver": "4.1.16",
|
||||
"@types/ws": "^8.5.12",
|
||||
"chromedriver": "129.0.0",
|
||||
"concat": "^1.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "^9.1.6",
|
||||
"jasmine": "^5.3.1",
|
||||
"jasmine-core": "5.3.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"karma": "^6.4.4",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage": "^2.2.1",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-notify-reporter": "1.3.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"ng-packagr": "18.2.1",
|
||||
"npm-watch": "^0.13.0",
|
||||
"prettier": "3.3.3",
|
||||
"selenium-webdriver": "4.25.0",
|
||||
"ts-node": "10.9.2",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "5.4.5",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"name": "openvidu-components-testapp",
|
||||
"private": true,
|
||||
"watch": {
|
||||
"doc:serve": {
|
||||
"patterns": [
|
||||
"projects",
|
||||
"src"
|
||||
],
|
||||
"extensions": "ts,html,scss,css,md",
|
||||
"quiet": false
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ng serve --configuration development --open",
|
||||
"start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200",
|
||||
"start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080",
|
||||
"build": "ng build openvidu-components-testapp --configuration production",
|
||||
"bundle-report": "ng build openvidu-webcomponent --stats-json --configuration production && webpack-bundle-analyzer dist/openvidu-webcomponent/stats.json",
|
||||
"doc:build": "npx compodoc -c ./projects/openvidu-components-angular/doc/.compodocrc.json",
|
||||
"doc:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js",
|
||||
"doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js",
|
||||
"doc:clean-copy": "rm -rf ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular && cp -r ./docs/openvidu-components-angular/ ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular",
|
||||
"doc:serve": "npx compodoc -c ../openvidu-components-angular/projects/openvidu-components-angular/doc/.compodocrc.json --serve --port 7000",
|
||||
"doc:serve-watch": "npm-watch doc:serve",
|
||||
"lib:serve": "ng build openvidu-components-angular --watch",
|
||||
"lib:build": "ng build openvidu-components-angular --configuration production && cd ./dist/openvidu-components-angular",
|
||||
"lib:pack": "cd ./dist/openvidu-components-angular && npm pack",
|
||||
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/openvidu-call-front",
|
||||
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
|
||||
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
|
||||
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
|
||||
"e2e:nested-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/directives.test.js",
|
||||
"e2e:webcomponent-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/**/*.test.js",
|
||||
"e2e:webcomponent-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/api-directives.test.js",
|
||||
"e2e:webcomponent-captions": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/captions.test.js",
|
||||
"e2e:webcomponent-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/chat.test.js",
|
||||
"e2e:webcomponent-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/events.test.js",
|
||||
"e2e:webcomponent-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/media-devices.test.js",
|
||||
"e2e:webcomponent-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/panels.test.js",
|
||||
"e2e:webcomponent-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/screensharing.test.js",
|
||||
"e2e:webcomponent-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/stream.test.js",
|
||||
"e2e:webcomponent-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/toolbar.test.js",
|
||||
"webcomponent:testing-build": "./node_modules/@angular/cli/bin/ng.js build openvidu-webcomponent --configuration testing && node ./openvidu-webcomponent-build.js",
|
||||
"webcomponent:build": "./node_modules/@angular/cli/bin/ng.js build openvidu-webcomponent --configuration production && node ./openvidu-webcomponent-build.js",
|
||||
"webcomponent:serve-testapp": "npx http-server ./e2e/webcomponent-app/",
|
||||
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
|
||||
"husky": "cd .. && husky install"
|
||||
},
|
||||
"version": "3.0.1-beta1"
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.15",
|
||||
"@angular/cdk": "20.2.14",
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/forms": "20.3.15",
|
||||
"@angular/material": "20.2.14",
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@livekit/track-processors": "0.7.2",
|
||||
"@types/dom-mediacapture-transform": "0.1.11",
|
||||
"autolinker": "4.1.5",
|
||||
"livekit-client": "2.16.1",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.8.1",
|
||||
"zone.js": "0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "20.3.13",
|
||||
"@angular/cli": "20.3.13",
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/compiler-cli": "20.3.15",
|
||||
"@compodoc/compodoc": "1.1.32",
|
||||
"@types/jasmine": "5.1.13",
|
||||
"@types/node": "22.19.3",
|
||||
"@types/pngjs": "6.0.5",
|
||||
"@types/selenium-webdriver": "4.35.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"chromedriver": "143.0.1",
|
||||
"concat": "1.0.3",
|
||||
"cpx": "1.5.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-config-prettier": "9.1.2",
|
||||
"eslint-plugin-prettier": "5.2.6",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "9.1.7",
|
||||
"jasmine": "5.13.0",
|
||||
"jasmine-core": "5.13.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"karma": "6.4.4",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage": "2.2.1",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-notify-reporter": "1.3.0",
|
||||
"lint-staged": "15.5.2",
|
||||
"ng-packagr": "20.3.2",
|
||||
"npm-watch": "0.13.0",
|
||||
"pixelmatch": "7.1.0",
|
||||
"pngjs": "7.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"rimraf": "6.1.2",
|
||||
"selenium-webdriver": "4.39.0",
|
||||
"ts-node": "10.9.2",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "5.8.3",
|
||||
"webpack-bundle-analyzer": "4.10.2"
|
||||
},
|
||||
"name": "openvidu-components-testapp",
|
||||
"private": true,
|
||||
"watch": {
|
||||
"doc:serve": {
|
||||
"patterns": [
|
||||
"projects",
|
||||
"src"
|
||||
],
|
||||
"extensions": "ts,html,scss,css,md",
|
||||
"quiet": false
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ng serve --configuration development --open",
|
||||
"start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200",
|
||||
"start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080",
|
||||
"build": "ng build openvidu-components-testapp --configuration production",
|
||||
"doc:build": "npx compodoc -c ./projects/openvidu-components-angular/doc/.compodocrc.json",
|
||||
"doc:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js",
|
||||
"doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js",
|
||||
"doc:clean-copy": "rm -rf ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular && cp -r ./docs/openvidu-components-angular/ ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular",
|
||||
"doc:serve": "npx compodoc -c ../openvidu-components-angular/projects/openvidu-components-angular/doc/.compodocrc.json --serve --port 7000",
|
||||
"doc:serve-watch": "npm-watch doc:serve",
|
||||
"lib:serve": "ng build openvidu-components-angular --watch",
|
||||
"lib:build": "ng build openvidu-components-angular --configuration production && rimraf dist/openvidu-components-angular && cpx \"projects/openvidu-components-angular/dist/**/*\" dist/openvidu-components-angular",
|
||||
"lib:pack": "cd ./dist/openvidu-components-angular && npm pack",
|
||||
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/frontend",
|
||||
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
|
||||
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
|
||||
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
|
||||
"e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js",
|
||||
"e2e:nested-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js",
|
||||
"e2e:lib-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/api-directives.test.js",
|
||||
"e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
|
||||
"e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
|
||||
"e2e:lib-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
|
||||
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
|
||||
"e2e:lib-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/panels.test.js",
|
||||
"e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
|
||||
"e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
|
||||
"e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
|
||||
"e2e:lib-virtual-backgrounds": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/virtual-backgrounds.test.js",
|
||||
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
|
||||
"husky": "cd .. && husky install"
|
||||
},
|
||||
"version": "3.6.0"
|
||||
}
|
||||
|
||||
@ -3,168 +3,334 @@ const glob = require('glob');
|
||||
|
||||
const startApiLine = '<!-- start-dynamic-api-directives-content -->';
|
||||
const apiDirectivesTable =
|
||||
'| **Parameter** | **Type** | **Reference** | \n' +
|
||||
'|:--------------------------------: | :-------: | :---------------------------------------------: |';
|
||||
'| **Parameter** | **Type** | **Reference** | \n' +
|
||||
'|:--------------------------------: | :-------: | :---------------------------------------------: |';
|
||||
const endApiLine = '<!-- end-dynamic-api-directives-content -->';
|
||||
|
||||
/**
|
||||
* Get all directive files from the API directives directory
|
||||
*/
|
||||
function getDirectiveFiles() {
|
||||
// Directory where directive files are located
|
||||
const directivesDir = 'projects/openvidu-components-angular/src/lib/directives/api';
|
||||
return listFiles(directivesDir, '.directive.ts');
|
||||
const directivesDir = 'projects/openvidu-components-angular/src/lib/directives/api';
|
||||
return listFiles(directivesDir, '.directive.ts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all component files
|
||||
*/
|
||||
function getComponentFiles() {
|
||||
// Directory where component files are located
|
||||
const componentsDir = 'projects/openvidu-components-angular/src/lib/components';
|
||||
return listFiles(componentsDir, '.component.ts');
|
||||
const componentsDir = 'projects/openvidu-components-angular/src/lib/components';
|
||||
return listFiles(componentsDir, '.component.ts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all admin files
|
||||
*/
|
||||
function getAdminFiles() {
|
||||
// Directory where component files are located
|
||||
const componentsDir = 'projects/openvidu-components-angular/src/lib/admin';
|
||||
return listFiles(componentsDir, '.component.ts');
|
||||
const componentsDir = 'projects/openvidu-components-angular/src/lib/admin';
|
||||
return listFiles(componentsDir, '.component.ts');
|
||||
}
|
||||
|
||||
/**
|
||||
* List all files with specific extension in directory
|
||||
*/
|
||||
function listFiles(directoryPath, fileExtension) {
|
||||
const files = glob.sync(`${directoryPath}/**/*${fileExtension}`);
|
||||
if (files.length === 0) {
|
||||
throw new Error(`No ${fileExtension} files found in ${directoryPath}`);
|
||||
}
|
||||
return files;
|
||||
const files = glob.sync(`${directoryPath}/**/*${fileExtension}`);
|
||||
if (files.length === 0) {
|
||||
throw new Error(`No ${fileExtension} files found in ${directoryPath}`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract component selector from component file
|
||||
*/
|
||||
function getComponentSelector(componentFile) {
|
||||
const componentContent = fs.readFileSync(componentFile, 'utf8');
|
||||
const selectorMatch = componentContent.match(/@Component\({[^]*?selector:\s*['"]([^'"]+)['"][^]*?}\)/s);
|
||||
|
||||
if (!selectorMatch) {
|
||||
throw new Error(`Unable to find selector in component file: ${componentFile}`);
|
||||
}
|
||||
|
||||
return selectorMatch[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directive class has @internal annotation
|
||||
*/
|
||||
function isInternalDirective(directiveContent, className) {
|
||||
const classRegex = new RegExp(`(/\\*\\*[\\s\\S]*?\\*/)?\\s*@Directive\\([\\s\\S]*?\\)\\s*export\\s+class\\s+${escapeRegex(className)}`, 'g');
|
||||
const match = classRegex.exec(directiveContent);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1].includes('@internal');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract attribute name from selector for a specific component
|
||||
*/
|
||||
function extractAttributeForComponent(selector, componentSelector) {
|
||||
// Split selector by comma and trim whitespace
|
||||
const selectorParts = selector.split(',').map(part => part.trim());
|
||||
|
||||
// Find the part that matches our component
|
||||
for (const part of selectorParts) {
|
||||
if (part.includes(componentSelector)) {
|
||||
// Extract attribute from this specific part
|
||||
const attributeMatch = part.match(/\[([^\]]+)\]/);
|
||||
if (attributeMatch) {
|
||||
return attributeMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no specific match, return the first attribute found
|
||||
const fallbackMatch = selector.match(/\[([^\]]+)\]/);
|
||||
return fallbackMatch ? fallbackMatch[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all directive classes from a directive file
|
||||
*/
|
||||
function extractDirectiveClasses(directiveContent) {
|
||||
const classes = [];
|
||||
|
||||
// Regex to find all directive class definitions with their preceding @Directive decorators
|
||||
const directiveClassRegex = /@Directive\(\s*{\s*selector:\s*['"]([^'"]+)['"][^}]*}\s*\)\s*export\s+class\s+(\w+)/gs;
|
||||
|
||||
let match;
|
||||
while ((match = directiveClassRegex.exec(directiveContent)) !== null) {
|
||||
const selector = match[1];
|
||||
const className = match[2];
|
||||
|
||||
// Skip internal directives
|
||||
if (isInternalDirective(directiveContent, className)) {
|
||||
console.log(`Skipping internal directive: ${className}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
classes.push({
|
||||
selector,
|
||||
className
|
||||
});
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all directives from a directive file that match a component selector
|
||||
*/
|
||||
function extractDirectivesForComponent(directiveFile, componentSelector) {
|
||||
const directiveContent = fs.readFileSync(directiveFile, 'utf8');
|
||||
const directives = [];
|
||||
|
||||
// Get all directive classes in the file (excluding internal ones)
|
||||
const directiveClasses = extractDirectiveClasses(directiveContent);
|
||||
|
||||
// Filter classes that match the component selector
|
||||
const matchingClasses = directiveClasses.filter(directiveClass =>
|
||||
directiveClass.selector.includes(componentSelector)
|
||||
);
|
||||
|
||||
// For each matching class, extract input type information
|
||||
matchingClasses.forEach(directiveClass => {
|
||||
// Extract the correct attribute name for this component
|
||||
const attributeName = extractAttributeForComponent(directiveClass.selector, componentSelector);
|
||||
|
||||
if (attributeName) {
|
||||
const inputInfo = extractInputInfo(directiveContent, attributeName, directiveClass.className);
|
||||
|
||||
if (inputInfo) {
|
||||
directives.push({
|
||||
attribute: attributeName,
|
||||
type: inputInfo.type,
|
||||
className: directiveClass.className
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return directives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract input information (type) for a specific attribute and class
|
||||
*/
|
||||
function extractInputInfo(directiveContent, attributeName, className) {
|
||||
// Create a regex to find the specific class section
|
||||
const classRegex = new RegExp(`export\\s+class\\s+${escapeRegex(className)}[^}]*?{([^]*?)(?=export\\s+class|$)`, 's');
|
||||
const classMatch = directiveContent.match(classRegex);
|
||||
|
||||
if (!classMatch) {
|
||||
console.warn(`Could not find class ${className}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const classContent = classMatch[1];
|
||||
|
||||
// Regex to find the @Input setter for this attribute within the class
|
||||
const inputRegex = new RegExp(
|
||||
`@Input\\(\\)\\s+set\\s+${escapeRegex(attributeName)}\\s*\\(\\s*\\w+:\\s*([^)]+)\\s*\\)`,
|
||||
'g'
|
||||
);
|
||||
|
||||
const inputMatch = inputRegex.exec(classContent);
|
||||
if (!inputMatch) {
|
||||
console.warn(`Could not find @Input setter for attribute: ${attributeName} in class: ${className}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
let type = inputMatch[1].trim();
|
||||
|
||||
// Clean up the type (remove extra whitespace, etc.)
|
||||
type = type.replace(/\s+/g, ' ');
|
||||
|
||||
return {
|
||||
type: type
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special regex characters
|
||||
*/
|
||||
function escapeRegex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate API directives table for components
|
||||
*/
|
||||
function generateApiDirectivesTable(componentFiles, directiveFiles) {
|
||||
componentFiles.forEach((componentFile) => {
|
||||
try {
|
||||
console.log(`Processing component: ${componentFile}`);
|
||||
|
||||
const componentSelector = getComponentSelector(componentFile);
|
||||
const readmeFilePath = componentFile.replace('.ts', '.md');
|
||||
|
||||
console.log(`Component selector: ${componentSelector}`);
|
||||
|
||||
// Initialize table with header
|
||||
initializeDynamicTableContent(readmeFilePath);
|
||||
|
||||
const allDirectives = [];
|
||||
|
||||
// Extract directives from all directive files
|
||||
directiveFiles.forEach((directiveFile) => {
|
||||
console.log(`Checking directive file: ${directiveFile}`);
|
||||
const directives = extractDirectivesForComponent(directiveFile, componentSelector);
|
||||
allDirectives.push(...directives);
|
||||
});
|
||||
|
||||
console.log(`Found ${allDirectives.length} directives for ${componentSelector}`);
|
||||
|
||||
// Sort directives alphabetically by attribute name
|
||||
allDirectives.sort((a, b) => a.attribute.localeCompare(b.attribute));
|
||||
|
||||
// Add rows to table
|
||||
allDirectives.forEach((directive) => {
|
||||
addRowToTable(readmeFilePath, directive.attribute, directive.type, directive.className);
|
||||
});
|
||||
|
||||
// If no directives found, add "no directives" message
|
||||
if (allDirectives.length === 0) {
|
||||
removeApiTableContent(readmeFilePath);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error processing component ${componentFile}:`, error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize table with header
|
||||
*/
|
||||
function initializeDynamicTableContent(filePath) {
|
||||
replaceDynamicTableContent(filePath, apiDirectivesTable);
|
||||
replaceDynamicTableContent(filePath, apiDirectivesTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace table content with "no directives" message
|
||||
*/
|
||||
function removeApiTableContent(filePath) {
|
||||
const content = '_No API directives available for this component_. \n';
|
||||
replaceDynamicTableContent(filePath, content);
|
||||
const content = '_No API directives available for this component_. \n';
|
||||
replaceDynamicTableContent(filePath, content);
|
||||
}
|
||||
|
||||
function apiTableContentIsEmpty(filePath) {
|
||||
try {
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
const startIdx = data.indexOf(startApiLine);
|
||||
const endIdx = data.indexOf(endApiLine);
|
||||
if (startIdx !== -1 && endIdx !== -1) {
|
||||
const capturedContent = data.substring(startIdx + startApiLine.length, endIdx).trim();
|
||||
return capturedContent === apiDirectivesTable;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writeApiDirectivesTable(componentFiles, directiveFiles) {
|
||||
componentFiles.forEach((componentFile) => {
|
||||
// const componentName = componentFile.split('/').pop()
|
||||
const componentFileName = componentFile.split('/').pop().replace('.component.ts', '');
|
||||
const componentName = componentFileName.replace(/(?:^|-)([a-z])/g, (_, char) => char.toUpperCase());
|
||||
const readmeFilePath = componentFile.replace('.ts', '.md');
|
||||
const componentContent = fs.readFileSync(componentFile, 'utf8');
|
||||
const selectorMatch = componentContent.match(/@Component\({[^]*?selector: ['"]([^'"]+)['"][^]*?}\)/);
|
||||
const componentSelectorName = selectorMatch[1];
|
||||
initializeDynamicTableContent(readmeFilePath);
|
||||
|
||||
if (!componentSelectorName) {
|
||||
throw new Error(`Unable to find the component name in the file ${componentFileName}`);
|
||||
}
|
||||
|
||||
// const directiveRegex = new RegExp(`@Directive\\(\\s*{[^}]*selector:\\s*['"]${componentName}\\s*\\[([^'"]+)\\]`, 'g');
|
||||
const directiveRegex = /^\s*(selector):\s*(['"])(.*?)\2\s*$/gm;
|
||||
|
||||
directiveFiles.forEach((directiveFile) => {
|
||||
const directiveContent = fs.readFileSync(directiveFile, 'utf8');
|
||||
|
||||
let directiveNameMatch;
|
||||
while ((directiveNameMatch = directiveRegex.exec(directiveContent)) !== null) {
|
||||
if (directiveNameMatch[0].includes('@Directive({\n//')) {
|
||||
// Skip directives that are commented out
|
||||
continue;
|
||||
}
|
||||
const selectorValue = directiveNameMatch[3].split(',');
|
||||
const directiveMatch = selectorValue.find((value) => value.includes(componentSelectorName));
|
||||
|
||||
if (directiveMatch) {
|
||||
const directiveName = directiveMatch.match(/\[(.*?)\]/).pop();
|
||||
const className = directiveName.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase()) + 'Directive';
|
||||
const inputRegex = new RegExp(
|
||||
`@Input\\(\\)\\s+set\\s+(${directiveName.replace(/\[/g, '\\[').replace(/\]/g, '\\]')})\\((\\w+):\\s+(\\w+)`
|
||||
);
|
||||
const inputMatch = directiveContent.match(inputRegex);
|
||||
const inputType = inputMatch && inputMatch.pop();
|
||||
|
||||
if (inputType && className) {
|
||||
let finalClassName = componentName === 'Videoconference' ? className : componentName + className;
|
||||
addRowToTable(readmeFilePath, directiveName, inputType, finalClassName);
|
||||
}
|
||||
} else {
|
||||
console.log(`The selector "${componentSelectorName}" does not match with ${selectorValue}. Skipping...`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (apiTableContentIsEmpty(readmeFilePath)) {
|
||||
removeApiTableContent(readmeFilePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to add a row to a Markdown table in a file
|
||||
/**
|
||||
* Add a row to the markdown table
|
||||
*/
|
||||
function addRowToTable(filePath, parameter, type, reference) {
|
||||
// Read the current content of the file
|
||||
try {
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
try {
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
const markdownRow = `| **${parameter}** | \`${type}\` | [${reference}](../directives/${reference}.html) |`;
|
||||
|
||||
// Define the target line and the Markdown row
|
||||
const markdownRow = `| **${parameter}** | \`${type}\` | [${reference}](../directives/${reference}.html) |`;
|
||||
const lines = data.split('\n');
|
||||
const targetIndex = lines.findIndex((line) => line.includes(endApiLine));
|
||||
|
||||
// Find the line that contains the table
|
||||
const lines = data.split('\n');
|
||||
const targetIndex = lines.findIndex((line) => line.includes(endApiLine));
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
// Insert the new row above the target line
|
||||
lines.splice(targetIndex, 0, markdownRow);
|
||||
|
||||
// Join the lines back together
|
||||
const updatedContent = lines.join('\n');
|
||||
|
||||
// Write the updated content to the file
|
||||
fs.writeFileSync(filePath, updatedContent, 'utf8');
|
||||
console.log('Row added successfully.');
|
||||
} else {
|
||||
console.error('Table not found in the file.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error writing to file:', error);
|
||||
}
|
||||
if (targetIndex !== -1) {
|
||||
lines.splice(targetIndex, 0, markdownRow);
|
||||
const updatedContent = lines.join('\n');
|
||||
fs.writeFileSync(filePath, updatedContent, 'utf8');
|
||||
console.log(`Added directive: ${parameter} -> ${reference}`);
|
||||
} else {
|
||||
console.error('End marker not found in file:', filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding row to table:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace content between start and end markers
|
||||
*/
|
||||
function replaceDynamicTableContent(filePath, content) {
|
||||
// Read the current content of the file
|
||||
try {
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
const pattern = new RegExp(`${startApiLine}([\\s\\S]*?)${endApiLine}`, 'g');
|
||||
try {
|
||||
const data = fs.readFileSync(filePath, 'utf8');
|
||||
const pattern = new RegExp(`${startApiLine}([\\s\\S]*?)${endApiLine}`, 'g');
|
||||
|
||||
// Replace the content between startLine and endLine with the replacement table
|
||||
const modifiedContent = data.replace(pattern, (match, capturedContent) => {
|
||||
return startApiLine + '\n' + content + '\n' + endApiLine;
|
||||
});
|
||||
// Write the modified content back to the file
|
||||
fs.writeFileSync(filePath, modifiedContent, 'utf8');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.log(`${filePath} not found! Maybe it is an internal component. Skipping...`);
|
||||
} else {
|
||||
console.error('Error writing to file:', error);
|
||||
}
|
||||
}
|
||||
const modifiedContent = data.replace(pattern, (match, capturedContent) => {
|
||||
return startApiLine + '\n' + content + '\n' + endApiLine;
|
||||
});
|
||||
|
||||
fs.writeFileSync(filePath, modifiedContent, 'utf8');
|
||||
console.log(`Updated table content in: ${filePath}`);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.log(`${filePath} not found! Maybe it is an internal component. Skipping...`);
|
||||
} else {
|
||||
console.error('Error writing to file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const directiveFiles = getDirectiveFiles();
|
||||
const componentFiles = getComponentFiles();
|
||||
const adminFiles = getAdminFiles();
|
||||
writeApiDirectivesTable(componentFiles.concat(adminFiles), directiveFiles);
|
||||
// Main execution
|
||||
if (require.main === module) {
|
||||
try {
|
||||
const directiveFiles = getDirectiveFiles();
|
||||
const componentFiles = getComponentFiles();
|
||||
const adminFiles = getAdminFiles();
|
||||
|
||||
console.log('Starting directive table generation...');
|
||||
generateApiDirectivesTable(componentFiles.concat(adminFiles), directiveFiles);
|
||||
console.log('Directive table generation completed!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Script execution failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for testing
|
||||
module.exports = {
|
||||
generateApiDirectivesTable,
|
||||
getDirectiveFiles,
|
||||
getComponentFiles,
|
||||
getAdminFiles
|
||||
};
|
||||
@ -5,8 +5,7 @@
|
||||
"../src/lib/directives/**/*.ts",
|
||||
"../src/lib/services/**/*.ts",
|
||||
"../src/lib/models/**/*.ts",
|
||||
"../src/lib/pipes/**/*.ts",
|
||||
// "../../../src/app/openvidu-webcomponent/**/*.ts",
|
||||
"../src/lib/pipes/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/test.ts",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/openvidu-components-angular",
|
||||
"dest": "./dist",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
|
||||
@ -1,418 +0,0 @@
|
||||
{
|
||||
"name": "openvidu-components-angular",
|
||||
"version": "3.0.1-beta1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openvidu-components-angular",
|
||||
"version": "3.0.1-beta1",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "^17.0.0 || ^18.0.0",
|
||||
"@angular/cdk": "^17.0.0 || ^18.0.0",
|
||||
"@angular/common": "^17.0.0 || ^18.0.0",
|
||||
"@angular/core": "^17.0.0 || ^18.0.0",
|
||||
"@angular/forms": "^17.0.0 || ^18.0.0",
|
||||
"@angular/material": "^17.0.0 || ^18.0.0",
|
||||
"@livekit/track-processors": "^0.3.2",
|
||||
"autolinker": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"livekit-client": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/animations": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.8.tgz",
|
||||
"integrity": "sha512-dMSn2hg70siv3lhP+vqhMbgc923xw6XBUvnpCPEzhZqFHvPXfh/LubmsD5RtqHmjWebXtgVcgS+zg3Gq3jB2lg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "18.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cdk": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.8.tgz",
|
||||
"integrity": "sha512-J8A2FkwTBzLleAEWz6EgW73dEoeq87GREBPjTv8+2JV09LX+V3hnbgNk6zWq5k4OXtQNg9WrWP9QyRbUyA597g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"parse5": "^7.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^18.0.0 || ^19.0.0",
|
||||
"@angular/core": "^18.0.0 || ^19.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/common": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.8.tgz",
|
||||
"integrity": "sha512-TYsKtE5nVaIScWSLGSO34Skc+s3hB/BujSddnfQHoNFvPT/WR0dfmdlpVCTeLj+f50htFoMhW11tW99PbK+whQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "18.2.8",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/core": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.8.tgz",
|
||||
"integrity": "sha512-NwIuX/Iby1jT6Iv1/s6S3wOFf8xfuQR3MPGvKhGgNtjXLbHG+TXceK9+QPZC0s9/Z8JR/hz+li34B79GrIKgUg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rxjs": "^6.5.3 || ^7.4.0",
|
||||
"zone.js": "~0.14.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/forms": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.8.tgz",
|
||||
"integrity": "sha512-JCLki7KC6D5vF6dE6yGlBmW33khIgpHs8N9SzuiJtkQqNDTIQA8cPsGV6qpLpxflxASynQOX5lDkWYdQyfm77Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "18.2.8",
|
||||
"@angular/core": "18.2.8",
|
||||
"@angular/platform-browser": "18.2.8",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/material": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.8.tgz",
|
||||
"integrity": "sha512-wQGMVsfQ9lQfih2VsWAvV4z3S3uBxrxc61owlE+K0T1BxH9u/jo3A/rnRitIdvR/L4NnYlfhCnmrW9K+Pl+WCg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "^18.0.0 || ^19.0.0",
|
||||
"@angular/cdk": "18.2.8",
|
||||
"@angular/common": "^18.0.0 || ^19.0.0",
|
||||
"@angular/core": "^18.0.0 || ^19.0.0",
|
||||
"@angular/forms": "^18.0.0 || ^19.0.0",
|
||||
"@angular/platform-browser": "^18.0.0 || ^19.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.8.tgz",
|
||||
"integrity": "sha512-EPai4ZPqSq3ilLJUC85kPi9wo5j5suQovwtgRyjM/75D9Qy4TV19g8hkVM5Co/zrltO8a2G6vDscCNI5BeGw2A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "18.2.8",
|
||||
"@angular/common": "18.2.8",
|
||||
"@angular/core": "18.2.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@angular/animations": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
|
||||
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@livekit/protocol": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.24.0.tgz",
|
||||
"integrity": "sha512-9dCsqnkMn7lvbI4NGh18zhLDsrXyUcpS++TEFgEk5Xv1WM3R2kT3EzqgL1P/mr3jaabM6rJ8wZA/KJLuQNpF5w==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@livekit/track-processors": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.3.2.tgz",
|
||||
"integrity": "sha512-4JUCzb7yIKoVsTo8J6FTzLZJHcI6DihfX/pGRDg0SOGaxprcDPrt8jaDBBTsnGBSXHeMxl2ugN+xQjdCWzLKEA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mediapipe/holistic": "0.5.1675471629",
|
||||
"@mediapipe/tasks-vision": "0.10.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"livekit-client": "^1.12.0 || ^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediapipe/holistic": {
|
||||
"version": "0.5.1675471629",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/holistic/-/holistic-0.5.1675471629.tgz",
|
||||
"integrity": "sha512-qY+cxtDeSOvVtevrLgnodiwXYaAtPi7dHZtNv/bUCGEjFicAOYtMmrZSqMmbPkTB2+4jLnPF1vgshkAqQRSYAw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.9.tgz",
|
||||
"integrity": "sha512-/gFguyJm1ng4Qr7VVH2vKO+zZcQd8wc3YafUfvBuYFX0Y5+CvrV+VNPEVkl5W/gUZF5KNKNZAiaHPULGPCIjyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/autolinker": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/autolinker/-/autolinker-4.0.0.tgz",
|
||||
"integrity": "sha512-fl5Kh6BmEEZx+IWBfEirnRUU5+cOiV0OK7PEt0RBKvJMJ8GaRseIOeDU3FKf4j3CE5HVefcjHmhYPOcaVt0bZw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/livekit-client": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.5.9.tgz",
|
||||
"integrity": "sha512-oDpK6SKYB1F+mNO+25DA0bF0cD2XoOJeD8ji4YQpzDBQv2IxeyKrQhoqXAqrYgIKuiMNkImSf+yg2v7EHSl4Og==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@livekit/protocol": "1.24.0",
|
||||
"events": "^3.3.0",
|
||||
"loglevel": "^1.8.0",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"ts-debounce": "^4.0.0",
|
||||
"tslib": "2.7.0",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"webrtc-adapter": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loglevel": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
|
||||
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz",
|
||||
"integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"entities": "^4.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/sdp-transform": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
|
||||
"integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"sdp-verify": "checker.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-debounce": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-debounce/-/ts-debounce-4.0.0.tgz",
|
||||
"integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typed-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"rxjs": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
|
||||
"integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"sdp": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zone.js": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
|
||||
"integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,22 @@
|
||||
{
|
||||
"name": "openvidu-components-angular",
|
||||
"main": "dist/fesm2022/openvidu-components-angular.mjs",
|
||||
"module": "dist/fesm2022/openvidu-components-angular.mjs",
|
||||
"typings": "dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"name": "openvidu-components-angular",
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "^17.0.0 || ^18.0.0",
|
||||
"@angular/cdk": "^17.0.0 || ^18.0.0",
|
||||
"@angular/common": "^17.0.0 || ^18.0.0",
|
||||
"@angular/core": "^17.0.0 || ^18.0.0",
|
||||
"@angular/forms": "^17.0.0 || ^18.0.0",
|
||||
"@angular/material": "^17.0.0 || ^18.0.0",
|
||||
"@angular/animations": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/cdk": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/material": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"autolinker": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"livekit-client": "^2.1.0",
|
||||
"@livekit/track-processors": "^0.3.2"
|
||||
"livekit-client": "^2.16.0",
|
||||
"@livekit/track-processors": "^0.7.2"
|
||||
},
|
||||
"version": "3.0.1-beta1"
|
||||
"version": "3.6.0"
|
||||
}
|
||||
|
||||
@ -4,5 +4,6 @@ With the following directives you can modify the default User Interface with the
|
||||
<!-- start-dynamic-api-directives-content -->
|
||||
| **Parameter** | **Type** | **Reference** |
|
||||
|:--------------------------------: | :-------: | :---------------------------------------------: |
|
||||
| **recordingsList** | `RecordingInfo` | [AdminDashboardRecordingsListDirective](../directives/AdminDashboardRecordingsListDirective.html) |
|
||||
| **navbarTitle** | `string` | [AdminDashboardTitleDirective](../directives/AdminDashboardTitleDirective.html) |
|
||||
| **recordingsList** | `RecordingInfo[]` | [AdminDashboardRecordingsListDirective](../directives/AdminDashboardRecordingsListDirective.html) |
|
||||
<!-- end-dynamic-api-directives-content -->
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
.header {
|
||||
height: 50px;
|
||||
background-color: var(--ov-secondary-action-color);
|
||||
background-color: var(--ov-primary-action-color);
|
||||
color: var(--ov-text-primary-color);
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
#sort-menu-btn {
|
||||
margin-left: 5px;
|
||||
background-color: var(--ov-surface-color);
|
||||
color: var(--ov-text-primary-color);
|
||||
color: var(--ov-text-surface-color);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
@ -140,7 +140,7 @@
|
||||
}
|
||||
|
||||
.video-btns button #play {
|
||||
color: var(--ov-text-primary-color);
|
||||
color: var(--ov-primary-action-color);
|
||||
}
|
||||
.video-btns button #download {
|
||||
color: var(--ov-accent-action-color);
|
||||
@ -167,7 +167,7 @@
|
||||
|
||||
.video-card-tag {
|
||||
font-size: 13px;
|
||||
color: var(--ov-text-primary-color);
|
||||
color: var(--ov-text-surface-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@ -236,13 +236,11 @@
|
||||
text-align: center;
|
||||
color: var(--ov-text-primary-color);
|
||||
}
|
||||
/* TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version. */
|
||||
::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex {
|
||||
padding: 0px !important;
|
||||
background-color: var(--ov-light-color) !important;
|
||||
}
|
||||
|
||||
/* TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version. */
|
||||
::ng-deep .mat-form-field-wrapper {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
@ -8,7 +8,8 @@ import { RecordingService } from '../../services/recording/recording.service';
|
||||
@Component({
|
||||
selector: 'ov-admin-dashboard',
|
||||
templateUrl: './admin-dashboard.component.html',
|
||||
styleUrls: ['./admin-dashboard.component.scss']
|
||||
styleUrls: ['./admin-dashboard.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class AdminDashboardComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
|
||||
@ -6,4 +6,5 @@ With the following directives you can modify the default User Interface with the
|
||||
| **Parameter** | **Type** | **Reference** |
|
||||
|:--------------------------------: | :-------: | :---------------------------------------------: |
|
||||
| **error** | `any` | [AdminLoginErrorDirective](../directives/AdminLoginErrorDirective.html) |
|
||||
| **navbarTitle** | `any` | [AdminLoginTitleDirective](../directives/AdminLoginTitleDirective.html) |
|
||||
<!-- end-dynamic-api-directives-content -->
|
||||
@ -19,7 +19,6 @@
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-btn {
|
||||
@ -33,5 +32,5 @@
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-progress-spinner {
|
||||
--mdc-circular-progress-active-indicator-color: var(--ov-accent-action-color);
|
||||
--mat-progress-spinner-active-indicator-color: var(--ov-accent-action-color);
|
||||
}
|
||||
|
||||
@ -7,7 +7,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
|
||||
@Component({
|
||||
selector: 'ov-admin-login',
|
||||
templateUrl: './admin-login.component.html',
|
||||
styleUrls: ['./admin-login.component.scss']
|
||||
styleUrls: ['./admin-login.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class AdminLoginComponent implements OnInit {
|
||||
/**
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
margin: auto;
|
||||
height: 80%;
|
||||
width: 3px;
|
||||
background: var(--ov-secondary-action-color);
|
||||
background: var(--ov-text-primary-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
@ -5,11 +5,14 @@ import { Component } from '@angular/core';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ov-audio-wave',
|
||||
template: `<div class="audio-container">
|
||||
<div class="stick normal play"></div>
|
||||
<div class="stick loud play"></div>
|
||||
<div class="stick normal play"></div>
|
||||
</div>`,
|
||||
styleUrls: ['./audio-wave.component.scss']
|
||||
template: `
|
||||
<div class="audio-container audio-wave-indicator">
|
||||
<div class="stick normal play"></div>
|
||||
<div class="stick loud play"></div>
|
||||
<div class="stick normal play"></div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./audio-wave.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class AudioWaveComponent {}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
.poster {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #000000;
|
||||
position: absolute;
|
||||
z-index: 888;
|
||||
border-radius: var(--ov-video-radius);
|
||||
}
|
||||
|
||||
.initial {
|
||||
position: absolute;
|
||||
display: inline-grid;
|
||||
z-index: 1;
|
||||
margin: auto;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
border-radius: var(--ov-video-radius);
|
||||
border: 2px solid var(--ov-text-primary-color);
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#poster-text {
|
||||
padding: 0px !important;
|
||||
font-weight: bold;
|
||||
font-size: 40px;
|
||||
margin: auto;
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ov-avatar-profile',
|
||||
template: `
|
||||
<div class="poster" id="video-poster">
|
||||
<div class="initial" [ngStyle]="{ 'background-color': color }">
|
||||
<span id="poster-text">{{ letter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./avatar-profile.component.scss']
|
||||
})
|
||||
export class AvatarProfileComponent {
|
||||
letter: string;
|
||||
|
||||
@Input()
|
||||
set name(name: string) {
|
||||
if (name) this.letter = name[0];
|
||||
}
|
||||
@Input() color;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@ -43,7 +43,7 @@ mat-spinner {
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-progress-spinner {
|
||||
--mdc-circular-progress-active-indicator-color: var(--ov-accent-action-color);
|
||||
--mat-progress-spinner-active-indicator-color: var(--ov-accent-action-color);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -5,8 +5,8 @@ import { MatDialogRef } from '@angular/material/dialog';
|
||||
* @internal
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-delete-dialog',
|
||||
template: `
|
||||
selector: 'app-delete-dialog',
|
||||
template: `
|
||||
<div mat-dialog-content>{{ 'PANEL.RECORDING.DELETE_QUESTION' | translate }}</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.RECORDING.CANCEL' | translate }}</button>
|
||||
@ -15,8 +15,8 @@ import { MatDialogRef } from '@angular/material/dialog';
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
styles: [
|
||||
`
|
||||
::ng-deep .mat-mdc-dialog-content {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
@ -26,17 +26,18 @@ import { MatDialogRef } from '@angular/material/dialog';
|
||||
}
|
||||
#delete-recording-confirm-btn {
|
||||
background-color: var(--ov-error-color) !important;
|
||||
color: var(--ov-secondary-action-color);
|
||||
color: var(--ov-primary-action-color);
|
||||
}
|
||||
.mat-mdc-button,
|
||||
.mat-mdc-button:not(:disabled),
|
||||
::ng-deep .mat-mdc-button .mat-mdc-button-persistent-ripple::before {
|
||||
color: var(--ov-secondary-action-color);
|
||||
color: var(--ov-text-primary-color) !important;
|
||||
background-color: var(--ov-primary-action-color) !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
`
|
||||
]
|
||||
],
|
||||
standalone: false
|
||||
})
|
||||
export class DeleteDialogComponent {
|
||||
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>) {}
|
||||
|
||||
@ -7,16 +7,16 @@ import { DialogData } from '../../models/dialog.model';
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ov-dialog-template',
|
||||
template: `
|
||||
selector: 'ov-dialog-template',
|
||||
template: `
|
||||
<h1 mat-dialog-title>{{ data.title }}</h1>
|
||||
<div mat-dialog-content id="openvidu-dialog">{{ data.description }}</div>
|
||||
<div mat-dialog-actions *ngIf="data.showActionButtons">
|
||||
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
styles: [
|
||||
`
|
||||
::ng-deep .mat-mdc-dialog-content {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
@ -28,12 +28,13 @@ import { DialogData } from '../../models/dialog.model';
|
||||
.mat-mdc-button,
|
||||
.mat-mdc-button:not(:disabled),
|
||||
::ng-deep .mat-mdc-button .mat-mdc-button-persistent-ripple::before {
|
||||
color: var(--ov-secondary-action-color);
|
||||
color: var(--ov-text-primary-color);
|
||||
background-color: var(--ov-primary-action-color) !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
`
|
||||
]
|
||||
],
|
||||
standalone: false
|
||||
})
|
||||
export class DialogTemplateComponent {
|
||||
constructor(
|
||||
|
||||
@ -8,8 +8,8 @@ import { DialogData } from '../../models/dialog.model';
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ov-pro-feature-template',
|
||||
template: `
|
||||
selector: 'ov-pro-feature-template',
|
||||
template: `
|
||||
<h1 mat-dialog-title>{{ data.title }}</h1>
|
||||
<div mat-dialog-content>{{ data.description }}</div>
|
||||
<div mat-dialog-actions *ngIf="data.showActionButtons">
|
||||
@ -19,7 +19,8 @@ import { DialogData } from '../../models/dialog.model';
|
||||
</button>
|
||||
<button mat-button (click)="close()">{{'PANEL.CLOSE' | translate}}</button>
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
standalone: false
|
||||
})
|
||||
export class ProFeatureDialogTemplateComponent {
|
||||
constructor(public dialogRef: MatDialogRef<ProFeatureDialogTemplateComponent>, @Inject(MAT_DIALOG_DATA) public data: DialogData) {}
|
||||
|
||||
@ -6,8 +6,8 @@ import { RecordingDialogData } from '../../models/dialog.model';
|
||||
* @internal
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-recording-dialog',
|
||||
template: `
|
||||
selector: 'app-recording-dialog',
|
||||
template: `
|
||||
<div mat-dialog-content>
|
||||
<video #videoElement controls autoplay [src]="src" (error)="handleError()"></video>
|
||||
</div>
|
||||
@ -15,8 +15,8 @@ import { RecordingDialogData } from '../../models/dialog.model';
|
||||
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
styles: [
|
||||
`
|
||||
::ng-deep .mat-mdc-dialog-content {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
@ -33,12 +33,13 @@ import { RecordingDialogData } from '../../models/dialog.model';
|
||||
.mat-mdc-button,
|
||||
.mat-mdc-button:not(:disabled),
|
||||
::ng-deep .mat-mdc-button .mat-mdc-button-persistent-ripple::before {
|
||||
color: var(--ov-secondary-action-color);
|
||||
color: var(--ov-text-primary-color);
|
||||
background-color: var(--ov-primary-action-color) !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
`
|
||||
]
|
||||
],
|
||||
standalone: false
|
||||
})
|
||||
export class RecordingDialogComponent {
|
||||
@ViewChild('videoElement', { static: true }) videoElement: ElementRef<HTMLVideoElement>;
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
<!-- Landscape orientation warning for mobile devices -->
|
||||
<div id="landscape-warning" [@inOutAnimation]>
|
||||
<div class="warning-message">
|
||||
<mat-icon class="warning-icon">screen_rotation</mat-icon>
|
||||
<span>{{ 'ROOM.LANDSCAPE_WARNING' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,33 @@
|
||||
#landscape-warning {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--ov-background-color);
|
||||
opacity: 95%;
|
||||
align-content: space-evenly;
|
||||
text-align: center;
|
||||
color: var(--ov-text-primary-color);
|
||||
|
||||
.warning-message {
|
||||
display: inline-grid;
|
||||
display: -moz-inline-grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 50px;
|
||||
margin: auto;
|
||||
margin-bottom: 10px;
|
||||
animation: boomerang 1.2s ease-in-out infinite alternate;
|
||||
|
||||
@keyframes boomerang {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Component to display a landscape orientation warning on mobile devices.
|
||||
* @internal
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ov-landscape-warning',
|
||||
templateUrl: './landscape-warning.component.html',
|
||||
styleUrl: './landscape-warning.component.scss',
|
||||
standalone: false,
|
||||
animations: [
|
||||
trigger('inOutAnimation', [
|
||||
transition(':enter', [style({ opacity: 0 }), animate('200ms', style({ opacity: 1 }))]),
|
||||
transition(':leave', [animate('200ms', style({ opacity: 0 }))])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class LandscapeWarningComponent {}
|
||||
@ -1,16 +1,23 @@
|
||||
<div class="container" [ngClass]="{ withCaptions: captionsEnabled, withMargin: localParticipant.isMinimized }">
|
||||
<div id="layout" class="layout" #layout>
|
||||
<!-- Top slot: Render elements that should appear at the top -->
|
||||
@if (layoutAdditionalElementsTemplate && templateConfig.layoutAdditionalElementsSlot === 'top') {
|
||||
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
|
||||
}
|
||||
|
||||
<div
|
||||
#localLayoutElement
|
||||
*ngFor="let track of localParticipant.tracks; trackBy: trackParticipantElement"
|
||||
[ngClass]="{
|
||||
local_participant: true,
|
||||
OV_root: !track.isAudioTrack && !track.isMinimized,
|
||||
OV_publisher: !track.isAudioTrack && !track.isMinimized,
|
||||
OV_minimized: track.isMinimized,
|
||||
OV_big: track.isPinned,
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
|
||||
OV_screen: track.isScreenTrack
|
||||
}"
|
||||
[id]="'local-element-' + track.source"
|
||||
[id]="'participant-' + track.participant.identity"
|
||||
cdkDrag
|
||||
cdkDragBoundary=".layout"
|
||||
[cdkDragDisabled]="!track.isMinimized"
|
||||
@ -19,18 +26,30 @@
|
||||
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Default slot: Render additional layout elements (backward compatibility and default position) -->
|
||||
@if (layoutAdditionalElementsTemplate && (templateConfig.layoutAdditionalElementsSlot === 'default' || !templateConfig.layoutAdditionalElementsSlot)) {
|
||||
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
|
||||
}
|
||||
|
||||
<div
|
||||
*ngFor="let track of remoteParticipants | tracks; trackBy: trackParticipantElement"
|
||||
class="remote-participant"
|
||||
[id]="'participant-' + track.participant.identity"
|
||||
[ngClass]="{
|
||||
OV_root: !track.isAudioTrack,
|
||||
OV_publisher: !track.isAudioTrack,
|
||||
OV_big: track.isPinned,
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
|
||||
OV_screen: track.isScreenTrack
|
||||
}"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Bottom slot: Render elements that should appear at the bottom -->
|
||||
@if (layoutAdditionalElementsTemplate && templateConfig.layoutAdditionalElementsSlot === 'bottom') {
|
||||
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- <ov-captions *ngIf="captionsEnabled" class="OV_ignored"></ov-captions> -->
|
||||
|
||||
@ -21,6 +21,6 @@ It will recognise the following directive in a child element.
|
||||
With the following directives you can modify the default User Interface with the aim of fully customizing your videoconference application.
|
||||
|
||||
<!-- start-dynamic-api-directives-content -->
|
||||
_No API directives available for this component_.
|
||||
_No API directives available for this component_.
|
||||
|
||||
<!-- end-dynamic-api-directives-content -->
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
.OV_root,
|
||||
.OV_root * {
|
||||
color: var(--ov-secondary-action-color);
|
||||
color: var(--ov-text-primary-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { LayoutAdditionalElementsDirective } from '../../directives/template/internals.directive';
|
||||
|
||||
import { CdkDrag } from '@angular/cdk/drag-drop';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@ -11,16 +14,15 @@ import {
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import { combineLatest, map, Subscription } from 'rxjs';
|
||||
import { combineLatest, map, Subject, takeUntil } from 'rxjs';
|
||||
import { StreamDirective } from '../../directives/template/openvidu-components-angular.directive';
|
||||
import { ParticipantTrackPublication, ParticipantModel } from '../../models/participant.model';
|
||||
import { LayoutService } from '../../services/layout/layout.service';
|
||||
import { ParticipantService } from '../../services/participant/participant.service';
|
||||
import { CdkDrag } from '@angular/cdk/drag-drop';
|
||||
import { PanelService } from '../../services/panel/panel.service';
|
||||
import { GlobalConfigService } from '../../services/config/global-config.service';
|
||||
import { ServiceConfigService } from '../../services/config/service-config.service';
|
||||
import { ParticipantModel, ParticipantTrackPublication } from '../../models/participant.model';
|
||||
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
||||
import { GlobalConfigService } from '../../services/config/global-config.service';
|
||||
import { LayoutService } from '../../services/layout/layout.service';
|
||||
import { PanelService } from '../../services/panel/panel.service';
|
||||
import { ParticipantService } from '../../services/participant/participant.service';
|
||||
import { LayoutTemplateConfiguration, TemplateManagerService } from '../../services/template/template-manager.service';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -31,7 +33,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
|
||||
selector: 'ov-layout',
|
||||
templateUrl: './layout.component.html',
|
||||
styleUrls: ['./layout.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
/**
|
||||
@ -39,6 +42,11 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
*/
|
||||
@ContentChild('stream', { read: TemplateRef }) streamTemplate: TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ContentChild('layoutAdditionalElements', { read: TemplateRef }) layoutAdditionalElementsTemplate: TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ -62,9 +70,27 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
// is inside of the layout component tagged with '*ovLayout' directive
|
||||
if (externalStream) {
|
||||
this.streamTemplate = externalStream.template;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ContentChild(LayoutAdditionalElementsDirective) set externalAdditionalElements(
|
||||
externalAdditionalElements: LayoutAdditionalElementsDirective
|
||||
) {
|
||||
if (externalAdditionalElements) {
|
||||
this._externalLayoutAdditionalElements = externalAdditionalElements;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
templateConfig: LayoutTemplateConfiguration = {};
|
||||
|
||||
localParticipant: ParticipantModel | undefined;
|
||||
remoteParticipants: ParticipantModel[] = [];
|
||||
/**
|
||||
@ -72,31 +98,32 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
*/
|
||||
captionsEnabled = true;
|
||||
|
||||
private localParticipantSubs: Subscription;
|
||||
private remoteParticipantsSubs: Subscription;
|
||||
private captionsSubs: Subscription;
|
||||
private _externalStream?: StreamDirective;
|
||||
private _externalLayoutAdditionalElements?: LayoutAdditionalElementsDirective;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private resizeObserver: ResizeObserver;
|
||||
private cdkSubscription: Subscription;
|
||||
private resizeTimeout: NodeJS.Timeout;
|
||||
private videoIsAtRight: boolean = false;
|
||||
private lastLayoutWidth: number = 0;
|
||||
private layoutService: LayoutService;
|
||||
private lastLayoutHeight: number = 0;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
constructor(
|
||||
private serviceConfig: ServiceConfigService,
|
||||
private layoutService: LayoutService,
|
||||
private panelService: PanelService,
|
||||
private participantService: ParticipantService,
|
||||
private globalService: GlobalConfigService,
|
||||
private directiveService: OpenViduComponentsConfigService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
this.layoutService = this.serviceConfig.getLayoutService();
|
||||
}
|
||||
private cd: ChangeDetectorRef,
|
||||
private templateManagerService: TemplateManagerService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupTemplates();
|
||||
|
||||
this.subscribeToParticipants();
|
||||
this.subscribeToCaptions();
|
||||
}
|
||||
@ -104,19 +131,19 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
ngAfterViewInit() {
|
||||
console.log('LayoutComponent.ngAfterViewInit');
|
||||
this.layoutService.initialize(this.layoutContainer.element.nativeElement);
|
||||
this.lastLayoutWidth = this.layoutContainer.element.nativeElement.getBoundingClientRect().width;
|
||||
const rect = this.layoutContainer.element.nativeElement.getBoundingClientRect();
|
||||
this.lastLayoutWidth = rect.width;
|
||||
this.lastLayoutHeight = rect.height;
|
||||
this.listenToResizeLayout();
|
||||
this.listenToCdkDrag();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.localParticipant = undefined;
|
||||
this.remoteParticipants = [];
|
||||
this.resizeObserver?.disconnect();
|
||||
this.localParticipantSubs?.unsubscribe();
|
||||
this.remoteParticipantsSubs?.unsubscribe();
|
||||
this.captionsSubs?.unsubscribe();
|
||||
this.cdkSubscription?.unsubscribe();
|
||||
this.layoutService.clear();
|
||||
}
|
||||
|
||||
@ -129,8 +156,36 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
return track;
|
||||
}
|
||||
|
||||
private setupTemplates() {
|
||||
this.templateConfig = this.templateManagerService.setupLayoutTemplates(
|
||||
this._externalStream,
|
||||
this._externalLayoutAdditionalElements
|
||||
);
|
||||
|
||||
// Apply templates to component properties for backward compatibility
|
||||
this.applyTemplateConfiguration();
|
||||
}
|
||||
|
||||
private applyTemplateConfiguration() {
|
||||
if (this.templateConfig.layoutStreamTemplate) {
|
||||
this.streamTemplate = this.templateConfig.layoutStreamTemplate;
|
||||
}
|
||||
if (this.templateConfig.layoutAdditionalElementsTemplate) {
|
||||
this.layoutAdditionalElementsTemplate = this.templateConfig.layoutAdditionalElementsTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Updates templates and triggers change detection
|
||||
*/
|
||||
private updateTemplatesAndMarkForCheck(): void {
|
||||
this.setupTemplates();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private subscribeToCaptions() {
|
||||
this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => {
|
||||
this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.captionsEnabled = value;
|
||||
this.cd.markForCheck();
|
||||
this.layoutService.update();
|
||||
@ -138,7 +193,7 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private subscribeToParticipants() {
|
||||
this.localParticipantSubs = this.participantService.localParticipant$.subscribe((p) => {
|
||||
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p) => {
|
||||
if (p) {
|
||||
this.localParticipant = p;
|
||||
if (!this.localParticipant?.isMinimized) {
|
||||
@ -149,29 +204,37 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.remoteParticipantsSubs = combineLatest([
|
||||
this.participantService.remoteParticipants$,
|
||||
this.directiveService.layoutRemoteParticipants$
|
||||
])
|
||||
.pipe(
|
||||
map(([serviceParticipants, directiveParticipants]) =>
|
||||
directiveParticipants !== undefined ? directiveParticipants : serviceParticipants
|
||||
combineLatest([this.participantService.remoteParticipants$, this.directiveService.layoutRemoteParticipants$])
|
||||
.pipe(
|
||||
map(([serviceParticipants, directiveParticipants]) =>
|
||||
directiveParticipants !== undefined ? directiveParticipants : serviceParticipants
|
||||
),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
)
|
||||
.subscribe((participants) => {
|
||||
this.remoteParticipants = participants;
|
||||
this.layoutService.update();
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
.subscribe((participants) => {
|
||||
this.remoteParticipants = participants;
|
||||
this.layoutService.update();
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private listenToResizeLayout() {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
const { width: parentWidth, height: parentHeight } = entries[0].contentRect;
|
||||
|
||||
clearTimeout(this.resizeTimeout);
|
||||
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
// Always update layout when container size changes
|
||||
// This ensures layout recalculates when parent containers change
|
||||
const widthDiff = Math.abs(this.lastLayoutWidth - parentWidth);
|
||||
const heightDiff = Math.abs(this.lastLayoutHeight - parentHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
this.layoutService.update();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
// Handle minimized participant positioning
|
||||
if (this.localParticipant?.isMinimized) {
|
||||
const { width: parentWidth } = entries[0].contentRect;
|
||||
if (this.panelService.isPanelOpened()) {
|
||||
if (this.lastLayoutWidth < parentWidth) {
|
||||
// Layout is bigger than before. Maybe the settings panel(wider) has been transitioned to another panel.
|
||||
@ -190,8 +253,10 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.moveStreamToRight(parentWidth);
|
||||
}
|
||||
}
|
||||
this.lastLayoutWidth = parentWidth;
|
||||
}
|
||||
|
||||
this.lastLayoutWidth = parentWidth;
|
||||
this.lastLayoutHeight = parentHeight;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
@ -208,7 +273,6 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
const handler = (event) => {
|
||||
if (!this.panelService.isPanelOpened()) return;
|
||||
const { x, width } = this.localLayoutElement.nativeElement.getBoundingClientRect();
|
||||
console.log(x);
|
||||
const { width: parentWidth } = this.layoutContainer.element.nativeElement.getBoundingClientRect();
|
||||
if (x === 0) {
|
||||
// Video is at the left
|
||||
@ -221,7 +285,8 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.videoIsAtRight = false;
|
||||
}
|
||||
};
|
||||
this.cdkSubscription = this.cdkDrag.released.subscribe(handler);
|
||||
|
||||
this.cdkDrag.released.pipe(takeUntil(this.destroy$)).subscribe(handler);
|
||||
|
||||
if (this.globalService.isProduction()) return;
|
||||
// Just for allow E2E testing with drag and drop
|
||||
|
||||
@ -9,5 +9,5 @@ video {
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
border-radius: var(--ov-video-radius);
|
||||
background-color: #000000;
|
||||
background-color: var(--ov-video-background, var(--ov-primary-action-color));
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Track } from 'livekit-client';
|
||||
|
||||
/**
|
||||
@ -8,7 +8,13 @@ import { Track } from 'livekit-client';
|
||||
@Component({
|
||||
selector: 'ov-media-element',
|
||||
template: `
|
||||
<ov-avatar-profile @posterAnimation *ngIf="showAvatar" [name]="avatarName" [color]="avatarColor"></ov-avatar-profile>
|
||||
<ov-video-poster
|
||||
@posterAnimation
|
||||
[showAvatar]="showAvatar"
|
||||
[nickname]="avatarName"
|
||||
[color]="avatarColor"
|
||||
[hasEncryptionError]="hasEncryptionError"
|
||||
></ov-video-poster>
|
||||
<video #videoElement *ngIf="_track?.kind === 'video'" class="OV_video-element" [attr.id]="_track?.sid"></video>
|
||||
<audio #audioElement *ngIf="_track?.kind === 'audio'" [attr.id]="_track?.sid"></audio>
|
||||
`,
|
||||
@ -18,38 +24,46 @@ import { Track } from 'livekit-client';
|
||||
transition(':enter', [style({ opacity: 0 }), animate('100ms', style({ opacity: 1 }))]),
|
||||
transition(':leave', [style({ opacity: 1 }), animate('200ms', style({ opacity: 0 }))])
|
||||
])
|
||||
]
|
||||
],
|
||||
standalone: false
|
||||
})
|
||||
export class MediaElementComponent implements AfterViewInit {
|
||||
export class MediaElementComponent implements AfterViewInit, OnDestroy {
|
||||
_track: Track;
|
||||
_videoElement: ElementRef;
|
||||
_audioElement: ElementRef;
|
||||
type: Track.Source = Track.Source.Camera;
|
||||
private _muted: boolean = false;
|
||||
private previousTrack: Track | null = null;
|
||||
|
||||
@Input() showAvatar: boolean;
|
||||
@Input() avatarColor: string;
|
||||
@Input() avatarName: string;
|
||||
@Input() isLocal: boolean;
|
||||
@Input() showAvatar: boolean = false;
|
||||
@Input() avatarColor: string = '#000000';
|
||||
@Input() avatarName: string = 'User';
|
||||
@Input() isLocal: boolean = false;
|
||||
@Input() hasEncryptionError: boolean = false;
|
||||
|
||||
@ViewChild('videoElement', { static: false })
|
||||
set videoElement(element: ElementRef) {
|
||||
this._videoElement = element;
|
||||
this.attachTracks();
|
||||
|
||||
}
|
||||
|
||||
@ViewChild('audioElement', { static: false })
|
||||
set audioElement(element: ElementRef) {
|
||||
this._audioElement = element;
|
||||
this.attachTracks();
|
||||
|
||||
}
|
||||
|
||||
@Input()
|
||||
set track(track: Track) {
|
||||
if (!track) return;
|
||||
|
||||
// Detach previous track if it's different
|
||||
if (this.previousTrack && this.previousTrack !== track) {
|
||||
this.detachPreviousTrack();
|
||||
}
|
||||
|
||||
this._track = track;
|
||||
this.previousTrack = track;
|
||||
this.attachTracks();
|
||||
}
|
||||
|
||||
@ -68,6 +82,23 @@ export class MediaElementComponent implements AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.detachPreviousTrack();
|
||||
}
|
||||
|
||||
private detachPreviousTrack() {
|
||||
if (this.previousTrack) {
|
||||
// Detach from video element
|
||||
if (this.isVideoTrack() && this._videoElement?.nativeElement) {
|
||||
this.previousTrack.detach(this._videoElement.nativeElement);
|
||||
}
|
||||
// Detach from audio element
|
||||
if (this.isAudioTrack() && this._audioElement?.nativeElement) {
|
||||
this.previousTrack.detach(this._audioElement.nativeElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateVideoStyles() {
|
||||
this.type = this._track.source;
|
||||
if (this.type === Track.Source.ScreenShare) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="panel-container" id="activities-container">
|
||||
<div class="panel-header-container">
|
||||
<h3 class="panel-title">Activities</h3>
|
||||
<h3 class="panel-title">{{ 'PANEL.ACTIVITIES.TITLE' | translate }}</h3>
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
@ -17,6 +17,8 @@
|
||||
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
|
||||
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
|
||||
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
|
||||
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
|
||||
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
|
||||
></ov-recording-activity>
|
||||
<ov-broadcasting-activity
|
||||
*ngIf="showBroadcastingActivity"
|
||||
|
||||
@ -4,6 +4,6 @@ With the following directives you can modify the default User Interface with the
|
||||
<!-- start-dynamic-api-directives-content -->
|
||||
| **Parameter** | **Type** | **Reference** |
|
||||
|:--------------------------------: | :-------: | :---------------------------------------------: |
|
||||
| **recordingActivity** | `boolean` | [ActivitiesPanelRecordingActivityDirective](../directives/ActivitiesPanelRecordingActivityDirective.html) |
|
||||
| **broadcastingActivity** | `boolean` | [ActivitiesPanelBroadcastingActivityDirective](../directives/ActivitiesPanelBroadcastingActivityDirective.html) |
|
||||
| **recordingActivity** | `boolean` | [ActivitiesPanelRecordingActivityDirective](../directives/ActivitiesPanelRecordingActivityDirective.html) |
|
||||
<!-- end-dynamic-api-directives-content -->
|
||||
@ -1,5 +1,3 @@
|
||||
$ov-activity-status-color: #afafaf;
|
||||
|
||||
:host {
|
||||
.activities-body-container {
|
||||
display: block !important;
|
||||
@ -14,12 +12,12 @@ $ov-activity-status-color: #afafaf;
|
||||
padding: 3px;
|
||||
font-size: 11px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
background-color: $ov-activity-status-color;
|
||||
background-color: var(--ov-secondary-action-color);
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
display: inherit;
|
||||
background-color: $ov-activity-status-color;;
|
||||
background-color: var(--ov-secondary-action-color);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
margin: 10px 0px !important;
|
||||
padding: 10px;
|
||||
@ -91,6 +89,10 @@ $ov-activity-status-color: #afafaf;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-indicator svg {
|
||||
fill: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep
|
||||
.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta
|
||||
.mdc-list-item__end::before {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { PanelStatusInfo, PanelType } from '../../../models/panel.model';
|
||||
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
|
||||
import { PanelService } from '../../../services/panel/panel.service';
|
||||
@ -20,7 +20,8 @@ import { BroadcastingStartRequestedEvent, BroadcastingStopRequestedEvent } from
|
||||
selector: 'ov-activities-panel',
|
||||
templateUrl: './activities-panel.component.html',
|
||||
styleUrls: ['../panel.component.scss', './activities-panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class ActivitiesPanelComponent implements OnInit {
|
||||
/**
|
||||
@ -53,6 +54,21 @@ export class ActivitiesPanelComponent implements OnInit {
|
||||
*/
|
||||
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Provides event notifications that fire when view recordings button has been clicked.
|
||||
* This event is triggered when the user wants to view all recordings in an external page.
|
||||
*/
|
||||
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Provides event notifications that fire when view recording button has been clicked.
|
||||
* This event is triggered when the user wants to view a specific recording in an external page.
|
||||
* It provides the recording ID as event data.
|
||||
*/
|
||||
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* Provides event notifications that fire when start broadcasting button is clicked.
|
||||
* It provides the {@link BroadcastingStartRequestedEvent} payload as event data.
|
||||
@ -79,9 +95,7 @@ export class ActivitiesPanelComponent implements OnInit {
|
||||
* @internal
|
||||
*/
|
||||
showBroadcastingActivity: boolean = true;
|
||||
private panelSubscription: Subscription;
|
||||
private recordingActivitySub: Subscription;
|
||||
private broadcastingActivitySub: Subscription;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -104,9 +118,8 @@ export class ActivitiesPanelComponent implements OnInit {
|
||||
* @internal
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
if (this.panelSubscription) this.panelSubscription.unsubscribe();
|
||||
if (this.recordingActivitySub) this.recordingActivitySub.unsubscribe();
|
||||
if (this.broadcastingActivitySub) this.broadcastingActivitySub.unsubscribe();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,7 +130,7 @@ export class ActivitiesPanelComponent implements OnInit {
|
||||
}
|
||||
|
||||
private subscribeToPanelToggling() {
|
||||
this.panelSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
|
||||
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
|
||||
if (ev.panelType === PanelType.ACTIVITIES && !!ev.subOptionType) {
|
||||
this.expandedPanel = ev.subOptionType;
|
||||
}
|
||||
@ -125,12 +138,12 @@ export class ActivitiesPanelComponent implements OnInit {
|
||||
}
|
||||
|
||||
private subscribeToActivitiesPanelDirective() {
|
||||
this.recordingActivitySub = this.libService.recordingActivity$.subscribe((value: boolean) => {
|
||||
this.libService.recordingActivity$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.showRecordingActivity = value;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.broadcastingActivitySub = this.libService.broadcastingActivity$.subscribe((value: boolean) => {
|
||||
this.libService.broadcastingActivity$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.showBroadcastingActivity = value;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
$ov-broadcasting-color: #5903ca;
|
||||
|
||||
$ov-input-color: #cccccc;
|
||||
|
||||
|
||||
.time-container {
|
||||
padding: 2px;
|
||||
}
|
||||
@ -14,29 +9,17 @@ $ov-input-color: #cccccc;
|
||||
}
|
||||
|
||||
#broadcasting-icon {
|
||||
color: $ov-broadcasting-color !important;
|
||||
color: var(--ov-broadcasting-color, #5903ca) !important;
|
||||
}
|
||||
|
||||
.broadcasting-duration {
|
||||
background-color: var(--ov-secondary-action-color);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.broadcasting-duration mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.started {
|
||||
background-color: $ov-broadcasting-color !important;
|
||||
background-color: var(--ov-broadcasting-color, #5903ca) !important;
|
||||
color: var(--ov-text-primary-color);
|
||||
}
|
||||
|
||||
.activity-icon.started {
|
||||
background-color: $ov-broadcasting-color !important;
|
||||
background-color: var(--ov-broadcasting-color, #5903ca) !important;
|
||||
color: var(--ov-text-primary-color);
|
||||
}
|
||||
|
||||
@ -119,10 +102,10 @@ mat-expansion-panel {
|
||||
.input-container {
|
||||
height: 25px;
|
||||
display: flex;
|
||||
background-color: $ov-input-color;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
border: 1px solid var(--ov-border-color);
|
||||
order: 3;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
@ -131,6 +114,7 @@ mat-expansion-panel {
|
||||
.input-container input {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
color: var(--ov-text-surface-color);
|
||||
margin: auto;
|
||||
background-color: transparent;
|
||||
display: block;
|
||||
@ -143,5 +127,19 @@ mat-expansion-panel {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ov-text-surface-color);
|
||||
opacity: 1; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
#broadcasting-btn {
|
||||
color: var(--ov-text-secondary-color);
|
||||
|
||||
&:disabled {
|
||||
// background-color: var(--ov-disabled-color) !important;
|
||||
color: var(--ov-text-disabled-color) !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import {
|
||||
BroadcastingStartRequestedEvent,
|
||||
BroadcastingStatus,
|
||||
@ -18,7 +18,8 @@ import { OpenViduService } from '../../../../services/openvidu/openvidu.service'
|
||||
selector: 'ov-broadcasting-activity',
|
||||
templateUrl: './broadcasting-activity.component.html',
|
||||
styleUrls: ['./broadcasting-activity.component.scss', '../activities-panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
|
||||
// TODO: Allow to add more than one broadcast url
|
||||
@ -75,7 +76,7 @@ export class BroadcastingActivityComponent implements OnInit {
|
||||
*/
|
||||
isPanelOpened: boolean = false;
|
||||
|
||||
private broadcastingSub: Subscription;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -98,7 +99,8 @@ export class BroadcastingActivityComponent implements OnInit {
|
||||
* @internal
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
if (this.broadcastingSub) this.broadcastingSub.unsubscribe();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,7 +148,7 @@ export class BroadcastingActivityComponent implements OnInit {
|
||||
}
|
||||
|
||||
private subscribeToBroadcastingStatus() {
|
||||
this.broadcastingSub = this.broadcastingService.broadcastingStatusObs.subscribe((event: BroadcastingStatusInfo | undefined) => {
|
||||
this.broadcastingService.broadcastingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: BroadcastingStatusInfo | undefined) => {
|
||||
if (!!event) {
|
||||
const { status, broadcastingId, error } = event;
|
||||
this.broadcastingStatus = status;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, OnDestroy, Output } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import {
|
||||
RecordingDeleteRequestedEvent,
|
||||
RecordingDownloadClickedEvent,
|
||||
@ -16,6 +16,7 @@ import { RecordingService } from '../../../../services/recording/recording.servi
|
||||
import { OpenViduService } from '../../../../services/openvidu/openvidu.service';
|
||||
import { ILogger } from '../../../../models/logger.model';
|
||||
import { LoggerService } from '../../../../services/logger/logger.service';
|
||||
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
|
||||
|
||||
/**
|
||||
* The **RecordingActivityComponent** is the component that allows showing the recording activity.
|
||||
@ -24,13 +25,14 @@ import { LoggerService } from '../../../../services/logger/logger.service';
|
||||
selector: 'ov-recording-activity',
|
||||
templateUrl: './recording-activity.component.html',
|
||||
styleUrls: ['./recording-activity.component.scss', '../activities-panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
|
||||
// TODO: Allow to add more than one recording type
|
||||
// TODO: Allow to choose where the recording is stored (s3, google cloud, etc)
|
||||
// TODO: Allow to choose the layout of the recording
|
||||
export class RecordingActivityComponent implements OnInit {
|
||||
export class RecordingActivityComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -66,6 +68,20 @@ export class RecordingActivityComponent implements OnInit {
|
||||
*/
|
||||
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Provides event notifications that fire when view recordings button has been clicked.
|
||||
* This event is triggered when the user wants to view all recordings in an external page.
|
||||
*/
|
||||
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* This event is fired when the user clicks on the view recording button.
|
||||
* It provides the recording ID as event data.
|
||||
*/
|
||||
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -98,12 +114,53 @@ export class RecordingActivityComponent implements OnInit {
|
||||
*/
|
||||
recordingError: any;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
hasRoomTracksPublished: boolean = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
mouseHovering: boolean = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isReadOnlyMode: boolean = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
viewButtonText: string = 'PANEL.RECORDING.VIEW';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
showStartStopRecordingButton: boolean = true;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
showViewRecordingsButton: boolean = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
showRecordingList: boolean = true; // Controls visibility of the recording list in the panel
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
showControls: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean } = {
|
||||
play: true,
|
||||
download: true,
|
||||
delete: true,
|
||||
externalView: false
|
||||
};
|
||||
|
||||
private log: ILogger;
|
||||
private recordingStatusSubscription: Subscription;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -114,7 +171,8 @@ export class RecordingActivityComponent implements OnInit {
|
||||
private actionService: ActionService,
|
||||
private openviduService: OpenViduService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private loggerSrv: LoggerService
|
||||
private loggerSrv: LoggerService,
|
||||
private libService: OpenViduComponentsConfigService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('RecordingActivityComponent');
|
||||
}
|
||||
@ -124,13 +182,23 @@ export class RecordingActivityComponent implements OnInit {
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.subscribeToRecordingStatus();
|
||||
this.subscribeToTracksChanges();
|
||||
this.subscribeToConfigChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
if (this.recordingStatusSubscription) this.recordingStatusSubscription.unsubscribe();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trackByRecordingId(index: number, recording: RecordingInfo): string | undefined {
|
||||
return recording.id;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -225,8 +293,97 @@ export class RecordingActivityComponent implements OnInit {
|
||||
this.recordingService.playRecording(recording);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
viewRecording(recording: RecordingInfo) {
|
||||
// This method can be overridden or emit a custom event for navigation
|
||||
// For now, it uses the same behavior as play, but can be customized
|
||||
if (!recording.filename) {
|
||||
this.log.e('Error viewing recording. Recording filename is undefined');
|
||||
return;
|
||||
}
|
||||
const payload: RecordingPlayClickedEvent = {
|
||||
roomName: this.openviduService.getRoomName(),
|
||||
recordingId: recording.id
|
||||
};
|
||||
this.onRecordingPlayClicked.emit(payload);
|
||||
// You can customize this to navigate to a different page instead
|
||||
this.recordingService.playRecording(recording);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
viewAllRecordings() {
|
||||
this.onViewRecordingsClicked.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Format duration in seconds to a readable format (e.g., "2m 30s")
|
||||
*/
|
||||
formatDuration(seconds: number): string {
|
||||
if (!seconds || seconds < 0) return '0s';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
} else {
|
||||
return `${remainingSeconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Format file size in bytes to a readable format (e.g., "2.5 MB")
|
||||
*/
|
||||
formatFileSize(bytes: number): string {
|
||||
if (!bytes || bytes < 0) return '0 B';
|
||||
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const size = bytes / Math.pow(1024, i);
|
||||
|
||||
return `${size.toFixed(1)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
private subscribeToConfigChanges() {
|
||||
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => {
|
||||
this.isReadOnlyMode = readOnly;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.recordingActivityShowControls$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((controls: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean }) => {
|
||||
this.showControls = controls;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.recordingActivityStartStopRecordingButton$.pipe(takeUntil(this.destroy$)).subscribe((show: boolean) => {
|
||||
this.showStartStopRecordingButton = show;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.recordingActivityViewRecordingsButton$.pipe(takeUntil(this.destroy$)).subscribe((show: boolean) => {
|
||||
this.showViewRecordingsButton = show;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.recordingActivityShowRecordingsList$.pipe(takeUntil(this.destroy$)).subscribe((show: boolean) => {
|
||||
this.showRecordingList = show;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeToRecordingStatus() {
|
||||
this.recordingStatusSubscription = this.recordingService.recordingStatusObs.subscribe((event: RecordingStatusInfo) => {
|
||||
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
|
||||
const { status, recordingList, error } = event;
|
||||
this.recordingStatus = status;
|
||||
this.recordingList = recordingList;
|
||||
@ -238,4 +395,24 @@ export class RecordingActivityComponent implements OnInit {
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeToTracksChanges() {
|
||||
this.hasRoomTracksPublished = this.openviduService.hasRoomTracksPublished();
|
||||
|
||||
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
const newValue = this.openviduService.hasRoomTracksPublished();
|
||||
if (this.hasRoomTracksPublished !== newValue) {
|
||||
this.hasRoomTracksPublished = newValue;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.participantService.remoteParticipants$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
const newValue = this.openviduService.hasRoomTracksPublished();
|
||||
if (this.hasRoomTracksPublished !== newValue) {
|
||||
this.hasRoomTracksPublished = newValue;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,24 @@
|
||||
<div class="panel-container" id="background-effects-container">
|
||||
<div class="panel-header-container">
|
||||
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
|
||||
<div class="panel-container" id="background-effects-container" [class.prejoin-mode]="mode === 'prejoin'">
|
||||
@if (mode === 'meeting') {
|
||||
<div class="panel-header-container">
|
||||
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
|
||||
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isVirtualBackgroundSupported()) {
|
||||
<div class="not-supported-message">
|
||||
<p class="warning-title">{{ 'PANEL.BACKGROUND.NOT_SUPPORTED' | translate }}</p>
|
||||
<p class="warning-description">{{ 'PANEL.BACKGROUND.NOT_SUPPORTED_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
|
||||
<div>
|
||||
@ -17,6 +31,7 @@
|
||||
[class.active-effect-btn]="backgroundSelectedId === effect.id"
|
||||
(click)="applyBackground(effect)"
|
||||
[attr.id]="effect.id + '-btn'"
|
||||
[disabled]="!isVirtualBackgroundSupported()"
|
||||
[matTooltip]="
|
||||
effect.type === effectType.NONE
|
||||
? ('PANEL.BACKGROUND.NO_EFFECTS' | translate)
|
||||
@ -37,7 +52,8 @@
|
||||
class="effect-button"
|
||||
[id]="'effect-' + effect.id"
|
||||
[class.active-effect-btn]="backgroundSelectedId === effect.id"
|
||||
(click)="applyBackground(effect)"
|
||||
[class.disabled]="!isVirtualBackgroundSupported()"
|
||||
(click)="isVirtualBackgroundSupported() && applyBackground(effect)"
|
||||
>
|
||||
<img [src]="effect.thumbnail" />
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,37 @@
|
||||
.prejoin-mode {
|
||||
margin: 0 10px 0px 10px;
|
||||
max-height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.panel-close-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.background-title {
|
||||
color: var(--ov-text-surface-color);
|
||||
margin: 10px 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.not-supported-message {
|
||||
padding: 15px;
|
||||
margin: 10px;
|
||||
background-color: var(--ov-warn-color, #ff9800);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
color: var(--ov-text-surface-color);
|
||||
|
||||
.warning-title {
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.warning-description {
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.effects-container {
|
||||
display: block !important;
|
||||
overflow-y: auto;
|
||||
@ -12,7 +43,7 @@
|
||||
margin: 5px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
background-color: var(--ov-secondary-action-color);
|
||||
color: var(--ov-primary-action-color);
|
||||
color: var(--ov-text-surface-color);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
line-height: inherit;
|
||||
@ -22,6 +53,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.effect-button.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.active-effect-btn {
|
||||
border: 2px solid var(--ov-accent-action-color);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, EventEmitter, Input, OnInit, Output, Signal } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
|
||||
import { PanelType } from '../../../models/panel.model';
|
||||
@ -12,14 +12,18 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
|
||||
selector: 'ov-background-effects-panel',
|
||||
templateUrl: './background-effects-panel.component.html',
|
||||
styleUrls: ['../panel.component.scss', './background-effects-panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class BackgroundEffectsPanelComponent implements OnInit {
|
||||
@Input() mode: 'prejoin' | 'meeting' = 'meeting';
|
||||
@Output() onClose = new EventEmitter<void>();
|
||||
|
||||
backgroundSelectedId: string;
|
||||
effectType = EffectType;
|
||||
backgroundImages: BackgroundEffect[] = [];
|
||||
noEffectAndBlurredBackground: BackgroundEffect[] = [];
|
||||
private backgrounds: BackgroundEffect[];
|
||||
private backgrounds: BackgroundEffect[] = [];
|
||||
private backgroundSubs: Subscription;
|
||||
|
||||
/**
|
||||
@ -34,6 +38,14 @@ export class BackgroundEffectsPanelComponent implements OnInit {
|
||||
private cd: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Computed signal that reactively tracks if virtual background is supported.
|
||||
* Updates automatically when browser support changes.
|
||||
*/
|
||||
readonly isVirtualBackgroundSupported: Signal<boolean> = computed(() =>
|
||||
this.backgroundService.isVirtualBackgroundSupported()
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscribeToBackgroundSelected();
|
||||
this.backgrounds = this.backgroundService.getBackgrounds();
|
||||
@ -52,14 +64,14 @@ export class BackgroundEffectsPanelComponent implements OnInit {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
|
||||
if (this.mode === 'prejoin') {
|
||||
this.onClose.emit();
|
||||
} else {
|
||||
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
|
||||
}
|
||||
}
|
||||
|
||||
async applyBackground(effect: BackgroundEffect) {
|
||||
if (effect.type === EffectType.NONE) {
|
||||
await this.backgroundService.removeBackground();
|
||||
} else {
|
||||
await this.backgroundService.applyBackground(effect);
|
||||
}
|
||||
await this.backgroundService.applyBackground(effect);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,13 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (hasEncryptionKeyMismatch()) {
|
||||
<div class="encryption-warning">
|
||||
<mat-icon>warning</mat-icon>
|
||||
<p>{{ 'PANEL.CHAT.ENCRYPTION_KEY_MISMATCH' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="text-container">
|
||||
<p class="text-info">{{ 'PANEL.CHAT.SUBTITLE' | translate }}</p>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,28 @@
|
||||
$ov-selection-color: #d4d6d7;
|
||||
.encryption-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
margin: 10px;
|
||||
background-color: rgba(255, 152, 0, 0.1);
|
||||
border: 1px solid rgba(255, 152, 0, 0.5);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
color: var(--ov-warn-color, #ff9800);
|
||||
font-size: 13px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.text-container {
|
||||
color: var(--ov-text-primary-color);
|
||||
@ -29,15 +53,14 @@ $ov-selection-color: #d4d6d7;
|
||||
.input-container {
|
||||
height: 65px;
|
||||
display: flex;
|
||||
background-color: var(--ov-surface-color);
|
||||
border: 1px solid $ov-selection-color;
|
||||
border: 1px solid var(--ov-secondary-action-color);
|
||||
padding: 10px 5px 10px 10px;
|
||||
margin: 10px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
order: 3;
|
||||
justify-content: space-evenly;
|
||||
align-items: none;
|
||||
|
||||
color: var(--ov-text-surface-color);
|
||||
}
|
||||
|
||||
.input-container textarea {
|
||||
@ -59,6 +82,10 @@ $ov-selection-color: #d4d6d7;
|
||||
box-shadow: none;
|
||||
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
|
||||
color: var(--ov-text-surface-color);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ov-text-surface-color);
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
@ -87,18 +114,18 @@ $ov-selection-color: #d4d6d7;
|
||||
position: relative;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
padding: 8px;
|
||||
color: var(--ov-secondary-action-color);
|
||||
color: var(--ov-text-surface-color);
|
||||
width: auto;
|
||||
max-width: 95%;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
background-color: var(--ov-primary-action-color);
|
||||
background-color: var(--ov-secondary-action-color);
|
||||
}
|
||||
|
||||
#send-btn {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
color: var(--ov-secondary-action-color);
|
||||
background-color: var(--ov-primary-action-color);
|
||||
color: var(--ov-text-primary-color);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
align-self: center;
|
||||
height: 75px;
|
||||
}
|
||||
@ -126,5 +153,6 @@ $ov-selection-color: #d4d6d7;
|
||||
}
|
||||
|
||||
::ng-deep a:-webkit-any-link {
|
||||
color: var(--ov-accent-action-color);
|
||||
color: #37a1f8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ChatMessage } from '../../../models/chat.model';
|
||||
import { PanelType } from '../../../models/panel.model';
|
||||
import { ChatService } from '../../../services/chat/chat.service';
|
||||
import { E2eeService } from '../../../services/e2ee/e2ee.service';
|
||||
import { PanelService } from '../../../services/panel/panel.service';
|
||||
import { ParticipantService } from '../../../services/participant/participant.service';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -13,27 +15,28 @@ import { PanelService } from '../../../services/panel/panel.service';
|
||||
selector: 'ov-chat-panel',
|
||||
templateUrl: './chat-panel.component.html',
|
||||
styleUrls: ['../panel.component.scss', './chat-panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class ChatPanelComponent implements OnInit, AfterViewInit {
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ViewChild('chatScroll') chatScroll: ElementRef;
|
||||
@ViewChild('chatScroll') chatScroll: ElementRef = new ElementRef(null);
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ViewChild('chatInput') chatInput: ElementRef;
|
||||
@ViewChild('chatInput') chatInput: ElementRef = new ElementRef(null);
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
message: string;
|
||||
message: string = '';
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
messageList: ChatMessage[] = [];
|
||||
|
||||
private chatMessageSubscription: Subscription;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
@ -41,8 +44,10 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
|
||||
constructor(
|
||||
private chatService: ChatService,
|
||||
private panelService: PanelService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {}
|
||||
private cd: ChangeDetectorRef,
|
||||
private e2eeService: E2eeService,
|
||||
private participantService: ParticipantService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
@ -65,13 +70,14 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
|
||||
* @ignore
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.chatMessageSubscription) this.chatMessageSubscription.unsubscribe();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
eventKeyPress(event) {
|
||||
eventKeyPress(event: KeyboardEvent): void {
|
||||
// Pressed 'Enter' key
|
||||
if (event && event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
@ -107,8 +113,21 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
|
||||
this.panelService.togglePanel(PanelType.CHAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
hasEncryptionKeyMismatch = computed(() => {
|
||||
if (!this.e2eeService.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
const remoteParticipants = this.participantService.remoteParticipantsSignal();
|
||||
return remoteParticipants.some(p => p.hasEncryptionError);
|
||||
});
|
||||
|
||||
|
||||
|
||||
private subscribeToMessages() {
|
||||
this.chatMessageSubscription = this.chatService.messagesObs.subscribe((messages: ChatMessage[]) => {
|
||||
this.chatService.chatMessages$.pipe(takeUntil(this.destroy$)).subscribe((messages: ChatMessage[]) => {
|
||||
this.messageList = messages;
|
||||
if (this.panelService.isChatPanelOpened()) {
|
||||
this.scrollToBottom();
|
||||
|
||||
@ -9,6 +9,16 @@
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
// Mobile responsiveness - ensure panels occupy full width on mobile devices
|
||||
@media only screen and (max-width: 768px) {
|
||||
.panel-container {
|
||||
margin: 8px;
|
||||
max-height: calc(100% - 16px);
|
||||
min-height: calc(100% - 16px);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-header-container {
|
||||
padding: 10px;
|
||||
flex: inherit;
|
||||
@ -37,12 +47,12 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #a7a7a7;
|
||||
background: var(--ov-selection-color-btn);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #7c7c7c;
|
||||
background: var(--ov-text-disabled-color);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
Output,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import { skip, Subscription } from 'rxjs';
|
||||
import { skip, Subject, takeUntil } from 'rxjs';
|
||||
import {
|
||||
ActivitiesPanelDirective,
|
||||
AdditionalPanelsDirective,
|
||||
@ -25,6 +25,7 @@ import {
|
||||
} from '../../models/panel.model';
|
||||
import { PanelService } from '../../services/panel/panel.service';
|
||||
import { BackgroundEffect } from '../../models/background-effect.model';
|
||||
import { TemplateManagerService, PanelTemplateConfiguration } from '../../services/template/template-manager.service';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -37,7 +38,8 @@ import { BackgroundEffect } from '../../models/background-effect.model';
|
||||
selector: 'ov-panel',
|
||||
templateUrl: './panel.component.html',
|
||||
styleUrls: ['./panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class PanelComponent implements OnInit {
|
||||
/**
|
||||
@ -74,42 +76,20 @@ export class PanelComponent implements OnInit {
|
||||
*/
|
||||
@ContentChild(ParticipantsPanelDirective)
|
||||
set externalParticipantPanel(externalParticipantsPanel: ParticipantsPanelDirective) {
|
||||
// This directive will has value only when PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
|
||||
// is inside of the PANEL component tagged with '*ovPanel'
|
||||
this._externalParticipantPanel = externalParticipantsPanel;
|
||||
if (externalParticipantsPanel) {
|
||||
this.participantsPanelTemplate = externalParticipantsPanel.template;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: backgroundEffectsPanel does not provides customization
|
||||
// @ContentChild(BackgroundEffectsPanelDirective)
|
||||
// set externalBackgroundEffectsPanel(externalBackgroundEffectsPanel: BackgroundEffectsPanelDirective) {
|
||||
// This directive will has value only when BACKGROUND EFFECTS PANEL component tagged with '*ovBackgroundEffectsPanel'
|
||||
// is inside of the PANEL component tagged with '*ovPanel'
|
||||
// if (externalBackgroundEffectsPanel) {
|
||||
// this.backgroundEffectsPanelTemplate = externalBackgroundEffectsPanel.template;
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO: settingsPanel does not provides customization
|
||||
// @ContentChild(SettingsPanelDirective)
|
||||
// set externalSettingsPanel(externalSettingsPanel: SettingsPanelDirective) {
|
||||
// This directive will has value only when SETTINGS PANEL component tagged with '*ovSettingsPanel'
|
||||
// is inside of the PANEL component tagged with '*ovPanel'
|
||||
// if (externalSettingsPanel) {
|
||||
// this.settingsPanelTemplate = externalSettingsPanel.template;
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ContentChild(ActivitiesPanelDirective)
|
||||
set externalActivitiesPanel(externalActivitiesPanel: ActivitiesPanelDirective) {
|
||||
// This directive will has value only when ACTIVITIES PANEL component tagged with '*ovActivitiesPanel'
|
||||
// is inside of the PANEL component tagged with '*ovPanel'
|
||||
this._externalActivitiesPanel = externalActivitiesPanel;
|
||||
if (externalActivitiesPanel) {
|
||||
this.activitiesPanelTemplate = externalActivitiesPanel.template;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,10 +98,9 @@ export class PanelComponent implements OnInit {
|
||||
*/
|
||||
@ContentChild(ChatPanelDirective)
|
||||
set externalChatPanel(externalChatPanel: ChatPanelDirective) {
|
||||
// This directive will has value only when CHAT PANEL component tagged with '*ovChatPanel'
|
||||
// is inside of the PANEL component tagged with '*ovPanel'
|
||||
this._externalChatPanel = externalChatPanel;
|
||||
if (externalChatPanel) {
|
||||
this.chatPanelTemplate = externalChatPanel.template;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,10 +109,9 @@ export class PanelComponent implements OnInit {
|
||||
*/
|
||||
@ContentChild(AdditionalPanelsDirective)
|
||||
set externalAdditionalPanels(externalAdditionalPanels: AdditionalPanelsDirective) {
|
||||
// This directive will has value only when ADDITIONAL PANELS component tagged with '*ovPanelAdditionalPanels'
|
||||
// is inside of the PANEL component tagged with '*ovPanel'
|
||||
this._externalAdditionalPanels = externalAdditionalPanels;
|
||||
if (externalAdditionalPanels) {
|
||||
this.additionalPanelsTemplate = externalAdditionalPanels.template;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,7 +172,20 @@ export class PanelComponent implements OnInit {
|
||||
* @internal
|
||||
*/
|
||||
isExternalPanelOpened: boolean;
|
||||
private panelSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Template configuration managed by the service
|
||||
*/
|
||||
templateConfig: PanelTemplateConfiguration = {};
|
||||
|
||||
// Store directive references for template setup
|
||||
private _externalParticipantPanel?: ParticipantsPanelDirective;
|
||||
private _externalChatPanel?: ChatPanelDirective;
|
||||
private _externalActivitiesPanel?: ActivitiesPanelDirective;
|
||||
private _externalAdditionalPanels?: AdditionalPanelsDirective;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private panelEmitersHandler: Map<
|
||||
PanelType,
|
||||
@ -206,30 +197,78 @@ export class PanelComponent implements OnInit {
|
||||
*/
|
||||
constructor(
|
||||
private panelService: PanelService,
|
||||
private cd: ChangeDetectorRef
|
||||
private cd: ChangeDetectorRef,
|
||||
private templateManagerService: TemplateManagerService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.setupTemplates();
|
||||
this.subscribeToPanelToggling();
|
||||
this.panelEmitersHandler.set(PanelType.CHAT, this.onChatPanelStatusChanged);
|
||||
this.panelEmitersHandler.set(PanelType.PARTICIPANTS, this.onParticipantsPanelStatusChanged);
|
||||
this.panelEmitersHandler.set(PanelType.SETTINGS, this.onSettingsPanelStatusChanged);
|
||||
this.panelEmitersHandler.set(PanelType.ACTIVITIES, this.onActivitiesPanelStatusChanged);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Sets up all templates using the template manager service
|
||||
*/
|
||||
private setupTemplates(): void {
|
||||
this.templateConfig = this.templateManagerService.setupPanelTemplates(
|
||||
this._externalParticipantPanel,
|
||||
this._externalChatPanel,
|
||||
this._externalActivitiesPanel,
|
||||
this._externalAdditionalPanels
|
||||
);
|
||||
|
||||
// Apply templates to component properties for backward compatibility
|
||||
this.applyTemplateConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Applies the template configuration to component properties
|
||||
*/
|
||||
private applyTemplateConfiguration(): void {
|
||||
if (this.templateConfig.participantsPanelTemplate) {
|
||||
this.participantsPanelTemplate = this.templateConfig.participantsPanelTemplate;
|
||||
}
|
||||
if (this.templateConfig.chatPanelTemplate) {
|
||||
this.chatPanelTemplate = this.templateConfig.chatPanelTemplate;
|
||||
}
|
||||
if (this.templateConfig.activitiesPanelTemplate) {
|
||||
this.activitiesPanelTemplate = this.templateConfig.activitiesPanelTemplate;
|
||||
}
|
||||
if (this.templateConfig.additionalPanelsTemplate) {
|
||||
this.additionalPanelsTemplate = this.templateConfig.additionalPanelsTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Updates templates and triggers change detection
|
||||
*/
|
||||
private updateTemplatesAndMarkForCheck(): void {
|
||||
this.setupTemplates();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.isChatPanelOpened = false;
|
||||
this.isParticipantsPanelOpened = false;
|
||||
if (this.panelSubscription) this.panelSubscription.unsubscribe();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private subscribeToPanelToggling() {
|
||||
this.panelSubscription = this.panelService.panelStatusObs.pipe(skip(1)).subscribe((ev: PanelStatusInfo) => {
|
||||
this.panelService.panelStatusObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
|
||||
this.isChatPanelOpened = ev.isOpened && ev.panelType === PanelType.CHAT;
|
||||
this.isParticipantsPanelOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
|
||||
this.isBackgroundEffectsPanelOpened = ev.isOpened && ev.panelType === PanelType.BACKGROUND_EFFECTS;
|
||||
|
||||
@ -1,33 +1,75 @@
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
<div matListItemIcon class="participant-avatar" [style.background-color]="_participant.colorProfile">
|
||||
<mat-icon>person</mat-icon>
|
||||
</div>
|
||||
<h3 matListItemTitle class="participant-name">{{ _participant.name }}
|
||||
<span *ngIf="_participant.isLocal"> ({{ 'PANEL.PARTICIPANTS.YOU' | translate }})</span>
|
||||
</h3>
|
||||
<p matListItemLine class="participant-subtitle">{{ _participant | tracksPublishedTypes }}</p>
|
||||
<!-- <p matListItemLine>
|
||||
<span class="participant-subtitle"></span>
|
||||
</p> -->
|
||||
|
||||
<div class="participant-action-buttons" matListItemMeta>
|
||||
<button
|
||||
mat-icon-button
|
||||
id="mute-btn"
|
||||
*ngIf="!_participant.isLocal && showMuteButton"
|
||||
[class.warn-btn]="_participant.isMutedForcibly"
|
||||
(click)="toggleMuteForcibly()"
|
||||
[disableRipple]="true"
|
||||
<!-- Main participant container with improved structure -->
|
||||
<div class="participant-container" [attr.data-participant-id]="_participant?.sid" [attr.data-participant-name]="participantDisplayName">
|
||||
<!-- Avatar section with dynamic color -->
|
||||
<div
|
||||
class="participant-avatar"
|
||||
[style.background-color]="_participant?.colorProfile"
|
||||
[attr.aria-label]="'Avatar for ' + participantDisplayName"
|
||||
>
|
||||
<mat-icon *ngIf="!_participant.isMutedForcibly">volume_up</mat-icon>
|
||||
<mat-icon *ngIf="_participant.isMutedForcibly">volume_off</mat-icon>
|
||||
</button>
|
||||
<mat-icon>{{ _participant.hasEncryptionError ? 'lock_person' : 'person' }}</mat-icon>
|
||||
</div>
|
||||
|
||||
<!-- External item elements -->
|
||||
<ng-container *ngIf="participantPanelItemElementsTemplate">
|
||||
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
||||
</ng-container>
|
||||
<!-- Content section with name and status -->
|
||||
<div class="participant-content">
|
||||
<!-- Name row with better space management -->
|
||||
<div class="participant-name-row">
|
||||
<div class="participant-name">
|
||||
<span class="participant-name-text">{{ participantDisplayName }}</span>
|
||||
<span *ngIf="isLocalParticipant" class="local-indicator">
|
||||
{{ 'PANEL.PARTICIPANTS.YOU' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Participant badges in separate container -->
|
||||
<div class="participant-badges">
|
||||
<ng-container *ngTemplateOutlet="participantBadgeTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!_participant.hasEncryptionError) {
|
||||
<div class="participant-subtitle">
|
||||
<span class="status-indicator">
|
||||
{{ _participant | tracksPublishedTypes }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Action buttons section -->
|
||||
@if (!_participant.hasEncryptionError) {
|
||||
<div class="participant-action-buttons">
|
||||
<!-- Mute/Unmute button for remote participants -->
|
||||
<button
|
||||
mat-icon-button
|
||||
id="mute-btn"
|
||||
*ngIf="!isLocalParticipant && showMuteButton"
|
||||
[class.warn-btn]="_participant?.isMutedForcibly"
|
||||
(click)="toggleMuteForcibly()"
|
||||
[disabled]="!_participant"
|
||||
[disableRipple]="true"
|
||||
[attr.aria-label]="
|
||||
_participant?.isMutedForcibly
|
||||
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
|
||||
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
||||
"
|
||||
[matTooltip]="
|
||||
_participant?.isMutedForcibly
|
||||
? ('PANEL.PARTICIPANTS.UNMUTE' | translate)
|
||||
: ('PANEL.PARTICIPANTS.MUTE' | translate)
|
||||
"
|
||||
>
|
||||
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
|
||||
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- External item elements with improved structure -->
|
||||
<div class="external-elements" *ngIf="hasExternalElements">
|
||||
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,23 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ParticipantPanelItemElementsDirective } from '../../../../directives/template/openvidu-components-angular.directive';
|
||||
import { ParticipantPanelParticipantBadgeDirective } from '../../../../directives/template/internals.directive';
|
||||
import { ParticipantModel } from '../../../../models/participant.model';
|
||||
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
|
||||
import { ParticipantService } from '../../../../services/participant/participant.service';
|
||||
import { TemplateManagerService, ParticipantPanelItemTemplateConfiguration } from '../../../../services/template/template-manager.service';
|
||||
|
||||
/**
|
||||
*
|
||||
* The **ParticipantPanelItemComponent** is hosted inside of the {@link ParticipantsPanelComponent}.
|
||||
* It is in charge of displaying the participants information inside of the ParticipansPanelComponent.
|
||||
* It displays participant information with enhanced UI/UX, including support for custom content
|
||||
* injection through structural directives.
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ov-participant-panel-item',
|
||||
templateUrl: './participant-panel-item.component.html',
|
||||
styleUrls: ['./participant-panel-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
@ -34,40 +36,69 @@ export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@ContentChild(ParticipantPanelItemElementsDirective)
|
||||
set externalItemElements(externalItemElements: ParticipantPanelItemElementsDirective) {
|
||||
// This directive will has value only when ITEM ELEMENTS component tagget with '*ovParticipantPanelItemElements' directive
|
||||
// is inside of the P PANEL ITEM component tagged with '*ovParticipantPanelItem' directive
|
||||
this._externalItemElements = externalItemElements;
|
||||
if (externalItemElements) {
|
||||
this.participantPanelItemElementsTemplate = externalItemElements.template;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The participant to be displayed
|
||||
* @ignore
|
||||
*/
|
||||
@ContentChild(ParticipantPanelParticipantBadgeDirective)
|
||||
set externalParticipantBadge(participantBadge: ParticipantPanelParticipantBadgeDirective) {
|
||||
this._externalParticipantBadge = participantBadge;
|
||||
if (participantBadge) {
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Template configuration managed by the service
|
||||
*/
|
||||
templateConfig: ParticipantPanelItemTemplateConfiguration = {};
|
||||
|
||||
// Store directive references for template setup
|
||||
private _externalItemElements?: ParticipantPanelItemElementsDirective;
|
||||
private _externalParticipantBadge?: ParticipantPanelParticipantBadgeDirective;
|
||||
|
||||
/**
|
||||
* The participant to be displayed
|
||||
*/
|
||||
@Input()
|
||||
set participant(participant: ParticipantModel) {
|
||||
this._participant = participant;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @internal
|
||||
* Current participant being displayed
|
||||
*/
|
||||
_participant: ParticipantModel;
|
||||
|
||||
/**
|
||||
* Whether to show the mute button for remote participants
|
||||
*/
|
||||
@Input()
|
||||
muteButton: boolean = true;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
constructor(
|
||||
private libService: OpenViduComponentsConfigService,
|
||||
private participantService: ParticipantService,
|
||||
private cd: ChangeDetectorRef
|
||||
private cd: ChangeDetectorRef,
|
||||
private templateManagerService: TemplateManagerService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.setupTemplates();
|
||||
this.subscribeToParticipantPanelItemDirectives();
|
||||
}
|
||||
|
||||
@ -79,14 +110,72 @@ export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* Toggles the mute state of a remote participant
|
||||
*/
|
||||
toggleMuteForcibly() {
|
||||
if (this._participant) {
|
||||
if (this._participant && !this._participant.isLocal) {
|
||||
this.participantService.setRemoteMutedForcibly(this._participant.sid, !this._participant.isMutedForcibly);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template for local participant badge
|
||||
*/
|
||||
get participantBadgeTemplate(): TemplateRef<any> | undefined {
|
||||
return this._externalParticipantBadge?.template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current participant is the local participant
|
||||
*/
|
||||
get isLocalParticipant(): boolean {
|
||||
return this._participant?.isLocal || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the participant's display name
|
||||
*/
|
||||
get participantDisplayName(): string {
|
||||
return this._participant?.name || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if external elements are available
|
||||
*/
|
||||
get hasExternalElements(): boolean {
|
||||
return !!this.participantPanelItemElementsTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Sets up all templates using the template manager service
|
||||
*/
|
||||
private setupTemplates(): void {
|
||||
this.templateConfig = this.templateManagerService.setupParticipantPanelItemTemplates(this._externalItemElements);
|
||||
|
||||
// Apply templates to component properties for backward compatibility
|
||||
this.applyTemplateConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Applies the template configuration to component properties
|
||||
*/
|
||||
private applyTemplateConfiguration(): void {
|
||||
if (this.templateConfig.participantPanelItemElementsTemplate) {
|
||||
this.participantPanelItemElementsTemplate = this.templateConfig.participantPanelItemElementsTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Updates templates and triggers change detection
|
||||
*/
|
||||
private updateTemplatesAndMarkForCheck(): void {
|
||||
this.setupTemplates();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private subscribeToParticipantPanelItemDirectives() {
|
||||
this.muteButtonSub = this.libService.participantItemMuteButton$.subscribe((value: boolean) => {
|
||||
this.showMuteButton = value;
|
||||
|
||||
@ -7,14 +7,13 @@
|
||||
</div>
|
||||
|
||||
<div class="scrollable">
|
||||
|
||||
<div class="local-participant-container" *ngIf="localParticipant">
|
||||
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: localParticipant }"></ng-container>
|
||||
<mat-divider *ngIf="true"></mat-divider>
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="participantPanelAfterLocalParticipantTemplate"></ng-container>
|
||||
|
||||
<div class="remote-participants-container" id="remote-participants-container" *ngIf="remoteParticipants.length > 0">
|
||||
|
||||
<div *ngFor="let participant of this.remoteParticipants" id="remote-participant-item">
|
||||
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: participant }"></ng-container>
|
||||
</div>
|
||||
|
||||
@ -13,8 +13,10 @@ import {
|
||||
import { ParticipantService } from '../../../../services/participant/participant.service';
|
||||
import { PanelService } from '../../../../services/panel/panel.service';
|
||||
import { ParticipantPanelItemDirective } from '../../../../directives/template/openvidu-components-angular.directive';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ParticipantModel } from '../../../../models/participant.model';
|
||||
import { TemplateManagerService, ParticipantsPanelTemplateConfiguration } from '../../../../services/template/template-manager.service';
|
||||
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
|
||||
|
||||
/**
|
||||
* The **ParticipantsPanelComponent** is hosted inside of the {@link PanelComponent}.
|
||||
@ -25,7 +27,8 @@ import { ParticipantModel } from '../../../../models/participant.model';
|
||||
selector: 'ov-participants-panel',
|
||||
templateUrl: './participants-panel.component.html',
|
||||
styleUrls: ['../../panel.component.scss', './participants-panel.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
/**
|
||||
@ -47,20 +50,33 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
|
||||
*/
|
||||
@ContentChild('participantPanelItem', { read: TemplateRef }) participantPanelItemTemplate: TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ContentChild('participantPanelAfterLocalParticipant', { read: TemplateRef })
|
||||
participantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@ContentChild(ParticipantPanelItemDirective)
|
||||
set externalParticipantPanelItem(externalParticipantPanelItem: ParticipantPanelItemDirective) {
|
||||
// This directive will has value only when PARTICIPANT PANEL ITEM component tagged with '*ovParticipantPanelItem'
|
||||
// is inside of the PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
|
||||
this._externalParticipantPanelItem = externalParticipantPanelItem;
|
||||
if (externalParticipantPanelItem) {
|
||||
this.participantPanelItemTemplate = externalParticipantPanelItem.template;
|
||||
this.updateTemplatesAndMarkForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
private localParticipantSubs: Subscription;
|
||||
private remoteParticipantsSubs: Subscription;
|
||||
/**
|
||||
* @internal
|
||||
* Template configuration managed by the service
|
||||
*/
|
||||
templateConfig: ParticipantsPanelTemplateConfiguration = {};
|
||||
|
||||
// Store directive references for template setup
|
||||
private _externalParticipantPanelItem?: ParticipantPanelItemDirective;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
@ -68,32 +84,26 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
|
||||
constructor(
|
||||
private participantService: ParticipantService,
|
||||
private panelService: PanelService,
|
||||
private cd: ChangeDetectorRef
|
||||
private cd: ChangeDetectorRef,
|
||||
private templateManagerService: TemplateManagerService,
|
||||
private libService: OpenViduComponentsConfigService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.localParticipantSubs = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
|
||||
if (p) {
|
||||
this.localParticipant = p;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
this.setupTemplates();
|
||||
|
||||
this.remoteParticipantsSubs = this.participantService.remoteParticipants$.subscribe((p: ParticipantModel[]) => {
|
||||
this.remoteParticipants = p;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.subscribeToParticipantsChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
if (this.localParticipantSubs) this.localParticipantSubs.unsubscribe();
|
||||
if (this.remoteParticipantsSubs) this.remoteParticipantsSubs.unsubscribe;
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,6 +118,57 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToParticipantsChanges() {
|
||||
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p: ParticipantModel | undefined) => {
|
||||
if (p) {
|
||||
this.localParticipant = p;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.participantService.remoteParticipants$.pipe(takeUntil(this.destroy$)).subscribe((p: ParticipantModel[]) => {
|
||||
this.remoteParticipants = p;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Sets up all templates using the template manager service
|
||||
*/
|
||||
private setupTemplates(): void {
|
||||
this.templateConfig = this.templateManagerService.setupParticipantsPanelTemplates(
|
||||
this._externalParticipantPanelItem,
|
||||
this.defaultParticipantPanelItemTemplate
|
||||
);
|
||||
|
||||
// Apply templates to component properties for backward compatibility
|
||||
this.applyTemplateConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Applies the template configuration to component properties
|
||||
*/
|
||||
private applyTemplateConfiguration(): void {
|
||||
if (this.templateConfig.participantPanelItemTemplate) {
|
||||
this.participantPanelItemTemplate = this.templateConfig.participantPanelItemTemplate;
|
||||
}
|
||||
if (this.templateConfig.participantPanelAfterLocalParticipantTemplate) {
|
||||
this.participantPanelAfterLocalParticipantTemplate = this.templateConfig.participantPanelAfterLocalParticipantTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Updates templates and triggers change detection
|
||||
*/
|
||||
private updateTemplatesAndMarkForCheck(): void {
|
||||
this.setupTemplates();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="panel-container" id="settings-container">
|
||||
<div class="panel-container" id="settings-container" [class.vertical-layout]="isVerticalLayout" [class.compact-view]="isCompactView">
|
||||
<div class="panel-header-container">
|
||||
<h3 class="panel-title">{{ 'PANEL.SETTINGS.TITLE' | translate }}</h3>
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
@ -6,8 +6,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-container">
|
||||
<div class="item-menu" [ngClass]="{ mobile: isMobile }">
|
||||
<div class="settings-container" [class.vertical-layout]="isVerticalLayout">
|
||||
<div class="item-menu" [class.compact]="isCompactView" [class.icons-only]="shouldHideMenuText">
|
||||
<mat-selection-list
|
||||
#optionList
|
||||
(selectionChange)="onSelectionChanged(optionList.selectedOptions.selected[0]?.value)"
|
||||
@ -20,27 +20,35 @@
|
||||
id="general-opt"
|
||||
[selected]="selectedOption === settingsOptions.GENERAL"
|
||||
[value]="settingsOptions.GENERAL"
|
||||
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.GENERAL' | translate) : '' }}"
|
||||
[matTooltipDisabled]="!shouldHideMenuText"
|
||||
>
|
||||
<mat-icon matListItemIcon>manage_accounts</mat-icon>
|
||||
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
|
||||
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
|
||||
</mat-list-option>
|
||||
<mat-list-option
|
||||
*ngIf="showCameraButton"
|
||||
class="option"
|
||||
id="video-opt"
|
||||
[selected]="selectedOption === settingsOptions.VIDEO"
|
||||
[value]="settingsOptions.VIDEO"
|
||||
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.VIDEO' | translate) : '' }}"
|
||||
[matTooltipDisabled]="!shouldHideMenuText"
|
||||
>
|
||||
<mat-icon matListItemIcon>videocam</mat-icon>
|
||||
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
|
||||
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
|
||||
</mat-list-option>
|
||||
<mat-list-option
|
||||
*ngIf="showMicrophoneButton"
|
||||
class="option"
|
||||
id="audio-opt"
|
||||
[selected]="selectedOption === settingsOptions.AUDIO"
|
||||
[value]="settingsOptions.AUDIO"
|
||||
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.AUDIO' | translate) : '' }}"
|
||||
[matTooltipDisabled]="!shouldHideMenuText"
|
||||
>
|
||||
<mat-icon matListItemIcon>mic</mat-icon>
|
||||
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
|
||||
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
|
||||
</mat-list-option>
|
||||
<!-- <mat-list-option
|
||||
*ngIf="showCaptions"
|
||||
@ -48,36 +56,65 @@
|
||||
[selected]="selectedOption === settingsOptions.CAPTIONS"
|
||||
[value]="settingsOptions.CAPTIONS"
|
||||
id="captions-opt"
|
||||
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.CAPTIONS' | translate) : '' }}"
|
||||
[matTooltipDisabled]="!shouldHideMenuText"
|
||||
>
|
||||
<mat-icon matListItemIcon>closed_caption</mat-icon>
|
||||
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.CAPTIONS' | translate }}</div>
|
||||
<div mat-line *ngIf="!shouldHideMenuText">{{ 'PANEL.SETTINGS.CAPTIONS' | translate }}</div>
|
||||
</mat-list-option> -->
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
|
||||
<div class="item-content">
|
||||
<div *ngIf="selectedOption === settingsOptions.GENERAL">
|
||||
<mat-label class="input-label">{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
|
||||
<ov-participant-name-input></ov-participant-name-input>
|
||||
<mat-list>
|
||||
<mat-list-item class="lang-selector">
|
||||
<mat-icon matListItemIcon>translate</mat-icon>
|
||||
<div matListItemTitle>{{ 'PANEL.SETTINGS.LANGUAGE' | translate }}</div>
|
||||
<ov-lang-selector matListItemMeta (onLangChanged)="onLangChanged.emit($event)"></ov-lang-selector>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
<div class="item-content" [class.full-width]="isVerticalLayout">
|
||||
<div *ngIf="selectedOption === settingsOptions.GENERAL" class="general-settings">
|
||||
<div class="nickname-section">
|
||||
<mat-label class="input-label">{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
|
||||
<div class="nickname-input-container">
|
||||
<ov-participant-name-input></ov-participant-name-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="language-section">
|
||||
<mat-list>
|
||||
<mat-list-item class="lang-selector">
|
||||
<mat-icon matListItemIcon>translate</mat-icon>
|
||||
<div matListItemTitle>{{ 'PANEL.SETTINGS.LANGUAGE' | translate }}</div>
|
||||
<ov-lang-selector matListItemMeta (onLangChanged)="onLangChanged.emit($event)"></ov-lang-selector>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
@if (showThemeSelector) {
|
||||
<div class="theme-section">
|
||||
<mat-list>
|
||||
<mat-list-item class="theme-selector">
|
||||
<mat-icon matListItemIcon class="material-symbols-outlined">routine</mat-icon>
|
||||
<div matListItemTitle>{{ 'PANEL.SETTINGS.THEME' | translate }}</div>
|
||||
<ov-theme-selector matListItemMeta></ov-theme-selector>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
}
|
||||
<!-- Additional elements injected via directive -->
|
||||
@if (generalAdditionalElementsTemplate) {
|
||||
<div class="additional-elements-section">
|
||||
<ng-container *ngTemplateOutlet="generalAdditionalElementsTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ov-video-devices-select
|
||||
*ngIf="selectedOption === settingsOptions.VIDEO"
|
||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
|
||||
></ov-video-devices-select>
|
||||
<ov-audio-devices-select
|
||||
*ngIf="selectedOption === settingsOptions.AUDIO"
|
||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
|
||||
></ov-audio-devices-select>
|
||||
<!-- <ov-captions-settings *ngIf="selectedOption === settingsOptions.CAPTIONS && showCaptions"></ov-captions-settings> -->
|
||||
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
|
||||
<ov-video-devices-select
|
||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
|
||||
></ov-video-devices-select>
|
||||
</div>
|
||||
<div *ngIf="showMicrophoneButton && selectedOption === settingsOptions.AUDIO" class="audio-settings">
|
||||
<ov-audio-devices-select
|
||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
|
||||
></ov-audio-devices-select>
|
||||
</div>
|
||||
<!-- <div *ngIf="selectedOption === settingsOptions.CAPTIONS && showCaptions" class="captions-settings">
|
||||
<ov-captions-settings></ov-captions-settings>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,39 +1,192 @@
|
||||
#settings-container {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
// Base layout - horizontal (desktop/tablet)
|
||||
.settings-container {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.item-menu {
|
||||
padding-right: 5px;
|
||||
border-right: 1px solid var(--ov-secondary-action-color);
|
||||
width: 170px;
|
||||
}
|
||||
.item-menu.mobile {
|
||||
width: 50px !important;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
flex-grow: 1;
|
||||
width: min-content;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lang-container button {
|
||||
// Vertical layout for mobile
|
||||
&.vertical-layout .settings-container {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
// Menu styling - Desktop/Tablet default
|
||||
.item-menu {
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--ov-secondary-action-color);
|
||||
width: 180px;
|
||||
min-width: 180px;
|
||||
padding-right: 16px;
|
||||
|
||||
// Compact view (tablet)
|
||||
&.compact {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
padding-right: 12px;
|
||||
|
||||
.option {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Icons only (mobile/small tablets) - ahora con mejor padding
|
||||
&.icons-only {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile vertical layout - no side border, full width menu
|
||||
&.vertical-layout .item-menu {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--ov-border-color);
|
||||
padding-right: 0;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
// Make menu horizontal on mobile with better spacing
|
||||
mat-selection-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
mat-list-option {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
min-height: 60px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
|
||||
// Estructura vertical: icono arriba, texto abajo
|
||||
::ng-deep .mdc-list-item__content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// Resetear el margin del icono para layout vertical
|
||||
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
// Mejor presentación en mobile con layout vertical
|
||||
.option-text {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content area styling
|
||||
.item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px 16px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.full-width {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// General settings specific styling
|
||||
.general-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 8px 0;
|
||||
|
||||
.nickname-section {
|
||||
.input-label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--ov-text-surface-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nickname-input-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Ensure the input takes full width of its container
|
||||
::ng-deep ov-participant-name-input {
|
||||
width: 100%;
|
||||
|
||||
.participant-name-input-container {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.participant-name-input {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.language-section {
|
||||
box-sizing: border-box;
|
||||
|
||||
mat-list {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Video and Audio settings containers
|
||||
.video-settings,
|
||||
.audio-settings,
|
||||
.captions-settings {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// List option styling
|
||||
mat-list-option[aria-selected='true'] {
|
||||
background: var(--ov-accent-action-color) !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
|
||||
::ng-deep .mat-mdc-list-item-unscoped-content,
|
||||
mat-icon {
|
||||
color: var(--ov-secondary-action-color) !important;
|
||||
color: var(--ov-text-primary-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,23 +195,224 @@
|
||||
mat-icon {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--ov-accent-action-color-rgb), 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Icon spacing
|
||||
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
|
||||
margin-right: 15px !important;
|
||||
}
|
||||
|
||||
// Remove focus state layer
|
||||
.mat-mdc-list-base {
|
||||
--mdc-list-list-item-focus-state-layer-color: transparent !important;
|
||||
--mat-list-list-item-focus-state-layer-color: transparent !important;
|
||||
}
|
||||
|
||||
// Language selector styling
|
||||
::ng-deep .lang-selector {
|
||||
.expand-more-icon,
|
||||
mat-icon {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
div {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
}
|
||||
::ng-deep .mat-mdc-list-item-meta.mdc-list-item__end {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .lang-selector .expand-more-icon,
|
||||
::ng-deep .lang-selector mat-icon {
|
||||
color: var(--ov-secondary-action-color) !important;
|
||||
::ng-deep .lang-selector mat-icon,
|
||||
::ng-deep .theme-selector .expand-more-icon,
|
||||
::ng-deep .theme-selector mat-icon {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep .lang-selector div,
|
||||
::ng-deep .theme-selector div,
|
||||
.input-label {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
// Icons-only mode styling for compact menu items
|
||||
&.compact-view .item-menu.icons-only {
|
||||
mat-list-option {
|
||||
min-height: 52px;
|
||||
padding: 8px;
|
||||
justify-content: center;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
|
||||
::ng-deep .mdc-list-item__content {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mejor transición para cambios de estado
|
||||
mat-list-option {
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover:not([aria-selected='true']) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Mejora en la tipografía y espaciado
|
||||
.option-text {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive breakpoints optimizados
|
||||
@media (max-width: 1024px) {
|
||||
#settings-container {
|
||||
.settings-container {
|
||||
padding: 14px;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#settings-container {
|
||||
.settings-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.general-settings {
|
||||
gap: 20px;
|
||||
|
||||
.nickname-input-container {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Ajustes para tablet en modo portrait
|
||||
.item-menu {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#settings-container {
|
||||
.settings-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.general-settings {
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#settings-container {
|
||||
.settings-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.general-settings {
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
// Mobile horizontal menu adjustments mejorados
|
||||
&.vertical-layout .item-menu {
|
||||
mat-selection-list {
|
||||
gap: 6px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
mat-list-option {
|
||||
min-height: 56px;
|
||||
padding: 6px 4px;
|
||||
|
||||
.option-text {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// High DPI / small screens optimization
|
||||
@media (max-width: 360px) {
|
||||
#settings-container {
|
||||
.settings-container {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.general-settings {
|
||||
gap: 14px;
|
||||
|
||||
.nickname-section .input-label {
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical-layout .item-menu {
|
||||
mat-list-option {
|
||||
min-height: 52px;
|
||||
padding: 4px 2px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Component, ContentChild, EventEmitter, OnInit, Output, TemplateRef } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model';
|
||||
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
|
||||
import { PanelService } from '../../../services/panel/panel.service';
|
||||
import { PlatformService } from '../../../services/platform/platform.service';
|
||||
import { ViewportService } from '../../../services/viewport/viewport.service';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { LangOption } from '../../../models/lang.model';
|
||||
import { SettingsPanelGeneralAdditionalElementsDirective } from '../../../directives/template/internals.directive';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -13,7 +15,8 @@ import { LangOption } from '../../../models/lang.model';
|
||||
@Component({
|
||||
selector: 'ov-settings-panel',
|
||||
templateUrl: './settings-panel.component.html',
|
||||
styleUrls: ['../panel.component.scss', './settings-panel.component.scss']
|
||||
styleUrls: ['../panel.component.scss', './settings-panel.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class SettingsPanelComponent implements OnInit {
|
||||
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
|
||||
@ -21,17 +24,50 @@ export class SettingsPanelComponent implements OnInit {
|
||||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onLangChanged = new EventEmitter<LangOption>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* ContentChild for custom elements in general section
|
||||
*/
|
||||
@ContentChild(SettingsPanelGeneralAdditionalElementsDirective)
|
||||
externalGeneralAdditionalElements!: SettingsPanelGeneralAdditionalElementsDirective;
|
||||
|
||||
settingsOptions: typeof PanelSettingsOptions = PanelSettingsOptions;
|
||||
selectedOption: PanelSettingsOptions = PanelSettingsOptions.GENERAL;
|
||||
showCameraButton: boolean = true;
|
||||
showMicrophoneButton: boolean = true;
|
||||
showCaptions: boolean = true;
|
||||
panelSubscription: Subscription;
|
||||
showThemeSelector: boolean = false;
|
||||
isMobile: boolean = false;
|
||||
private captionsSubs: Subscription;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Gets the template for additional elements in general section
|
||||
*/
|
||||
get generalAdditionalElementsTemplate(): TemplateRef<any> | undefined {
|
||||
return this.externalGeneralAdditionalElements?.template;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private panelService: PanelService,
|
||||
private platformService: PlatformService,
|
||||
private libService: OpenViduComponentsConfigService
|
||||
private libService: OpenViduComponentsConfigService,
|
||||
public viewportService: ViewportService
|
||||
) {}
|
||||
|
||||
// Computed properties for responsive behavior
|
||||
get isCompactView(): boolean {
|
||||
return this.viewportService.isMobileView() || this.viewportService.isTabletDown();
|
||||
}
|
||||
|
||||
get isVerticalLayout(): boolean {
|
||||
return this.viewportService.isMobileView();
|
||||
}
|
||||
|
||||
get shouldHideMenuText(): boolean {
|
||||
return !this.viewportService.isMobileView() && this.viewportService.isTablet();
|
||||
}
|
||||
ngOnInit() {
|
||||
this.isMobile = this.platformService.isMobile();
|
||||
this.subscribeToPanelToggling();
|
||||
@ -39,7 +75,8 @@ export class SettingsPanelComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.captionsSubs) this.captionsSubs.unsubscribe();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
close() {
|
||||
@ -50,13 +87,14 @@ export class SettingsPanelComponent implements OnInit {
|
||||
}
|
||||
|
||||
private subscribeToDirectives() {
|
||||
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => {
|
||||
this.showCaptions = value;
|
||||
});
|
||||
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCameraButton = value));
|
||||
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showMicrophoneButton = value));
|
||||
this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCaptions = value));
|
||||
this.libService.showThemeSelector$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showThemeSelector = value));
|
||||
}
|
||||
|
||||
private subscribeToPanelToggling() {
|
||||
this.panelSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
|
||||
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
|
||||
if (ev.panelType === PanelType.SETTINGS && !!ev.subOptionType) {
|
||||
this.selectedOption = ev.subOptionType as PanelSettingsOptions;
|
||||
}
|
||||
|
||||
@ -1,64 +1,131 @@
|
||||
<div class="container" id="prejoin-container">
|
||||
@if (viewportService.shouldShowLandscapeWarning()) {
|
||||
<ov-landscape-warning></ov-landscape-warning>
|
||||
} @else {
|
||||
<div class="prejoin-container" id="prejoin-container" [class.mobile]="viewportService.isMobile()" [class.name-error]="!!_error">
|
||||
<!-- Top Language Toolbar -->
|
||||
@if (!isMinimal) {
|
||||
<div class="top-toolbar">
|
||||
<ov-lang-selector [compact]="false" class="language-selector" (onLangChanged)="onLangChanged.emit($event)">
|
||||
</ov-lang-selector>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div *ngIf="isLoading" id="loading-container">
|
||||
<mat-spinner [diameter]="50"></mat-spinner>
|
||||
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isLoading" id="prejoin-card">
|
||||
<ov-lang-selector *ngIf="!isMinimal" [compact]="true" class="lang-btn" (onLangChanged)="onLangChanged.emit($event)">
|
||||
</ov-lang-selector>
|
||||
|
||||
<div>
|
||||
<div class="video-container">
|
||||
<div id="video-poster">
|
||||
<ov-media-element
|
||||
[track]="videoTrack"
|
||||
[showAvatar]="!videoTrack || videoTrack.isMuted"
|
||||
[avatarName]="participantName"
|
||||
[avatarColor]="'hsl(48, 100%, 50%)'"
|
||||
[isLocal]="true"
|
||||
></ov-media-element>
|
||||
<!-- Loading State -->
|
||||
@if (isLoading) {
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<mat-spinner [diameter]="40"></mat-spinner>
|
||||
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Main Content -->
|
||||
<div class="prejoin-content">
|
||||
<!-- Main Card -->
|
||||
<div class="prejoin-main">
|
||||
<!-- Video Preview Section -->
|
||||
<div class="video-preview-section">
|
||||
<div class="video-preview-container" [class.compact]="showBackgroundPanel">
|
||||
<div class="video-frame">
|
||||
<ov-media-element
|
||||
[track]="videoTrack"
|
||||
[showAvatar]="!isVideoEnabled"
|
||||
[avatarName]="participantName"
|
||||
[avatarColor]="'hsl(48, 100%, 50%)'"
|
||||
[isLocal]="true"
|
||||
class="video-element"
|
||||
[id]="videoTrack?.id || 'no-video'"
|
||||
>
|
||||
</ov-media-element>
|
||||
|
||||
<div class="media-controls-container">
|
||||
<!-- Camera -->
|
||||
<div class="video-controls-container">
|
||||
<ov-video-devices-select
|
||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||
></ov-video-devices-select>
|
||||
</div>
|
||||
<!-- Video Controls Overlay -->
|
||||
<div class="video-overlay">
|
||||
<div class="device-controls">
|
||||
<div class="control-group" *ngIf="showCameraButton">
|
||||
<ov-video-devices-select
|
||||
[compact]="true"
|
||||
(onVideoDeviceChanged)="videoDeviceChanged($event)"
|
||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||
(onVideoDevicesLoaded)="onVideoDevicesLoaded($event)"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-video-devices-select>
|
||||
</div>
|
||||
|
||||
<!-- Microphone -->
|
||||
<div class="audio-controls-container">
|
||||
<ov-audio-devices-select
|
||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
||||
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
||||
></ov-audio-devices-select>
|
||||
</div>
|
||||
<div class="control-group" *ngIf="showMicrophoneButton">
|
||||
<ov-audio-devices-select
|
||||
[compact]="true"
|
||||
(onAudioDeviceChanged)="audioDeviceChanged($event)"
|
||||
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
||||
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-audio-devices-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participant-name-container">
|
||||
<ov-participant-name-input
|
||||
[isPrejoinPage]="true"
|
||||
[error]="!!_error"
|
||||
(onNameUpdated)="onParticipantNameChanged($event)"
|
||||
(onEnterPressed)="onEnterPressed()"
|
||||
></ov-participant-name-input>
|
||||
</div>
|
||||
<!-- Virtual Background Button -->
|
||||
@if (backgroundEffectEnabled && hasVideoDevices) {
|
||||
<div class="background-control">
|
||||
<button
|
||||
mat-flat-button
|
||||
class="background-button"
|
||||
(click)="toggleBackgroundPanel()"
|
||||
[matTooltip]="'Virtual Backgrounds'"
|
||||
[disabled]="!isVideoEnabled"
|
||||
id="backgrounds-button"
|
||||
>
|
||||
<mat-icon class="material-symbols-outlined">background_replace</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!_error" id="token-error">
|
||||
<span class="error">{{ _error }}</span>
|
||||
</div>
|
||||
@if (showBackgroundPanel) {
|
||||
<div class="vb-container">
|
||||
<ov-background-effects-panel [mode]="'prejoin'" (onClose)="closeBackgroundPanel()">
|
||||
</ov-background-effects-panel>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Configuration Section -->
|
||||
<div class="configuration-section">
|
||||
<!-- Participant Name Input -->
|
||||
<div class="participant-name-container input-section" *ngIf="showParticipantName">
|
||||
<ov-participant-name-input
|
||||
[isPrejoinPage]="true"
|
||||
[error]="!!_error"
|
||||
(onNameUpdated)="onParticipantNameChanged($event)"
|
||||
(onEnterPressed)="onEnterPressed()"
|
||||
class="name-input"
|
||||
>
|
||||
</ov-participant-name-input>
|
||||
</div>
|
||||
|
||||
<div class="join-btn-container">
|
||||
<button mat-flat-button (click)="joinSession()" id="join-button">
|
||||
{{ 'PREJOIN.JOIN' | translate }}
|
||||
</button>
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="!!_error" class="error-message" id="token-error">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<span class="error-text">{{ _error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Join Button -->
|
||||
<div class="join-section">
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="join()"
|
||||
class="join-button"
|
||||
id="join-button"
|
||||
[disabled]="showParticipantName && !participantName"
|
||||
>
|
||||
{{ 'PREJOIN.JOIN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,25 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { LocalTrack, Track } from 'livekit-client';
|
||||
import { filter, Subject, take, takeUntil } from 'rxjs';
|
||||
import { CustomDevice } from '../../models/device.model';
|
||||
import { LangOption } from '../../models/lang.model';
|
||||
import { ILogger } from '../../models/logger.model';
|
||||
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
|
||||
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
||||
import { LoggerService } from '../../services/logger/logger.service';
|
||||
import { OpenViduService } from '../../services/openvidu/openvidu.service';
|
||||
import { TranslateService } from '../../services/translate/translate.service';
|
||||
import { LocalTrack } from 'livekit-client';
|
||||
import { CustomDevice } from '../../models/device.model';
|
||||
import { LangOption } from '../../models/lang.model';
|
||||
import { StorageService } from '../../services/storage/storage.service';
|
||||
import { ViewportService } from '../../services/viewport/viewport.service';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -17,7 +27,9 @@ import { StorageService } from '../../services/storage/storage.service';
|
||||
@Component({
|
||||
selector: 'ov-pre-join',
|
||||
templateUrl: './pre-join.component.html',
|
||||
styleUrls: ['./pre-join.component.scss']
|
||||
styleUrls: ['./pre-join.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
export class PreJoinComponent implements OnInit, OnDestroy {
|
||||
@Input() set error(error: { name: string; message: string } | undefined) {
|
||||
@ -31,24 +43,30 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||
@Output() onReadyToJoin = new EventEmitter<any>();
|
||||
|
||||
_error: string | undefined;
|
||||
|
||||
windowSize: number;
|
||||
windowSize!: number;
|
||||
isLoading = true;
|
||||
participantName: string | undefined;
|
||||
participantName: string | undefined = '';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
isMinimal: boolean = false;
|
||||
showCameraButton: boolean = true;
|
||||
showMicrophoneButton: boolean = true;
|
||||
showLogo: boolean = true;
|
||||
showParticipantName: boolean = true;
|
||||
|
||||
// Future feature preparation
|
||||
backgroundEffectEnabled: boolean = true; // Enable virtual backgrounds by default
|
||||
showBackgroundPanel: boolean = false;
|
||||
|
||||
videoTrack: LocalTrack | undefined;
|
||||
audioTrack: LocalTrack | undefined;
|
||||
private tracks: LocalTrack[];
|
||||
isVideoEnabled: boolean = false;
|
||||
hasVideoDevices: boolean = true;
|
||||
private tracks: LocalTrack[] = [];
|
||||
private log: ILogger;
|
||||
private screenShareStateSubscription: Subscription;
|
||||
private minimalSub: Subscription;
|
||||
private displayLogoSub: Subscription;
|
||||
private destroy$ = new Subject<void>();
|
||||
private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
|
||||
|
||||
@HostListener('window:resize')
|
||||
@ -61,98 +79,177 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||
private libService: OpenViduComponentsConfigService,
|
||||
private cdkSrv: CdkOverlayService,
|
||||
private openviduService: OpenViduService,
|
||||
private storageService: StorageService,
|
||||
private translateService: TranslateService,
|
||||
private changeDetector: ChangeDetectorRef
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
protected viewportService: ViewportService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('PreJoinComponent');
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.subscribeToPrejoinDirectives();
|
||||
await this.initializeDevices();
|
||||
await this.initializeDevicesWithRetry();
|
||||
this.windowSize = window.innerWidth;
|
||||
this.isLoading = false;
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
// ngAfterContentChecked(): void {
|
||||
// // this.changeDetector.detectChanges();
|
||||
// this.isLoading = false;
|
||||
// }
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.cdkSrv.setSelector('body');
|
||||
if (this.screenShareStateSubscription) this.screenShareStateSubscription.unsubscribe();
|
||||
if (this.minimalSub) this.minimalSub.unsubscribe();
|
||||
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
|
||||
|
||||
if (this.shouldRemoveTracksWhenComponentIsDestroyed) {
|
||||
this.tracks.forEach((track) => {
|
||||
this.tracks?.forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeDevices() {
|
||||
try {
|
||||
this.tracks = await this.openviduService.createLocalTracks();
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
|
||||
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
|
||||
} catch (error) {
|
||||
this.log.e('Error creating local tracks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onDeviceSelectorClicked() {
|
||||
// Some devices as iPhone do not show the menu panels correctly
|
||||
// Updating the container where the panel is added fix the problem.
|
||||
this.cdkSrv.setSelector('#prejoin-container');
|
||||
}
|
||||
|
||||
joinSession() {
|
||||
if (!this.participantName) {
|
||||
join() {
|
||||
if (this.showParticipantName && !this.participantName?.trim()) {
|
||||
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous errors
|
||||
this._error = undefined;
|
||||
|
||||
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
|
||||
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
|
||||
this.onReadyToJoin.emit();
|
||||
|
||||
// Assign participant name to the observable if it is defined
|
||||
if (this.participantName?.trim()) {
|
||||
this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
|
||||
|
||||
this.libService.participantName$
|
||||
.pipe(
|
||||
filter((name) => name === this.participantName?.trim()),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => this.onReadyToJoin.emit());
|
||||
} else {
|
||||
// No participant name to set, emit immediately
|
||||
this.onReadyToJoin.emit();
|
||||
}
|
||||
}
|
||||
|
||||
onParticipantNameChanged(name: string) {
|
||||
this.participantName = name;
|
||||
this.participantName = name?.trim() || '';
|
||||
// Clear error when user starts typing
|
||||
if (this._error && this.participantName) {
|
||||
this._error = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
onEnterPressed() {
|
||||
this.joinSession();
|
||||
this.join();
|
||||
}
|
||||
|
||||
private subscribeToPrejoinDirectives() {
|
||||
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
|
||||
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.isMinimal = value;
|
||||
// this.cd.markForCheck();
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
|
||||
|
||||
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.showCameraButton = value;
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.showMicrophoneButton = value;
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.showLogo = value;
|
||||
// this.cd.markForCheck();
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
this.libService.participantName$.subscribe((value: string) => {
|
||||
if (value) this.participantName = value;
|
||||
// this.cd.markForCheck();
|
||||
|
||||
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
|
||||
if (value) {
|
||||
this.participantName = value;
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.libService.prejoinDisplayParticipantName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.showParticipantName = value;
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
async videoEnabledChanged(enabled: boolean) {
|
||||
if (enabled && !this.videoTrack) {
|
||||
this.isVideoEnabled = enabled;
|
||||
if (!enabled) {
|
||||
this.closeBackgroundPanel();
|
||||
} else if (!this.videoTrack) {
|
||||
const newVideoTrack = await this.openviduService.createLocalTracks(true, false);
|
||||
this.videoTrack = newVideoTrack[0];
|
||||
this.tracks.push(this.videoTrack);
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
}
|
||||
|
||||
this.onVideoEnabledChanged.emit(enabled);
|
||||
}
|
||||
|
||||
async videoDeviceChanged(device: CustomDevice) {
|
||||
try {
|
||||
this.log.d('Video device changed to:', device);
|
||||
|
||||
// Get the updated tracks from the service
|
||||
const updatedTracks = this.openviduService.getLocalTracks();
|
||||
|
||||
// Find the new video track
|
||||
const newVideoTrack = updatedTracks.find((track) => track.kind === Track.Kind.Video);
|
||||
|
||||
// if (newVideoTrack && newVideoTrack !== this.videoTrack) {
|
||||
this.tracks = updatedTracks;
|
||||
this.videoTrack = newVideoTrack;
|
||||
|
||||
this.onVideoDeviceChanged.emit(device);
|
||||
} catch (error) {
|
||||
this.log.e('Error handling video device change:', error);
|
||||
this.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
onVideoDevicesLoaded(devices: CustomDevice[]) {
|
||||
this.hasVideoDevices = devices.length > 0;
|
||||
}
|
||||
|
||||
audioDeviceChanged(device: CustomDevice) {
|
||||
try {
|
||||
this.log.d('Audio device changed to:', device);
|
||||
|
||||
// Get the updated tracks from the service
|
||||
const updatedTracks = this.openviduService.getLocalTracks();
|
||||
|
||||
// Find the new audio track
|
||||
const newAudioTrack = updatedTracks.find((track) => track.kind === Track.Kind.Audio);
|
||||
|
||||
this.tracks = updatedTracks;
|
||||
this.audioTrack = newAudioTrack;
|
||||
|
||||
this.onAudioDeviceChanged.emit(device);
|
||||
} catch (error) {
|
||||
this.log.e('Error handling audio device change:', error);
|
||||
this.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async audioEnabledChanged(enabled: boolean) {
|
||||
if (enabled && !this.audioTrack) {
|
||||
const newAudioTrack = await this.openviduService.createLocalTracks(false, true);
|
||||
@ -162,4 +259,68 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.onAudioEnabledChanged.emit(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle virtual background panel visibility with smooth animation
|
||||
*/
|
||||
toggleBackgroundPanel() {
|
||||
// Add a small delay to ensure smooth transition
|
||||
if (!this.showBackgroundPanel) {
|
||||
// Opening panel
|
||||
this.showBackgroundPanel = true;
|
||||
this.changeDetector.markForCheck();
|
||||
} else {
|
||||
// Closing panel - add slight delay for smooth animation
|
||||
setTimeout(() => {
|
||||
this.showBackgroundPanel = false;
|
||||
this.changeDetector.markForCheck();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close virtual background panel with smooth animation
|
||||
*/
|
||||
closeBackgroundPanel() {
|
||||
// Add animation delay for smooth closing
|
||||
setTimeout(() => {
|
||||
this.showBackgroundPanel = false;
|
||||
this.changeDetector.markForCheck();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error handling with better UX
|
||||
*/
|
||||
private handleError(error: any) {
|
||||
this.log.e('PreJoin component error:', error);
|
||||
this._error = error.message || 'An unexpected error occurred';
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved device initialization with error handling
|
||||
*/
|
||||
private async initializeDevicesWithRetry(maxRetries: number = 3): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
this.tracks = await this.openviduService.createLocalTracks();
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
this.videoTrack = this.tracks.find((track) => track.kind === Track.Kind.Video);
|
||||
this.audioTrack = this.tracks.find((track) => track.kind === Track.Kind.Audio);
|
||||
this.isVideoEnabled = this.openviduService.isVideoTrackEnabled();
|
||||
|
||||
return; // Success, exit retry loop
|
||||
} catch (error) {
|
||||
this.log.w(`Device initialization attempt ${attempt} failed:`, error);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
this.handleError(error);
|
||||
} else {
|
||||
// Wait before retrying
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,36 @@
|
||||
<div id="spinner" *ngIf="loading" @sessionAnimation>
|
||||
<mat-spinner [diameter]="50"></mat-spinner>
|
||||
<span>{{ 'ROOM.JOINING' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div id="session-container" *ngIf="!loading" @sessionAnimation>
|
||||
<mat-sidenav-container #container #videoContainer class="sidenav-container">
|
||||
<mat-sidenav
|
||||
#sidenav
|
||||
mode="{{ sidenavMode }}"
|
||||
position="end"
|
||||
class="sidenav-menu"
|
||||
[ngClass]="{big: settingsPanelOpened}"
|
||||
fixedInViewport="true"
|
||||
fixedTopGap="0"
|
||||
fixedBottomGap="0"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="panelTemplate"></ng-container>
|
||||
</mat-sidenav>
|
||||
|
||||
<mat-sidenav-content class="sidenav-main">
|
||||
<div id="layout-container" #layoutContainer>
|
||||
<ng-container *ngTemplateOutlet="layoutTemplate"></ng-container>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
||||
<div id="footer-container" *ngIf="toolbarTemplate">
|
||||
<ng-container *ngTemplateOutlet="toolbarTemplate"></ng-container>
|
||||
@if (loading) {
|
||||
<div id="spinner" [@inOutAnimation]>
|
||||
<mat-spinner [diameter]="50"></mat-spinner>
|
||||
<span>{{ 'ROOM.JOINING' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
@if (viewportService.shouldShowLandscapeWarning()) {
|
||||
<ov-landscape-warning></ov-landscape-warning>
|
||||
}
|
||||
<div id="session-container" [@inOutAnimation]>
|
||||
<mat-sidenav-container #container #videoContainer class="sidenav-container">
|
||||
<mat-sidenav
|
||||
#sidenav
|
||||
mode="{{ sidenavMode }}"
|
||||
position="end"
|
||||
class="sidenav-menu"
|
||||
[ngClass]="{ big: settingsPanelOpened }"
|
||||
fixedInViewport="true"
|
||||
fixedTopGap="0"
|
||||
fixedBottomGap="0"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="panelTemplate"></ng-container>
|
||||
</mat-sidenav>
|
||||
|
||||
<mat-sidenav-content class="sidenav-main">
|
||||
<div id="layout-container" #layoutContainer>
|
||||
<ng-container *ngTemplateOutlet="layoutTemplate"></ng-container>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
||||
<div id="footer-container" *ngIf="toolbarTemplate">
|
||||
<ng-container *ngTemplateOutlet="toolbarTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
/* min-width: 400px; */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
@ -84,29 +85,37 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
background-color: black;
|
||||
background-color: var(--ov-video-background, var(--ov-primary-action-color));
|
||||
opacity: 80%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
@media only screen and (max-width: 768px) {
|
||||
#session-container {
|
||||
width: 100%;
|
||||
/* position: fixed; */
|
||||
}
|
||||
|
||||
.sidenav-menu {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.big {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO(mdc-migration): The following rule targets internal classes of button that may no longer apply for the MDC version. */
|
||||
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
|
||||
padding: 1px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-input-element {
|
||||
caret-color: #000000;
|
||||
caret-color: var(--ov-video-background, var(--ov-primary-action-color));
|
||||
}
|
||||
/* TODO(mdc-migration): The following rule targets internal classes of option that may no longer apply for the MDC version. */
|
||||
::ng-deep .mat-primary .mat-mdc-option.mat-selected:not(.mat-option-disabled) {
|
||||
color: #000000;
|
||||
color: var(--ov-video-background, var(--ov-primary-action-color)) !important;
|
||||
}
|
||||
|
||||
/* TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version. */
|
||||
|
||||
@ -3,7 +3,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { ActionService } from '../../services/action/action.service';
|
||||
import { ActionServiceMock } from '../../services/action/action.service.mock';
|
||||
import { ActionServiceMock } from '../../../test-helpers/action.service.mock';
|
||||
|
||||
import { ChatService } from '../../services/chat/chat.service';
|
||||
import { ChatServiceMock } from '../../services/chat/chat.service.mock';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,55 +1,99 @@
|
||||
<div class="device-container-element" [class.mute-btn]="!isMicrophoneEnabled">
|
||||
<!-- <button mat-stroked-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" id="audio-devices-menu">
|
||||
<mat-icon class="audio-icon">mic</mat-icon>
|
||||
<span class="device-label"> {{ microphoneSelected.label }} </span>
|
||||
<mat-icon iconPositionEnd class="chevron-icon">
|
||||
{{ menuTrigger.menuOpen ? 'expand_less' : 'expand_more' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let microphone of microphones">{{ microphone.label }}</button>
|
||||
</mat-menu> -->
|
||||
<mat-form-field id="audio-devices-form" *ngIf="microphones.length > 0">
|
||||
<mat-select
|
||||
[disabled]="!hasAudioDevices"
|
||||
[compareWith]="compareObjectDevices"
|
||||
[value]="microphoneSelected"
|
||||
(selectionChange)="onMicrophoneSelected($event)"
|
||||
>
|
||||
<mat-select-trigger>
|
||||
<div class="audio-device-selector" [class.compact]="compact">
|
||||
<!-- Unified Device Button (Compact Mode) -->
|
||||
@if (compact) {
|
||||
@if (hasAudioDevices()) {
|
||||
<div class="unified-device-button">
|
||||
<!-- Main toggle button -->
|
||||
<button
|
||||
mat-flat-button
|
||||
class="toggle-section"
|
||||
[disabled]="!hasAudioDevices() || microphoneStatusChanging"
|
||||
[class.device-enabled]="isMicrophoneEnabled"
|
||||
[class.device-disabled]="!isMicrophoneEnabled"
|
||||
(click)="toggleMic($event)"
|
||||
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
|
||||
[matTooltipDisabled]="!hasAudioDevices()"
|
||||
id="microphone-button"
|
||||
[disableRipple]="true"
|
||||
[disabled]="!hasAudioDevices || microphoneStatusChanging"
|
||||
[class.mute-btn]="!isMicrophoneEnabled"
|
||||
(click)="toggleMic($event)"
|
||||
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
|
||||
[matTooltipDisabled]="!hasAudioDevices"
|
||||
>
|
||||
<mat-icon *ngIf="isMicrophoneEnabled" id="mic"> mic </mat-icon>
|
||||
<mat-icon *ngIf="!isMicrophoneEnabled" id="mic_off"> mic_off </mat-icon>
|
||||
<mat-icon [id]="isMicrophoneEnabled ? 'mic' : 'mic_off'">{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
|
||||
</button>
|
||||
<span class="selected-text" *ngIf="!isMicrophoneEnabled">{{ 'PANEL.SETTINGS.DISABLED_AUDIO' | translate }}</span>
|
||||
<span class="selected-text" *ngIf="isMicrophoneEnabled"> {{ microphoneSelected.label }} </span>
|
||||
</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let microphone of microphones"
|
||||
[disabled]="!isMicrophoneEnabled"
|
||||
[value]="microphone"
|
||||
id="option-{{ microphone.label }}"
|
||||
>
|
||||
{{ microphone.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div id="audio-devices-form" *ngIf="microphones.length === 0">
|
||||
<div id="mat-select-trigger">
|
||||
<button mat-icon-button id="microphone-button" class="mute-btn" [disabled]="true">
|
||||
<mat-icon id="mic_off"> mic_off </mat-icon>
|
||||
</button>
|
||||
<span id="audio-devices-not-found"> {{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }} </span>
|
||||
<!-- Dropdown section -->
|
||||
@if (isMicrophoneEnabled) {
|
||||
<button
|
||||
mat-flat-button
|
||||
id="audio-dropdown"
|
||||
class="dropdown-section"
|
||||
[matMenuTriggerFor]="microphoneMenu"
|
||||
[disabled]="microphoneStatusChanging"
|
||||
>
|
||||
<mat-icon>expand_more</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No Microphone Available -->
|
||||
<div id="no-audio-device-message" class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<!-- Normal Mode - Input Style Selector -->
|
||||
<div class="normal-device-selector">
|
||||
<!-- Input-style Device Selector -->
|
||||
<div class="device-input-selector" [class.disabled]="!hasAudioDevices() || !isMicrophoneEnabled">
|
||||
<!-- When microphone is enabled -->
|
||||
@if (isMicrophoneEnabled) {
|
||||
<div class="device-input-selector">
|
||||
<button
|
||||
mat-flat-button
|
||||
id="audio-dropdown"
|
||||
class="selector-button"
|
||||
[disabled]="microphoneStatusChanging || microphones().length <= 1"
|
||||
[matMenuTriggerFor]="microphoneMenu"
|
||||
[attr.aria-expanded]="false"
|
||||
>
|
||||
<mat-icon class="device-icon">mic</mat-icon>
|
||||
<span class="selected-device-name">{{ microphoneSelected()?.label || 'No microphone selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="microphones().length > 1">expand_more</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
@if (hasAudioDevices()) {
|
||||
<!-- When microphone is disabled -->
|
||||
<div class="device-input-selector disabled">
|
||||
<div class="selector-button disabled">
|
||||
<mat-icon class="device-icon">mic_off</mat-icon>
|
||||
<span class="selected-device-name">
|
||||
{{ !hasAudioDevices() ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No Microphone Available -->
|
||||
<div id="no-audio-device-message" class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Device Selection Menu (Shared) -->
|
||||
<mat-menu #microphoneMenu="matMenu" class="device-menu">
|
||||
@for (microphone of microphones(); track microphone.device) {
|
||||
<button
|
||||
mat-menu-item
|
||||
id="option-{{ microphone.label }}"
|
||||
(click)="onMicrophoneSelected({ value: microphone })"
|
||||
[class.selected]="microphone.device === microphoneSelected()?.device"
|
||||
>
|
||||
<mat-icon *ngIf="microphone.device === microphoneSelected()?.device">check</mat-icon>
|
||||
<span>{{ microphone.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
@ -1,103 +1,30 @@
|
||||
$ov-selection-color-btn: #afafaf;
|
||||
$ov-selection-color: #cccccc;
|
||||
@use '../selector-shared' as shared;
|
||||
|
||||
:host {
|
||||
.device-container-element {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
border: 1px solid $ov-selection-color-btn;
|
||||
}
|
||||
.device-container-element.mute-btn {
|
||||
border: 1px solid var(--ov-error-color);
|
||||
}
|
||||
#audio-devices-form {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
#audio-devices-not-found {
|
||||
font-size: 13px;
|
||||
}
|
||||
.audio-device-selector {
|
||||
@include shared.device-selector-base();
|
||||
|
||||
#microphone-button {
|
||||
color:#000000
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper,
|
||||
::ng-deep .mat-mdc-form-field-flex,
|
||||
::ng-deep .mat-mdc-select-trigger {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
background-color: $ov-selection-color !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
border-top-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background-color: $ov-selection-color-btn !important;
|
||||
width: 48px !important;
|
||||
min-width: 48px !important;
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px !important;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
padding: 0px !important;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-button-touch-target {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
}
|
||||
|
||||
.mute-btn {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--ov-error-color) !important;
|
||||
// Audio-specific overrides for normal mode
|
||||
// &:not(.compact) {
|
||||
// .normal-device-selector {
|
||||
// .device-input-selector {
|
||||
// &:not(.disabled) {
|
||||
// .selector-button {
|
||||
// // Audio-specific hover effect (simpler than video)
|
||||
// &:hover:not([disabled]) {
|
||||
// border-color: var(--ov-primary-action-color, #4285f4);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
::ng-deep .mat-mdc-select-panel {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-option {
|
||||
padding: 10px 10px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
||||
color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
|
||||
border-bottom-color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
|
||||
background-color: $ov-selection-color !important;
|
||||
}
|
||||
// Include shared device menu styles
|
||||
@include shared.device-menu-styles();
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Component, effect, EventEmitter, Input, OnInit, Output, Signal, WritableSignal } from '@angular/core';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { ILogger } from '../../../models/logger.model';
|
||||
import { DeviceService } from '../../../services/device/device.service';
|
||||
import { LoggerService } from '../../../services/logger/logger.service';
|
||||
import { ParticipantService } from '../../../services/participant/participant.service';
|
||||
import { StorageService } from '../../../services/storage/storage.service';
|
||||
import { ParticipantModel } from '../../../models/participant.model';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -12,39 +12,46 @@ import { ParticipantModel } from '../../../models/participant.model';
|
||||
@Component({
|
||||
selector: 'ov-audio-devices-select',
|
||||
templateUrl: './audio-devices.component.html',
|
||||
styleUrls: ['./audio-devices.component.scss']
|
||||
styleUrls: ['./audio-devices.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class AudioDevicesComponent implements OnInit, OnDestroy {
|
||||
export class AudioDevicesComponent implements OnInit {
|
||||
@Input() compact: boolean = false;
|
||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
||||
|
||||
microphoneStatusChanging: boolean;
|
||||
hasAudioDevices: boolean;
|
||||
isMicrophoneEnabled: boolean;
|
||||
microphoneSelected: CustomDevice | undefined;
|
||||
microphones: CustomDevice[] = [];
|
||||
private localParticipantSubscription: Subscription;
|
||||
microphoneStatusChanging: boolean = false;
|
||||
isMicrophoneEnabled: boolean = false;
|
||||
private log: ILogger;
|
||||
|
||||
// Expose signals directly from service (reactive)
|
||||
protected readonly microphones: WritableSignal<CustomDevice[]>;
|
||||
protected readonly microphoneSelected: WritableSignal<CustomDevice | undefined>;
|
||||
protected readonly hasAudioDevices: Signal<boolean>;
|
||||
|
||||
constructor(
|
||||
private deviceSrv: DeviceService,
|
||||
private storageSrv: StorageService,
|
||||
private participantService: ParticipantService
|
||||
) {}
|
||||
private participantService: ParticipantService,
|
||||
private loggerSrv: LoggerService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('AudioDevicesComponent');
|
||||
this.microphones = this.deviceSrv.microphones;
|
||||
this.microphoneSelected = this.deviceSrv.microphoneSelected;
|
||||
this.hasAudioDevices = this.deviceSrv.hasAudioDevices;
|
||||
|
||||
async ngOnInit() {
|
||||
this.subscribeToParticipantMediaProperties();
|
||||
this.hasAudioDevices = this.deviceSrv.hasAudioDeviceAvailable();
|
||||
if (this.hasAudioDevices) {
|
||||
this.microphones = this.deviceSrv.getMicrophones();
|
||||
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
|
||||
}
|
||||
|
||||
this.isMicrophoneEnabled = this.participantService.isMyMicrophoneEnabled();
|
||||
// Use effect instead of subscription for reactive updates
|
||||
effect(() => {
|
||||
const participant = this.participantService.localParticipantSignal();
|
||||
if (participant) {
|
||||
this.isMicrophoneEnabled = participant.isMicrophoneEnabled;
|
||||
this.storageSrv.setMicrophoneEnabled(this.isMicrophoneEnabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.microphones = [];
|
||||
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
|
||||
async ngOnInit() {
|
||||
this.isMicrophoneEnabled = this.participantService.isMyMicrophoneEnabled();
|
||||
}
|
||||
|
||||
async toggleMic(event: any) {
|
||||
@ -58,14 +65,18 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async onMicrophoneSelected(event: any) {
|
||||
const device: CustomDevice = event?.value;
|
||||
if (this.deviceSrv.needUpdateAudioTrack(device)) {
|
||||
this.microphoneStatusChanging = true;
|
||||
await this.participantService.switchMicrophone(device.device);
|
||||
this.deviceSrv.setMicSelected(device.device);
|
||||
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
|
||||
try {
|
||||
const device: CustomDevice = event?.value;
|
||||
if (this.deviceSrv.needUpdateAudioTrack(device)) {
|
||||
this.microphoneStatusChanging = true;
|
||||
await this.participantService.switchMicrophone(device.device);
|
||||
this.deviceSrv.setMicSelected(device.device);
|
||||
this.onAudioDeviceChanged.emit(this.microphoneSelected());
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.e('Error switching microphone', error);
|
||||
} finally {
|
||||
this.microphoneStatusChanging = false;
|
||||
this.onAudioDeviceChanged.emit(this.microphoneSelected);
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,17 +87,4 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
|
||||
compareObjectDevices(o1: CustomDevice, o2: CustomDevice): boolean {
|
||||
return o1.label === o2.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* This subscription is necessary to update the microphone status when the user changes it from toolbar and
|
||||
* the settings panel is opened. With this, the microphone status is updated in the settings panel.
|
||||
*/
|
||||
private subscribeToParticipantMediaProperties() {
|
||||
this.localParticipantSubscription = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
|
||||
if (p) {
|
||||
this.isMicrophoneEnabled = p.isMicrophoneEnabled;
|
||||
this.storageSrv.setMicrophoneEnabled(this.isMicrophoneEnabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,11 +23,11 @@ export class CaptionsSettingComponent implements OnInit, OnDestroy {
|
||||
private captionsStatusSubs: Subscription;
|
||||
private sttStatusSubs: Subscription;
|
||||
|
||||
private layoutService: LayoutService;
|
||||
|
||||
constructor(private serviceConfig: ServiceConfigService, private captionService: CaptionService, private openviduService: OpenViduService) {
|
||||
this.layoutService = this.serviceConfig.getLayoutService();
|
||||
}
|
||||
constructor(
|
||||
private layoutService: LayoutService,
|
||||
private captionService: CaptionService,
|
||||
private openviduService: OpenViduService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// this.isOpenViduPro = this.openviduService.isOpenViduPro();
|
||||
|
||||
@ -1,18 +1,33 @@
|
||||
<button id="lang-btn-compact" *ngIf="compact" mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>translate</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="!compact" mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" id="lang-btn">
|
||||
<span id="lang-selected-name">{{ langSelected?.name }}</span>
|
||||
<mat-icon class="expand-more-icon">expand_more</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let lang of languages"
|
||||
(click)="onLangSelected(lang.lang)"
|
||||
[attr.id]="'lang-opt-' + lang.lang"
|
||||
class="lang-menu-opt"
|
||||
>
|
||||
<span>{{ lang.name }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<div class="language-selector-container">
|
||||
@if (compact) {
|
||||
<!-- Compact version (icon only) -->
|
||||
<button mat-icon-button [matMenuTriggerFor]="langMenu" class="compact-lang-button" [matTooltip]="'Change language'" disableRipple>
|
||||
<mat-icon>translate</mat-icon>
|
||||
</button>
|
||||
} @else {
|
||||
<!-- Full version (with text) -->
|
||||
<button mat-flat-button [matMenuTriggerFor]="langMenu" class="full-lang-button">
|
||||
<!-- <mat-icon class="lang-icon">translate</mat-icon> -->
|
||||
<span class="lang-name">
|
||||
{{ langSelected?.name }}
|
||||
<mat-icon class="expand-icon">expand_more</mat-icon>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Language Menu -->
|
||||
<mat-menu #langMenu="matMenu" class="language-menu">
|
||||
@for (lang of languages; track lang.lang) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onLangSelected(lang.lang)"
|
||||
[attr.id]="'lang-opt-' + lang.lang"
|
||||
[class.selected]="langSelected?.lang === lang.lang"
|
||||
class="language-option"
|
||||
>
|
||||
<mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon>
|
||||
<span class="lang-option-name">{{ lang.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
@ -1,21 +1,32 @@
|
||||
$ov-surface-color-lighter: color-mix(in srgb, var(--ov-surface-color), #fff 5%);
|
||||
@use '../selector-shared' as shared;
|
||||
|
||||
.lang-button {
|
||||
background-color: var(--ov-primary-action-color) !important;
|
||||
color: var(--ov-secondary-action-color) !important;
|
||||
}
|
||||
.lang-button .mat-icon {
|
||||
color: var(--ov-secondary-action-color);
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
::ng-deep .mat-mdc-menu-panel {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
background-color: $ov-surface-color-lighter !important;
|
||||
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
|
||||
.language-selector-container {
|
||||
.compact-lang-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.full-lang-button {
|
||||
@include shared.selector-button('lang-icon', 'lang-name', 'expand-icon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-menu-item,
|
||||
.mat-mdc-menu-item:visited,
|
||||
.mat-mdc-menu-item:link {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
::ng-deep .language-menu.mat-mdc-menu-panel {
|
||||
@include shared.selector-menu('language-option', 'lang-option-name');
|
||||
}
|
||||
|
||||
@ -12,7 +12,8 @@ import { Subscription } from 'rxjs';
|
||||
@Component({
|
||||
selector: 'ov-lang-selector',
|
||||
templateUrl: './lang-selector.component.html',
|
||||
styleUrls: ['./lang-selector.component.scss']
|
||||
styleUrls: ['./lang-selector.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class LangSelectorComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
<div id="name-input-container" [ngClass]="{ warn: !name }">
|
||||
<mat-form-field id="name-form" [ngClass]="{ error: error }">
|
||||
<mat-select-trigger>
|
||||
<button mat-flat-button disabled>
|
||||
<mat-icon>person</mat-icon>
|
||||
</button>
|
||||
</mat-select-trigger>
|
||||
<div class="participant-name-input-container" [class.error]="error">
|
||||
<div class="input-wrapper">
|
||||
<mat-icon class="input-icon">person</mat-icon>
|
||||
<input
|
||||
id="name-input"
|
||||
matInput
|
||||
(change)="updateName()"
|
||||
id="participant-name-input"
|
||||
type="text"
|
||||
maxlength="20"
|
||||
[(ngModel)]="name"
|
||||
autocomplete="off"
|
||||
[disabled]="!isPrejoinPage"
|
||||
(input)="updateName()"
|
||||
(keypress)="eventKeyPress($event)"
|
||||
[placeholder]="'PREJOIN.NICKNAME' | translate"
|
||||
class="name-input-field"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user