mirror of https://github.com/OpenVidu/openvidu.git
Compare commits
80 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
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 |
|
|
@ -1,4 +1,4 @@
|
||||||
name: openvidu-components-angular Tests
|
name: openvidu-components-angular E2E
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
@ -17,6 +17,10 @@ on:
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: '20'
|
||||||
|
CHROME_IMAGE: selenium/standalone-chrome:138.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test_setup:
|
test_setup:
|
||||||
name: Test setup
|
name: Test setup
|
||||||
|
|
@ -29,7 +33,7 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Commit URL
|
- name: Commit URL
|
||||||
run: echo https://github.com/OpenVidu/openvidu/commit/${{ inputs.commit_sha || github.sha }}
|
run: echo https://github.com/OpenVidu/openvidu/commit/${{ inputs.commit_sha || github.sha }}
|
||||||
- name: Send Dispatch Event
|
- name: Send Dispatch Event
|
||||||
|
|
@ -45,10 +49,41 @@ jobs:
|
||||||
https://api.github.com/repos/OpenVidu/openvidu-tutorials/dispatches \
|
https://api.github.com/repos/OpenVidu/openvidu-tutorials/dispatches \
|
||||||
-d '{"event_type":"openvidu-components-angular","client_payload":{"commit-message":"'"$COMMIT_MESSAGE"'","commit-ref":"'"$COMMIT_URL"'", "branch-name":"'"$BRANCH_NAME"'"}}'
|
-d '{"event_type":"openvidu-components-angular","client_payload":{"commit-message":"'"$COMMIT_MESSAGE"'","commit-ref":"'"$COMMIT_URL"'", "branch-name":"'"$BRANCH_NAME"'"}}'
|
||||||
|
|
||||||
nested_events:
|
e2e_tests:
|
||||||
needs: test_setup
|
needs: test_setup
|
||||||
name: Nested events
|
name: ${{ matrix.name }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: Nested events
|
||||||
|
script: e2e:nested-events
|
||||||
|
- name: Nested Structural Directives
|
||||||
|
script: e2e:nested-structural-directives
|
||||||
|
- name: Nested Attribute Directives
|
||||||
|
script: e2e:nested-attribute-directives
|
||||||
|
- name: API Directives Tests
|
||||||
|
script: e2e:lib-directives
|
||||||
|
- name: Internal Directives Tests
|
||||||
|
script: e2e:lib-internal-directives
|
||||||
|
- name: Chat E2E
|
||||||
|
script: e2e:lib-chat
|
||||||
|
- name: Events E2E
|
||||||
|
script: e2e:lib-events
|
||||||
|
- name: Media devices E2E
|
||||||
|
script: e2e:lib-media-devices
|
||||||
|
- name: Panels E2E
|
||||||
|
script: e2e:lib-panels
|
||||||
|
- name: Screen sharing E2E
|
||||||
|
script: e2e:lib-screensharing
|
||||||
|
- name: Stream E2E
|
||||||
|
script: e2e:lib-stream
|
||||||
|
mount_assets: true
|
||||||
|
- name: Toolbar E2E
|
||||||
|
script: e2e:lib-toolbar
|
||||||
|
- name: Virtual Backgrounds E2E
|
||||||
|
script: e2e:lib-virtual-backgrounds
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
@ -57,116 +92,16 @@ jobs:
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- name: Install wait-on package
|
- name: Install wait-on package
|
||||||
run: npm install -g wait-on
|
run: npm install -g wait-on
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
- name: Run Chrome
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
run: |
|
||||||
cd openvidu-components-angular
|
if [ "${{ matrix.mount_assets }}" = "true" ]; then
|
||||||
npm install
|
docker run --network=host -d -p 4444:4444 -v $(pwd)/openvidu-components-angular/e2e/assets:/e2e-assets ${{ env.CHROME_IMAGE }}
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
else
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
docker run --network=host -d -p 4444:4444 ${{ env.CHROME_IMAGE }}
|
||||||
- name: Run nested components E2E event tests
|
fi
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:nested-events --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
nested_structural_directives:
|
|
||||||
needs: test_setup
|
|
||||||
name: Nested Structural Directives
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run nested structural directives tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:nested-structural-directives --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
nested_attribute_directives:
|
|
||||||
needs: test_setup
|
|
||||||
name: Nested Attribute Directives
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run nested attribute directives tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:nested-attribute-directives --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_directives:
|
|
||||||
needs: test_setup
|
|
||||||
name: API Directives Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
- name: Run openvidu-local-deployment
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
||||||
- name: Start OpenVidu Call backend
|
- name: Start OpenVidu Call backend
|
||||||
|
|
@ -176,303 +111,7 @@ jobs:
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
env:
|
env:
|
||||||
LAUNCH_MODE: CI
|
LAUNCH_MODE: CI
|
||||||
run: npm run e2e:lib-directives --prefix openvidu-components-angular
|
run: npm run ${{ matrix.script }} --prefix openvidu-components-angular
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
e2e_internal_directives:
|
|
||||||
needs: test_setup
|
|
||||||
name: Internal Directives Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-internal-directives --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_chat:
|
|
||||||
needs: test_setup
|
|
||||||
name: Chat E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-chat --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_events:
|
|
||||||
needs: test_setup
|
|
||||||
name: Events E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-events --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_media_devices:
|
|
||||||
needs: test_setup
|
|
||||||
name: Media devices E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-media-devices --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_panels:
|
|
||||||
needs: test_setup
|
|
||||||
name: Panels E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-panels --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_screen_sharing:
|
|
||||||
needs: test_setup
|
|
||||||
name: Screen sharing E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-screensharing --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_stream:
|
|
||||||
needs: test_setup
|
|
||||||
name: Stream E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -v $(pwd)/openvidu-components-angular/e2e/assets:/e2e-assets selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Tests
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-stream --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_toolbar:
|
|
||||||
needs: test_setup
|
|
||||||
name: Toolbar E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Webcomponent E2E
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-toolbar --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
uses: OpenVidu/actions/cleanup@main
|
|
||||||
|
|
||||||
e2e_virtual_backgrounds:
|
|
||||||
needs: test_setup
|
|
||||||
name: Virtual Backgrounds E2E
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.commit_sha || github.sha }}
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Install wait-on package
|
|
||||||
run: npm install -g wait-on
|
|
||||||
# - name: Run Browserless Chrome
|
|
||||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
|
||||||
- name: Run Chrome
|
|
||||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
|
||||||
- name: Run openvidu-local-deployment
|
|
||||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
|
||||||
- name: Start OpenVidu Call backend
|
|
||||||
uses: OpenVidu/actions/start-openvidu-call@main
|
|
||||||
- name: Build and Serve openvidu-components-angular Testapp
|
|
||||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
|
||||||
- name: Run Webcomponent E2E
|
|
||||||
env:
|
|
||||||
LAUNCH_MODE: CI
|
|
||||||
run: npm run e2e:lib-virtual-backgrounds --prefix openvidu-components-angular
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
if: always()
|
if: always()
|
||||||
uses: OpenVidu/actions/cleanup@main
|
uses: OpenVidu/actions/cleanup@main
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -205,112 +205,151 @@ describe('E2E: Screensharing features', () => {
|
||||||
await browser.sleep(500);
|
await browser.sleep(500);
|
||||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
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();
|
// ==================== 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
|
||||||
|
|
||||||
// // Clicking to screensharing button
|
it('should NOT have multiple screens pinned when both participants share screen', async () => {
|
||||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
const roomName = 'pinBugCase1';
|
||||||
// expect(await screenshareButton.isDisplayed()).toBeTrue();
|
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||||
// await screenshareButton.click();
|
|
||||||
|
|
||||||
// await utils.waitForElement('.OV_big');
|
// Participant A joins and shares screen
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
await browser.get(fixedUrl);
|
||||||
|
await utils.checkLayoutPresent();
|
||||||
|
await utils.waitForElement('#screenshare-btn');
|
||||||
|
await utils.clickOn('#screenshare-btn');
|
||||||
|
await browser.sleep(500);
|
||||||
|
|
||||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
// Verify A's screen is pinned
|
||||||
// await muteVideoButton.click();
|
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)`);
|
||||||
|
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
// Participant B joins
|
||||||
// });
|
const tabs = await utils.openTab(fixedUrl);
|
||||||
|
await browser.switchTo().window(tabs[1]);
|
||||||
|
await utils.checkLayoutPresent();
|
||||||
|
await browser.sleep(1000);
|
||||||
|
|
||||||
// it('should screenshare has audio active when camera is muted', async () => {
|
// B should see A's screen pinned
|
||||||
// let isAudioEnabled;
|
expect(await utils.getNumberOfElements('video')).toEqual(3); // 2 cameras + 1 screen
|
||||||
// const audioEnableScript = 'return document.getElementsByTagName("video")[0].srcObject.getAudioTracks()[0].enabled;';
|
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||||
|
const pinnedCountB1 = await utils.getNumberOfPinnedStreams();
|
||||||
|
console.log(`[Tab B] After B joins: ${pinnedCountB1} pinned stream(s)`);
|
||||||
|
|
||||||
// await browser.get(`${url}&prejoin=false`);
|
// B shares screen
|
||||||
|
await utils.waitForElement('#screenshare-btn');
|
||||||
|
await utils.clickOn('#screenshare-btn');
|
||||||
|
await browser.sleep(500);
|
||||||
|
|
||||||
// await utils.checkLayoutPresent();
|
// 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
|
||||||
|
|
||||||
// // Clicking to screensharing button
|
// Switch to Tab A and check
|
||||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
await browser.switchTo().window(tabs[0]);
|
||||||
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
|
await browser.sleep(1000);
|
||||||
// await screenshareButton.click();
|
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
|
||||||
|
|
||||||
// await utils.waitForElement('.OV_big');
|
// BUG: In A's view, BOTH screens are pinned
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
|
||||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
console.log(`[Tab A] After B shares: ${pinnedCountA2} pinned stream(s)`);
|
||||||
|
|
||||||
// // Muting camera video
|
// EXPECTED: Only B's screen should be pinned (the most recent one)
|
||||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
// ACTUAL: Both A's and B's screens are pinned
|
||||||
// await muteVideoButton.click();
|
expect(pinnedCountA2).toEqual(1, 'BUG DETECTED: Multiple screens are pinned. Expected only the most recent screen to be pinned.');
|
||||||
|
});
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
|
||||||
|
it('should NOT re-pin manually unpinned screen when new participant joins', async () => {
|
||||||
// await browser.sleep(500);
|
const roomName = 'pinBugCase2';
|
||||||
// expect(await utils.isPresent('#status-mic')).toBeFalse();
|
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||||
|
|
||||||
// // Checking if audio is muted after join the room
|
// Participant A joins and shares screen
|
||||||
// isAudioEnabled = await browser.executeScript(audioEnableScript);
|
await browser.get(fixedUrl);
|
||||||
// expect(isAudioEnabled).toBeTrue();
|
await utils.checkLayoutPresent();
|
||||||
|
await utils.waitForElement('#screenshare-btn');
|
||||||
// // Unmuting camera
|
await utils.clickOn('#screenshare-btn');
|
||||||
// await muteVideoButton.click();
|
await browser.sleep(500);
|
||||||
// await browser.sleep(1000);
|
|
||||||
|
// Verify A's screen is auto-pinned
|
||||||
// await utils.waitForElement('.camera-type');
|
await utils.waitForElement('.OV_big');
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
|
||||||
// });
|
// Participant B joins and shares screen
|
||||||
|
const tabs = await utils.openTab(fixedUrl);
|
||||||
// it('should camera come back with audio muted when screensharing', async () => {
|
await browser.switchTo().window(tabs[1]);
|
||||||
// let element, isAudioEnabled;
|
await utils.checkLayoutPresent();
|
||||||
|
await browser.sleep(1000);
|
||||||
// const getAudioScript = (className: string) => {
|
await utils.waitForElement('#screenshare-btn');
|
||||||
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
|
await utils.clickOn('#screenshare-btn');
|
||||||
// };
|
await browser.sleep(500);
|
||||||
|
|
||||||
// await browser.get(`${url}&prejoin=false`);
|
// B should see their own screen pinned
|
||||||
|
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
|
||||||
// await utils.checkLayoutPresent();
|
await utils.waitForElement('.OV_big');
|
||||||
|
let pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||||
// // Clicking to screensharing button
|
console.log(`[Tab B] After B shares: ${pinnedCountB} pinned stream(s)`);
|
||||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
|
||||||
// await screenshareButton.click();
|
// B manually unpins their own screen
|
||||||
|
const screenStreams = await utils.getScreenShareStreams();
|
||||||
// await utils.waitForElement('.screen-type');
|
if (screenStreams.length > 0) {
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
// Find B's own screen (it should be the pinned one)
|
||||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
await utils.toggleStreamPin('.OV_big');
|
||||||
|
await browser.sleep(1000);
|
||||||
// // Mute camera
|
}
|
||||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
|
||||||
// await muteVideoButton.click();
|
// Verify B's screen is now unpinned
|
||||||
|
pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
console.log(`[Tab B] After manually unpinning B's screen: ${pinnedCountB} pinned stream(s)`);
|
||||||
// expect(await utils.isPresent('#status-mic')).toBeFalse();
|
expect(pinnedCountB).toEqual(0, 'B should have no pinned streams after manual unpin');
|
||||||
|
|
||||||
// // Checking if audio is muted after join the room
|
// B manually pins A's screen
|
||||||
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
|
const screenElements = await utils.getScreenShareStreams();
|
||||||
// expect(isAudioEnabled).toBeTrue();
|
if (screenElements.length >= 2) {
|
||||||
|
// Pin the first screen that is not already pinned (should be A's screen)
|
||||||
// // Mute audio
|
await utils.toggleStreamPin('.OV_stream.remote .screen-type');
|
||||||
// const muteAudioButton = await utils.waitForElement('#mic-btn');
|
await utils.toggleStreamPin('#pin-btn');
|
||||||
// await muteAudioButton.click();
|
await browser.sleep(500);
|
||||||
|
}
|
||||||
// await utils.waitForElement('#status-mic');
|
|
||||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
// Verify A's screen is now pinned in B's view
|
||||||
|
pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||||
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
|
console.log(`[Tab B] After manually pinning A's screen: ${pinnedCountB} pinned stream(s)`);
|
||||||
// expect(isAudioEnabled).toBeFalse();
|
expect(pinnedCountB).toEqual(1, "Only A's screen should be pinned");
|
||||||
|
|
||||||
// // Unmute camera
|
// Participant C joins the room
|
||||||
// await muteVideoButton.click();
|
const tab3 = await utils.openTab(fixedUrl);
|
||||||
|
await browser.switchTo().window(tab3[2]);
|
||||||
// await utils.waitForElement('.camera-type');
|
await utils.checkLayoutPresent();
|
||||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
await browser.sleep(1500);
|
||||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(2);
|
|
||||||
|
// Switch back to B's tab
|
||||||
// isAudioEnabled = await browser.executeScript(getAudioScript('camera-type'));
|
await browser.switchTo().window(tabs[1]);
|
||||||
// expect(isAudioEnabled).toBeFalse();
|
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.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ export const TestAppConfig: BrowserConfig = {
|
||||||
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
|
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
|
||||||
browserName: 'chrome',
|
browserName: 'chrome',
|
||||||
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
|
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 = {
|
export const NestedConfig: BrowserConfig = {
|
||||||
|
|
@ -64,13 +65,14 @@ export const NestedConfig: BrowserConfig = {
|
||||||
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
|
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
|
||||||
browserName: 'Chrome',
|
browserName: 'Chrome',
|
||||||
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
|
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() {
|
export function getBrowserOptionsWithoutDevices() {
|
||||||
if (LAUNCH_MODE === 'CI') {
|
if (LAUNCH_MODE === 'CI') {
|
||||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI);
|
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI) as chrome.Options;
|
||||||
} else {
|
} else {
|
||||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices);
|
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices) as chrome.Options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Builder, ILocation, IRectangle, ISize, WebDriver } from 'selenium-webdriver';
|
import { Builder, IRectangle, WebDriver } from 'selenium-webdriver';
|
||||||
import { TestAppConfig } from './selenium.conf';
|
import { TestAppConfig } from './selenium.conf';
|
||||||
import { OpenViduComponentsPO } from './utils.po.test';
|
import { OpenViduComponentsPO } from './utils.po.test';
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
||||||
|
|
||||||
await utils.clickOn('#screenshare-btn');
|
await utils.clickOn('#screenshare-btn');
|
||||||
await browser.sleep(1000);
|
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('.OV_stream')).toEqual(2);
|
||||||
expect(await utils.getNumberOfElements('video')).toEqual(1); //screen sharse video
|
expect(await utils.getNumberOfElements('video')).toEqual(1); //screen sharse video
|
||||||
expect(await utils.getNumberOfElements('audio')).toEqual(1); //screen share audio
|
expect(await utils.getNumberOfElements('audio')).toEqual(1); //screen share audio
|
||||||
|
|
@ -106,7 +106,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
||||||
|
|
||||||
await utils.clickOn('#screenshare-btn');
|
await utils.clickOn('#screenshare-btn');
|
||||||
await browser.sleep(1000);
|
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('.OV_stream')).toEqual(2);
|
||||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||||
expect(await utils.getNumberOfElements('audio')).toEqual(2); //screen share audio and local audio
|
expect(await utils.getNumberOfElements('audio')).toEqual(2); //screen share audio and local audio
|
||||||
|
|
@ -221,7 +221,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
||||||
|
|
||||||
await utils.clickOn('#screenshare-btn');
|
await utils.clickOn('#screenshare-btn');
|
||||||
await browser.sleep(1000);
|
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('.OV_stream')).toEqual(3);
|
||||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||||
expect(await utils.getNumberOfElements('audio')).toEqual(1); // screen share audios
|
expect(await utils.getNumberOfElements('audio')).toEqual(1); // screen share audios
|
||||||
|
|
@ -260,7 +260,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
||||||
|
|
||||||
await utils.clickOn('#screenshare-btn');
|
await utils.clickOn('#screenshare-btn');
|
||||||
await browser.sleep(1000);
|
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('.OV_stream')).toEqual(3);
|
||||||
expect(await utils.getNumberOfElements('video')).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
|
expect(await utils.getNumberOfElements('audio')).toEqual(3); // screen share audios and local audio and remote audio
|
||||||
|
|
@ -304,7 +304,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
||||||
|
|
||||||
await utils.clickOn('#screenshare-btn');
|
await utils.clickOn('#screenshare-btn');
|
||||||
await browser.sleep(500);
|
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('.OV_stream')).toEqual(4);
|
||||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||||
expect(await utils.getNumberOfElements('audio')).toEqual(2); // screen share audios
|
expect(await utils.getNumberOfElements('audio')).toEqual(2); // screen share audios
|
||||||
|
|
@ -681,12 +681,47 @@ describe('Stream UI controls and interaction features', () => {
|
||||||
|
|
||||||
await browser.switchTo().window(tabs[0]);
|
await browser.switchTo().window(tabs[0]);
|
||||||
|
|
||||||
await utils.waitForElement('.OV_stream.remote.speaking');
|
// Wait with retries for audio detection to appear (handles timing issues)
|
||||||
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
|
const maxRetries = 5;
|
||||||
|
const retryInterval = 1000;
|
||||||
|
let audioDetected = false;
|
||||||
|
|
||||||
// Check only one element is marked as speaker due to the local participant is muted
|
for (let i = 0; i < maxRetries && !audioDetected; i++) {
|
||||||
await utils.waitForElement('.OV_stream.speaking');
|
await browser.sleep(retryInterval);
|
||||||
expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
|
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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { PNG } from 'pngjs';
|
|
||||||
import pixelmatch from 'pixelmatch';
|
import pixelmatch from 'pixelmatch';
|
||||||
|
import { PNG } from 'pngjs';
|
||||||
|
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
|
||||||
type PNGWithMetadata = PNG & { data: Buffer };
|
type PNGWithMetadata = PNG & { data: Buffer };
|
||||||
|
|
||||||
export class OpenViduComponentsPO {
|
export class OpenViduComponentsPO {
|
||||||
|
|
@ -279,4 +279,50 @@ export class OpenViduComponentsPO {
|
||||||
// fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
// fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
||||||
// expect(numDiffPixels).to.be.greaterThan(500, 'The virtual background was not applied correctly');
|
// 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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,63 +1,65 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "20.3.4",
|
"@angular/animations": "20.3.15",
|
||||||
"@angular/cdk": "20.2.8",
|
"@angular/cdk": "20.2.14",
|
||||||
"@angular/common": "20.3.4",
|
"@angular/common": "20.3.15",
|
||||||
"@angular/core": "20.3.4",
|
"@angular/core": "20.3.15",
|
||||||
"@angular/forms": "20.3.4",
|
"@angular/forms": "20.3.15",
|
||||||
"@angular/material": "20.2.8",
|
"@angular/material": "20.2.14",
|
||||||
"@angular/platform-browser": "20.3.4",
|
"@angular/platform-browser": "20.3.15",
|
||||||
"@angular/platform-browser-dynamic": "20.3.4",
|
"@angular/platform-browser-dynamic": "20.3.15",
|
||||||
"@angular/router": "20.3.4",
|
"@angular/router": "20.3.15",
|
||||||
"@livekit/track-processors": "^0.5.6",
|
"@livekit/track-processors": "0.7.0",
|
||||||
"@types/dom-mediacapture-transform": "^0.1.11",
|
"@types/dom-mediacapture-transform": "0.1.11",
|
||||||
"autolinker": "4.0.0",
|
"autolinker": "4.1.5",
|
||||||
"livekit-client": "2.11.4",
|
"livekit-client": "2.16.0",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"tslib": "2.7.0",
|
"tslib": "2.8.1",
|
||||||
"zone.js": "^0.15.1"
|
"zone.js": "0.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "20.3.5",
|
"@angular-devkit/build-angular": "20.3.13",
|
||||||
"@angular/cli": "20.3.5",
|
"@angular/cli": "20.3.13",
|
||||||
"@angular/compiler": "20.3.4",
|
"@angular/compiler": "20.3.15",
|
||||||
"@angular/compiler-cli": "20.3.4",
|
"@angular/compiler-cli": "20.3.15",
|
||||||
"@compodoc/compodoc": "^1.1.25",
|
"@compodoc/compodoc": "1.1.32",
|
||||||
"@types/jasmine": "^5.1.4",
|
"@types/jasmine": "5.1.13",
|
||||||
"@types/node": "^20.12.14",
|
"@types/node": "20.19.26",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "6.0.5",
|
||||||
"@types/selenium-webdriver": "4.1.16",
|
"@types/selenium-webdriver": "4.1.29",
|
||||||
"@types/ws": "^8.5.12",
|
"@types/ws": "8.5.14",
|
||||||
"chromedriver": "141.0.1",
|
"chromedriver": "143.0.0",
|
||||||
"concat": "^1.0.3",
|
"concat": "1.0.3",
|
||||||
"cross-env": "^7.0.3",
|
"cpx": "1.5.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-config-prettier": "9.1.0",
|
||||||
|
"eslint-plugin-prettier": "5.2.1",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"husky": "^9.1.6",
|
"husky": "9.1.6",
|
||||||
"jasmine": "^5.3.1",
|
"jasmine": "5.3.1",
|
||||||
"jasmine-core": "5.3.0",
|
"jasmine-core": "5.3.0",
|
||||||
"jasmine-spec-reporter": "7.0.0",
|
"jasmine-spec-reporter": "7.0.0",
|
||||||
"karma": "^6.4.4",
|
"karma": "6.4.4",
|
||||||
"karma-chrome-launcher": "3.2.0",
|
"karma-chrome-launcher": "3.2.0",
|
||||||
"karma-coverage": "^2.2.1",
|
"karma-coverage": "2.2.1",
|
||||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||||
"karma-jasmine": "5.1.0",
|
"karma-jasmine": "5.1.0",
|
||||||
"karma-jasmine-html-reporter": "2.1.0",
|
"karma-jasmine-html-reporter": "2.1.0",
|
||||||
"karma-junit-reporter": "2.0.1",
|
"karma-junit-reporter": "2.0.1",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"karma-notify-reporter": "1.3.0",
|
"karma-notify-reporter": "1.3.0",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "15.2.10",
|
||||||
"ng-packagr": "20.3.0",
|
"ng-packagr": "20.3.0",
|
||||||
"npm-watch": "^0.13.0",
|
"npm-watch": "0.13.0",
|
||||||
"pixelmatch": "^7.1.0",
|
"pixelmatch": "7.1.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "7.0.0",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"selenium-webdriver": "4.36.0",
|
"rimraf": "6.0.1",
|
||||||
|
"selenium-webdriver": "4.39.0",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"tslint": "6.1.3",
|
"tslint": "6.1.3",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"webpack-bundle-analyzer": "^4.10.2"
|
"webpack-bundle-analyzer": "4.10.2"
|
||||||
},
|
},
|
||||||
"name": "openvidu-components-testapp",
|
"name": "openvidu-components-testapp",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -83,7 +85,7 @@
|
||||||
"doc:serve": "npx compodoc -c ../openvidu-components-angular/projects/openvidu-components-angular/doc/.compodocrc.json --serve --port 7000",
|
"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",
|
"doc:serve-watch": "npm-watch doc:serve",
|
||||||
"lib:serve": "ng build openvidu-components-angular --watch",
|
"lib:serve": "ng build openvidu-components-angular --watch",
|
||||||
"lib:build": "ng build openvidu-components-angular --configuration production",
|
"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:pack": "cd ./dist/openvidu-components-angular && npm pack",
|
||||||
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/frontend",
|
"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",
|
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||||
"dest": "../../dist/openvidu-components-angular",
|
"dest": "./dist",
|
||||||
"lib": {
|
"lib": {
|
||||||
"entryFile": "src/public-api.ts"
|
"entryFile": "src/public-api.ts"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,418 +0,0 @@
|
||||||
{
|
|
||||||
"name": "openvidu-components-angular",
|
|
||||||
"version": "3.4.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "openvidu-components-angular",
|
|
||||||
"version": "3.4.0",
|
|
||||||
"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,5 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "openvidu-components-angular",
|
"name": "openvidu-components-angular",
|
||||||
|
"main": "dist/fesm2022/openvidu-components-angular.mjs",
|
||||||
|
"module": "dist/fesm2022/openvidu-components-angular.mjs",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -12,8 +15,8 @@
|
||||||
"@angular/material": "^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",
|
"autolinker": "^4.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"livekit-client": "^2.15.0",
|
"livekit-client": "^2.16.0",
|
||||||
"@livekit/track-processors": "^0.6.0"
|
"@livekit/track-processors": "^0.7.0"
|
||||||
},
|
},
|
||||||
"version": "3.4.0"
|
"version": "3.4.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ import { Component } from '@angular/core';
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ov-audio-wave',
|
selector: 'ov-audio-wave',
|
||||||
template: `<div class="audio-container">
|
template: `
|
||||||
|
<div class="audio-container audio-wave-indicator">
|
||||||
<div class="stick normal play"></div>
|
<div class="stick normal play"></div>
|
||||||
<div class="stick loud play"></div>
|
<div class="stick loud play"></div>
|
||||||
<div class="stick normal play"></div>
|
<div class="stick normal play"></div>
|
||||||
</div>`,
|
</div>
|
||||||
|
`,
|
||||||
styleUrls: ['./audio-wave.component.scss'],
|
styleUrls: ['./audio-wave.component.scss'],
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
.poster {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--ov-video-background, var(--ov-primary-action-color));
|
|
||||||
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: var(--ov-video-background, var(--ov-text-primary-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
#poster-text {
|
|
||||||
padding: 0px !important;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 40px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ov-avatar-profile',
|
|
||||||
template: `
|
|
||||||
<div class="poster" id="video-poster">
|
|
||||||
@if (letter) {
|
|
||||||
<div class="initial" [ngStyle]="{ 'background-color': color }">
|
|
||||||
<span id="poster-text">{{ letter }}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
styleUrls: ['./avatar-profile.component.scss'],
|
|
||||||
standalone: false
|
|
||||||
})
|
|
||||||
export class AvatarProfileComponent {
|
|
||||||
letter: string;
|
|
||||||
|
|
||||||
@Input()
|
|
||||||
set name(name: string) {
|
|
||||||
if (name) this.letter = name[0];
|
|
||||||
}
|
|
||||||
@Input() color;
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
}
|
|
||||||
|
|
@ -4,13 +4,15 @@
|
||||||
#localLayoutElement
|
#localLayoutElement
|
||||||
*ngFor="let track of localParticipant.tracks; trackBy: trackParticipantElement"
|
*ngFor="let track of localParticipant.tracks; trackBy: trackParticipantElement"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
|
local_participant: true,
|
||||||
OV_root: !track.isAudioTrack && !track.isMinimized,
|
OV_root: !track.isAudioTrack && !track.isMinimized,
|
||||||
OV_publisher: !track.isAudioTrack && !track.isMinimized,
|
OV_publisher: !track.isAudioTrack && !track.isMinimized,
|
||||||
OV_minimized: track.isMinimized,
|
OV_minimized: track.isMinimized,
|
||||||
OV_big: track.isPinned,
|
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
|
cdkDrag
|
||||||
cdkDragBoundary=".layout"
|
cdkDragBoundary=".layout"
|
||||||
[cdkDragDisabled]="!track.isMinimized"
|
[cdkDragDisabled]="!track.isMinimized"
|
||||||
|
|
@ -27,11 +29,13 @@
|
||||||
<div
|
<div
|
||||||
*ngFor="let track of remoteParticipants | tracks; trackBy: trackParticipantElement"
|
*ngFor="let track of remoteParticipants | tracks; trackBy: trackParticipantElement"
|
||||||
class="remote-participant"
|
class="remote-participant"
|
||||||
|
[id]="'participant-' + track.participant.identity"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
OV_root: !track.isAudioTrack,
|
OV_root: !track.isAudioTrack,
|
||||||
OV_publisher: !track.isAudioTrack,
|
OV_publisher: !track.isAudioTrack,
|
||||||
OV_big: track.isPinned,
|
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>
|
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,13 @@ import { Track } from 'livekit-client';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ov-media-element',
|
selector: 'ov-media-element',
|
||||||
template: `
|
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>
|
<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>
|
<audio #audioElement *ngIf="_track?.kind === 'audio'" [attr.id]="_track?.sid"></audio>
|
||||||
`,
|
`,
|
||||||
|
|
@ -29,10 +35,11 @@ export class MediaElementComponent implements AfterViewInit, OnDestroy {
|
||||||
private _muted: boolean = false;
|
private _muted: boolean = false;
|
||||||
private previousTrack: Track | null = null;
|
private previousTrack: Track | null = null;
|
||||||
|
|
||||||
@Input() showAvatar: boolean;
|
@Input() showAvatar: boolean = false;
|
||||||
@Input() avatarColor: string;
|
@Input() avatarColor: string = '#000000';
|
||||||
@Input() avatarName: string;
|
@Input() avatarName: string = 'User';
|
||||||
@Input() isLocal: boolean;
|
@Input() isLocal: boolean = false;
|
||||||
|
@Input() hasEncryptionError: boolean = false;
|
||||||
|
|
||||||
@ViewChild('videoElement', { static: false })
|
@ViewChild('videoElement', { static: false })
|
||||||
set videoElement(element: ElementRef) {
|
set videoElement(element: ElementRef) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="panel-container" id="activities-container">
|
<div class="panel-container" id="activities-container">
|
||||||
<div class="panel-header-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()">
|
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||||
<mat-icon>close</mat-icon>
|
<mat-icon>close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToMessages() {
|
private subscribeToMessages() {
|
||||||
this.chatService.messagesObs.pipe(takeUntil(this.destroy$)).subscribe((messages: ChatMessage[]) => {
|
this.chatService.chatMessages$.pipe(takeUntil(this.destroy$)).subscribe((messages: ChatMessage[]) => {
|
||||||
this.messageList = messages;
|
this.messageList = messages;
|
||||||
if (this.panelService.isChatPanelOpened()) {
|
if (this.panelService.isChatPanelOpened()) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<mat-list>
|
<mat-list>
|
||||||
<mat-list-item>
|
<mat-list-item>
|
||||||
<!-- Main participant container with improved structure -->
|
<!-- Main participant container with improved structure -->
|
||||||
<div class="participant-container" [attr.data-participant-id]="_participant?.sid">
|
<div class="participant-container" [attr.data-participant-id]="_participant?.sid" [attr.data-participant-name]="participantDisplayName">
|
||||||
<!-- Avatar section with dynamic color -->
|
<!-- Avatar section with dynamic color -->
|
||||||
<div
|
<div
|
||||||
class="participant-avatar"
|
class="participant-avatar"
|
||||||
[style.background-color]="_participant?.colorProfile"
|
[style.background-color]="_participant?.colorProfile"
|
||||||
[attr.aria-label]="'Avatar for ' + participantDisplayName"
|
[attr.aria-label]="'Avatar for ' + participantDisplayName"
|
||||||
>
|
>
|
||||||
<mat-icon>person</mat-icon>
|
<mat-icon>{{ _participant.hasEncryptionError ? 'lock_person' : 'person' }}</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content section with name and status -->
|
<!-- Content section with name and status -->
|
||||||
|
|
@ -28,14 +28,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (!_participant.hasEncryptionError) {
|
||||||
<div class="participant-subtitle">
|
<div class="participant-subtitle">
|
||||||
<span class="status-indicator">
|
<span class="status-indicator">
|
||||||
{{ _participant | tracksPublishedTypes }}
|
{{ _participant | tracksPublishedTypes }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons section -->
|
<!-- Action buttons section -->
|
||||||
|
@if (!_participant.hasEncryptionError) {
|
||||||
<div class="participant-action-buttons">
|
<div class="participant-action-buttons">
|
||||||
<!-- Mute/Unmute button for remote participants -->
|
<!-- Mute/Unmute button for remote participants -->
|
||||||
<button
|
<button
|
||||||
|
|
@ -52,7 +55,9 @@
|
||||||
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
||||||
"
|
"
|
||||||
[matTooltip]="
|
[matTooltip]="
|
||||||
_participant?.isMutedForcibly ? ('PANEL.PARTICIPANTS.UNMUTE' | translate) : ('PANEL.PARTICIPANTS.MUTE' | translate)
|
_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_up</mat-icon>
|
||||||
|
|
@ -64,6 +69,7 @@
|
||||||
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,12 @@
|
||||||
</mat-list>
|
</mat-list>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<!-- Additional elements injected via directive -->
|
||||||
|
@if (generalAdditionalElementsTemplate) {
|
||||||
|
<div class="additional-elements-section">
|
||||||
|
<ng-container *ngTemplateOutlet="generalAdditionalElementsTemplate"></ng-container>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
|
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
|
||||||
<ov-video-devices-select
|
<ov-video-devices-select
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
import { Component, ContentChild, EventEmitter, OnInit, Output, TemplateRef } from '@angular/core';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model';
|
import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model';
|
||||||
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
|
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
|
||||||
|
|
@ -7,6 +7,7 @@ import { PlatformService } from '../../../services/platform/platform.service';
|
||||||
import { ViewportService } from '../../../services/viewport/viewport.service';
|
import { ViewportService } from '../../../services/viewport/viewport.service';
|
||||||
import { CustomDevice } from '../../../models/device.model';
|
import { CustomDevice } from '../../../models/device.model';
|
||||||
import { LangOption } from '../../../models/lang.model';
|
import { LangOption } from '../../../models/lang.model';
|
||||||
|
import { SettingsPanelGeneralAdditionalElementsDirective } from '../../../directives/template/internals.directive';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -23,6 +24,14 @@ export class SettingsPanelComponent implements OnInit {
|
||||||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
||||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||||
@Output() onLangChanged = new EventEmitter<LangOption>();
|
@Output() onLangChanged = new EventEmitter<LangOption>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* ContentChild for custom elements in general section
|
||||||
|
*/
|
||||||
|
@ContentChild(SettingsPanelGeneralAdditionalElementsDirective)
|
||||||
|
externalGeneralAdditionalElements!: SettingsPanelGeneralAdditionalElementsDirective;
|
||||||
|
|
||||||
settingsOptions: typeof PanelSettingsOptions = PanelSettingsOptions;
|
settingsOptions: typeof PanelSettingsOptions = PanelSettingsOptions;
|
||||||
selectedOption: PanelSettingsOptions = PanelSettingsOptions.GENERAL;
|
selectedOption: PanelSettingsOptions = PanelSettingsOptions.GENERAL;
|
||||||
showCameraButton: boolean = true;
|
showCameraButton: boolean = true;
|
||||||
|
|
@ -32,6 +41,14 @@ export class SettingsPanelComponent implements OnInit {
|
||||||
isMobile: boolean = false;
|
isMobile: boolean = false;
|
||||||
private destroy$ = new Subject<void>();
|
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(
|
constructor(
|
||||||
private panelService: PanelService,
|
private panelService: PanelService,
|
||||||
private platformService: PlatformService,
|
private platformService: PlatformService,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { ActionService } from '../../services/action/action.service';
|
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 { ChatService } from '../../services/chat/chat.service';
|
||||||
import { ChatServiceMock } from '../../services/chat/chat.service.mock';
|
import { ChatServiceMock } from '../../services/chat/chat.service.mock';
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '.
|
||||||
import { RecordingStatus } from '../../models/recording.model';
|
import { RecordingStatus } from '../../models/recording.model';
|
||||||
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
|
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
|
||||||
import { ViewportService } from '../../services/viewport/viewport.service';
|
import { ViewportService } from '../../services/viewport/viewport.service';
|
||||||
|
import { E2eeService } from '../../services/e2ee/e2ee.service';
|
||||||
|
import { safeJsonParse } from '../../utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -138,7 +140,8 @@ export class SessionComponent implements OnInit, OnDestroy {
|
||||||
private backgroundService: VirtualBackgroundService,
|
private backgroundService: VirtualBackgroundService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private templateManagerService: TemplateManagerService,
|
private templateManagerService: TemplateManagerService,
|
||||||
protected viewportService: ViewportService
|
protected viewportService: ViewportService,
|
||||||
|
private e2eeService: E2eeService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('SessionComponent');
|
this.log = this.loggerSrv.get('SessionComponent');
|
||||||
this.setupTemplates();
|
this.setupTemplates();
|
||||||
|
|
@ -230,7 +233,8 @@ export class SessionComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.subscribeToCaptionLanguage();
|
// this.subscribeToCaptionLanguage();
|
||||||
this.subcribeToActiveSpeakersChanged();
|
this.subscribeToEncryptionErrors();
|
||||||
|
this.subscribeToActiveSpeakersChanged();
|
||||||
this.subscribeToParticipantConnected();
|
this.subscribeToParticipantConnected();
|
||||||
this.subscribeToTrackSubscribed();
|
this.subscribeToTrackSubscribed();
|
||||||
this.subscribeToTrackUnsubscribed();
|
this.subscribeToTrackUnsubscribed();
|
||||||
|
|
@ -261,7 +265,18 @@ export class SessionComponent implements OnInit, OnDestroy {
|
||||||
this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error);
|
this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subcribeToActiveSpeakersChanged() {
|
|
||||||
|
protected subscribeToEncryptionErrors() {
|
||||||
|
this.room.on(RoomEvent.EncryptionError, (error: Error, participant?: Participant) => {
|
||||||
|
if (!participant) {
|
||||||
|
this.log.w('Encryption error received without participant info:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.participantService.setEncryptionError(participant.sid, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected subscribeToActiveSpeakersChanged() {
|
||||||
this.room.on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {
|
this.room.on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {
|
||||||
this.participantService.setSpeaking(speakers);
|
this.participantService.setSpeaking(speakers);
|
||||||
});
|
});
|
||||||
|
|
@ -450,12 +465,54 @@ export class SessionComponent implements OnInit, OnDestroy {
|
||||||
private subscribeToDataMessage() {
|
private subscribeToDataMessage() {
|
||||||
this.room.on(
|
this.room.on(
|
||||||
RoomEvent.DataReceived,
|
RoomEvent.DataReceived,
|
||||||
(payload: Uint8Array, participant?: RemoteParticipant, _?: DataPacket_Kind, topic?: string) => {
|
async (payload: Uint8Array, participant?: RemoteParticipant, _?: DataPacket_Kind, topic?: string) => {
|
||||||
const event = JSON.parse(new TextDecoder().decode(payload));
|
try {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const fromServer = participant === undefined;
|
||||||
|
// Validate source and resolve participant info
|
||||||
|
const storedParticipant = participant
|
||||||
|
? this.participantService.getRemoteParticipantBySid(participant.sid || '')
|
||||||
|
: undefined;
|
||||||
|
if (participant && !storedParticipant) {
|
||||||
|
this.log.w('DataReceived from unknown participant', participant);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fromServer && !participant) {
|
||||||
|
this.log.w('DataReceived from unknown source', payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const participantIdentity = storedParticipant?.identity || '';
|
||||||
|
const participantName = storedParticipant?.name || '';
|
||||||
|
|
||||||
|
if (this.e2eeService.isEnabled) {
|
||||||
|
payload = await this.decryptIfNeeded(topic, payload, participantIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawText = decoder.decode(payload);
|
||||||
|
this.log.d('DataReceived (raw)', { topic });
|
||||||
|
|
||||||
|
const eventMessage = safeJsonParse(rawText);
|
||||||
|
if (!eventMessage) {
|
||||||
|
this.log.w('Discarding data: malformed JSON', rawText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.log.d(`Data event received: ${topic}`);
|
this.log.d(`Data event received: ${topic}`);
|
||||||
|
|
||||||
|
// Dispatch handling
|
||||||
|
this.handleDataEvent(topic, eventMessage, participantName || participantIdentity || 'Unknown');
|
||||||
|
} catch (err) {
|
||||||
|
this.log.e('Unhandled error processing DataReceived', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDataEvent(topic: string | undefined, event: any, participantName: string) {
|
||||||
|
// Handle the event based on topic
|
||||||
switch (topic) {
|
switch (topic) {
|
||||||
case DataTopic.CHAT:
|
case DataTopic.CHAT:
|
||||||
const participantName = participant?.name || 'Unknown';
|
|
||||||
this.chatService.addRemoteMessage(event.message, participantName);
|
this.chatService.addRemoteMessage(event.message, participantName);
|
||||||
break;
|
break;
|
||||||
case DataTopic.RECORDING_STARTING:
|
case DataTopic.RECORDING_STARTING:
|
||||||
|
|
@ -522,7 +579,18 @@ export class SessionComponent implements OnInit, OnDestroy {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
private async decryptIfNeeded(topic: string | undefined, payload: Uint8Array, identity: string): Promise<Uint8Array> {
|
||||||
|
if (topic === DataTopic.CHAT && this.e2eeService.isEnabled) {
|
||||||
|
try {
|
||||||
|
return await this.e2eeService.decryptOrMask(payload, identity, JSON.stringify({ message: '******' }));
|
||||||
|
} catch (e) {
|
||||||
|
this.log.e('Error decrypting payload, using masked fallback', e);
|
||||||
|
// In case of decryption error, return a masked JSON so subsequent parsing won't crash
|
||||||
|
return new TextEncoder().encode(JSON.stringify({ message: '******' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToReconnection() {
|
private subscribeToReconnection() {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<mat-icon class="input-icon">person</mat-icon>
|
<mat-icon class="input-icon">person</mat-icon>
|
||||||
<input
|
<input
|
||||||
id="name-input"
|
id="participant-name-input"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
[(ngModel)]="name"
|
[(ngModel)]="name"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
#streamContainer
|
#streamContainer
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
*ngIf="!isMinimal && showParticipantName"
|
*ngIf="!isMinimal && showParticipantName && !_track.isAudioTrack || (_track.isAudioTrack && _track.participant.onlyHasAudioTracks)"
|
||||||
id="participant-name-container"
|
id="participant-name-container"
|
||||||
class="participant-name"
|
class="participant-name"
|
||||||
[class.fullscreen]="isFullscreen"
|
[class.fullscreen]="isFullscreen"
|
||||||
|
|
@ -34,8 +34,10 @@
|
||||||
[avatarName]="_track.participant.name"
|
[avatarName]="_track.participant.name"
|
||||||
[muted]="_track.isMutedForcibly"
|
[muted]="_track.isMutedForcibly"
|
||||||
[isLocal]="_track.participant.isLocal"
|
[isLocal]="_track.participant.isLocal"
|
||||||
|
[hasEncryptionError]="_track.participant.hasEncryptionError"
|
||||||
></ov-media-element>
|
></ov-media-element>
|
||||||
|
|
||||||
|
@if (!_track.participant.hasEncryptionError) {
|
||||||
<div class="status-icons">
|
<div class="status-icons">
|
||||||
<mat-icon id="status-mic" fontIcon="mic_off" *ngIf="!_track.participant?.isMicrophoneEnabled"></mat-icon>
|
<mat-icon id="status-mic" fontIcon="mic_off" *ngIf="!_track.participant?.isMicrophoneEnabled"></mat-icon>
|
||||||
<mat-icon id="status-muted-forcibly" fontIcon="volume_off" *ngIf="_track.isMutedForcibly"></mat-icon>
|
<mat-icon id="status-muted-forcibly" fontIcon="volume_off" *ngIf="_track.isMutedForcibly"></mat-icon>
|
||||||
|
|
@ -77,4 +79,5 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,25 +47,28 @@
|
||||||
.stream-video-controls {
|
.stream-video-controls {
|
||||||
background-color: var(--ov-primary-action-color);
|
background-color: var(--ov-primary-action-color);
|
||||||
border-radius: var(--ov-video-radius);
|
border-radius: var(--ov-video-radius);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: 50px;
|
height: 44px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
margin: auto;
|
bottom: 12px;
|
||||||
bottom: 0;
|
left: 50%;
|
||||||
right: 0;
|
transform: translateX(-50%);
|
||||||
left: 0;
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
top: 0;
|
padding: 4px 8px;
|
||||||
// border: 2px solid var(--ov-text-primary-color);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
button {
|
button {
|
||||||
color: var(--ov-text-primary-color);
|
color: var(--ov-text-primary-color);
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-video-controls:hover {
|
.stream-video-controls:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-icons {
|
.status-icons {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
|
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
|
||||||
|
import { Track } from 'livekit-client';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
import { ParticipantTrackPublication } from '../../models/participant.model';
|
||||||
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
|
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
|
||||||
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
||||||
import { LayoutService } from '../../services/layout/layout.service';
|
import { LayoutService } from '../../services/layout/layout.service';
|
||||||
import { ParticipantService } from '../../services/participant/participant.service';
|
import { ParticipantService } from '../../services/participant/participant.service';
|
||||||
import { Track } from 'livekit-client';
|
|
||||||
import { ParticipantTrackPublication } from '../../models/participant.model';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The **StreamComponent** is hosted inside of the {@link LayoutComponent}.
|
* The **StreamComponent** is hosted inside of the {@link LayoutComponent}.
|
||||||
|
|
@ -93,7 +93,7 @@ export class StreamComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private _streamContainer: ElementRef;
|
private _streamContainer: ElementRef;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private readonly HOVER_TIMEOUT = 3000;
|
private readonly HOVER_TIMEOUT = 2000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @ignore
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,11 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Additional menu items injected via directive -->
|
||||||
|
@if (moreOptionsAdditionalMenuItemsTemplate) {
|
||||||
|
<ng-container *ngTemplateOutlet="moreOptionsAdditionalMenuItemsTemplate"></ng-container>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Divider before settings -->
|
<!-- Divider before settings -->
|
||||||
@if (showSettingsButton) {
|
@if (showSettingsButton) {
|
||||||
<mat-divider class="divider"></mat-divider>
|
<mat-divider class="divider"></mat-divider>
|
||||||
|
|
@ -198,7 +203,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Leave session button -->
|
<!-- Leave session button -->
|
||||||
@if (showLeaveButtonDirect()) {
|
@if (showLeaveButton) {
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
(click)="onLeaveClick()"
|
(click)="onLeaveClick()"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Component, EventEmitter, Input, Output, TemplateRef, computed, inject } from '@angular/core';
|
import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef, computed, inject } from '@angular/core';
|
||||||
import { RecordingStatus } from '../../../models/recording.model';
|
import { RecordingStatus } from '../../../models/recording.model';
|
||||||
import { BroadcastingStatus } from '../../../models/broadcasting.model';
|
import { BroadcastingStatus } from '../../../models/broadcasting.model';
|
||||||
import { ToolbarAdditionalButtonsPosition } from '../../../models/toolbar.model';
|
import { ToolbarAdditionalButtonsPosition } from '../../../models/toolbar.model';
|
||||||
import { ViewportService } from '../../../services/viewport/viewport.service';
|
import { ViewportService } from '../../../services/viewport/viewport.service';
|
||||||
|
import { ToolbarMoreOptionsAdditionalMenuItemsDirective } from '../../../directives/template/internals.directive';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -71,6 +72,21 @@ export class ToolbarMediaButtonsComponent {
|
||||||
// Leave button template
|
// Leave button template
|
||||||
@Input() toolbarLeaveButtonTemplate: TemplateRef<any> | null = null;
|
@Input() toolbarLeaveButtonTemplate: TemplateRef<any> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* ContentChild for custom menu items in more options menu
|
||||||
|
*/
|
||||||
|
@ContentChild(ToolbarMoreOptionsAdditionalMenuItemsDirective)
|
||||||
|
externalMoreOptionsAdditionalMenuItems!: ToolbarMoreOptionsAdditionalMenuItemsDirective;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Gets the template for additional menu items in more options
|
||||||
|
*/
|
||||||
|
get moreOptionsAdditionalMenuItemsTemplate(): TemplateRef<any> | undefined {
|
||||||
|
return this.externalMoreOptionsAdditionalMenuItems?.template;
|
||||||
|
}
|
||||||
|
|
||||||
// Status enums for template usage
|
// Status enums for template usage
|
||||||
_recordingStatus = RecordingStatus;
|
_recordingStatus = RecordingStatus;
|
||||||
_broadcastingStatus = BroadcastingStatus;
|
_broadcastingStatus = BroadcastingStatus;
|
||||||
|
|
@ -96,9 +112,6 @@ export class ToolbarMediaButtonsComponent {
|
||||||
// More options button - always visible when not minimal
|
// More options button - always visible when not minimal
|
||||||
readonly showMoreOptionsButtonDirect = computed(() => this.showMoreOptionsButton && !this.isMinimal);
|
readonly showMoreOptionsButtonDirect = computed(() => this.showMoreOptionsButton && !this.isMinimal);
|
||||||
|
|
||||||
// Leave button
|
|
||||||
readonly showLeaveButtonDirect = computed(() => this.showLeaveButton);
|
|
||||||
|
|
||||||
// Check if there are active features that should show a badge on More Options
|
// Check if there are active features that should show a badge on More Options
|
||||||
readonly hasActiveFeatures = computed(
|
readonly hasActiveFeatures = computed(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,12 @@
|
||||||
(captionsToggled)="onCaptionsToggle()"
|
(captionsToggled)="onCaptionsToggle()"
|
||||||
(settingsToggled)="toggleSettings()"
|
(settingsToggled)="toggleSettings()"
|
||||||
(leaveClicked)="disconnect()"
|
(leaveClicked)="disconnect()"
|
||||||
></ov-toolbar-media-buttons>
|
>
|
||||||
|
<!-- Inject additional menu items via content projection -->
|
||||||
|
<ng-container *ovToolbarMoreOptionsAdditionalMenuItems>
|
||||||
|
<ng-container *ngTemplateOutlet="externalMoreOptionsAdditionalMenuItems?.template"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ov-toolbar-media-buttons>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panel buttons -->
|
<!-- Panel buttons -->
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.servic
|
||||||
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
|
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
|
||||||
import { Room, RoomEvent } from 'livekit-client';
|
import { Room, RoomEvent } from 'livekit-client';
|
||||||
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
|
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
|
||||||
import { LeaveButtonDirective } from '../../directives/template/internals.directive';
|
import { LeaveButtonDirective, ToolbarMoreOptionsAdditionalMenuItemsDirective } from '../../directives/template/internals.directive';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
|
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
|
||||||
|
|
@ -80,6 +80,28 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
| TemplateRef<any>
|
| TemplateRef<any>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Template for additional menu items in the more options menu
|
||||||
|
*/
|
||||||
|
moreOptionsAdditionalMenuItemsTemplate: TemplateRef<any> | undefined;
|
||||||
|
|
||||||
|
private _externalMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@ContentChild(ToolbarMoreOptionsAdditionalMenuItemsDirective)
|
||||||
|
set externalMoreOptionsAdditionalMenuItems(value: ToolbarMoreOptionsAdditionalMenuItemsDirective) {
|
||||||
|
this._externalMoreOptionsAdditionalMenuItems = value;
|
||||||
|
this.setupTemplates();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
get externalMoreOptionsAdditionalMenuItems(): ToolbarMoreOptionsAdditionalMenuItemsDirective | undefined {
|
||||||
|
return this._externalMoreOptionsAdditionalMenuItems;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
|
|
@ -494,7 +516,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
|
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
|
||||||
this._externalAdditionalButtons,
|
this._externalAdditionalButtons,
|
||||||
this._externalAdditionalPanelButtons,
|
this._externalAdditionalPanelButtons,
|
||||||
this._externalLeaveButton
|
this._externalLeaveButton,
|
||||||
|
this._externalMoreOptionsAdditionalMenuItems
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply templates to component properties for backward compatibility
|
// Apply templates to component properties for backward compatibility
|
||||||
|
|
@ -515,6 +538,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
if (this.templateConfig.toolbarLeaveButtonTemplate) {
|
if (this.templateConfig.toolbarLeaveButtonTemplate) {
|
||||||
this.toolbarLeaveButtonTemplate = this.templateConfig.toolbarLeaveButtonTemplate;
|
this.toolbarLeaveButtonTemplate = this.templateConfig.toolbarLeaveButtonTemplate;
|
||||||
}
|
}
|
||||||
|
if (this.templateConfig.toolbarMoreOptionsAdditionalMenuItemsTemplate) {
|
||||||
|
this.moreOptionsAdditionalMenuItemsTemplate = this.templateConfig.toolbarMoreOptionsAdditionalMenuItemsTemplate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -766,7 +792,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToChatMessages() {
|
private subscribeToChatMessages() {
|
||||||
this.chatService.messagesObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((messages) => {
|
this.chatService.chatMessages$.pipe(skip(1), takeUntil(this.destroy$)).subscribe((messages) => {
|
||||||
if (!this.panelService.isChatPanelOpened()) {
|
if (!this.panelService.isChatPanelOpened()) {
|
||||||
this.unreadMessages++;
|
this.unreadMessages++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
@if (hasEncryptionError) {
|
||||||
|
<div class="encryption-error-poster">
|
||||||
|
<div class="encryption-error-content">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 9.9-1"></path>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
<h3>{{ 'ERRORS.E2EE_ERROR_TITLE' | translate }}</h3>
|
||||||
|
<p>{{ 'ERRORS.E2EE_ERROR_CONTENT' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else if (showAvatar) {
|
||||||
|
<div class="poster" id="video-poster">
|
||||||
|
@if (letter) {
|
||||||
|
<div class="initial" [ngStyle]="{ 'background-color': color }">
|
||||||
|
<span id="poster-text">{{ letter }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
.encryption-error-poster {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000000;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: var(--ov-video-radius);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encryption-error-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 300px;
|
||||||
|
color: var(--ov-text-primary-color);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: #dc3545;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(220, 53, 69, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--ov-text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--ov-text-secondary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--ov-video-background, var(--ov-primary-action-color));
|
||||||
|
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: var(--ov-video-background, var(--ov-text-primary-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
#poster-text {
|
||||||
|
padding: 0px !important;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 40px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ov-video-poster',
|
||||||
|
templateUrl: './video-poster.component.html',
|
||||||
|
styleUrl: './video-poster.component.scss',
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class VideoPosterComponent {
|
||||||
|
letter: string = '';
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set nickname(name: string) {
|
||||||
|
if (name) this.letter = name[0];
|
||||||
|
}
|
||||||
|
@Input() color: string = '#000000';
|
||||||
|
@Input() showAvatar: boolean = true;
|
||||||
|
|
||||||
|
@Input() hasEncryptionError: boolean = false;
|
||||||
|
}
|
||||||
|
|
@ -96,6 +96,11 @@
|
||||||
<ng-template #toolbarLeaveButton>
|
<ng-template #toolbarLeaveButton>
|
||||||
<ng-container *ngTemplateOutlet="openviduAngularToolbarLeaveButtonTemplate"></ng-container>
|
<ng-container *ngTemplateOutlet="openviduAngularToolbarLeaveButtonTemplate"></ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Inject additional menu items in toolbar more options -->
|
||||||
|
<ng-container *ovToolbarMoreOptionsAdditionalMenuItems>
|
||||||
|
<ng-container *ngTemplateOutlet="ovToolbarMoreOptionsAdditionalMenuItemsTemplate"></ng-container>
|
||||||
|
</ng-container>
|
||||||
</ov-toolbar>
|
</ov-toolbar>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
@ -127,7 +132,11 @@
|
||||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||||
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
|
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
|
||||||
(onLangChanged)="onLangChanged.emit($event)"
|
(onLangChanged)="onLangChanged.emit($event)"
|
||||||
></ov-settings-panel>
|
>
|
||||||
|
<ng-container *ovSettingsPanelGeneralAdditionalElements>
|
||||||
|
<ng-container *ngTemplateOutlet="ovSettingsPanelGeneralAdditionalElementsTemplate"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ov-settings-panel>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #activitiesPanel>
|
<ng-template #activitiesPanel>
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,12 @@ import {
|
||||||
LayoutAdditionalElementsDirective,
|
LayoutAdditionalElementsDirective,
|
||||||
ParticipantPanelAfterLocalParticipantDirective,
|
ParticipantPanelAfterLocalParticipantDirective,
|
||||||
PreJoinDirective,
|
PreJoinDirective,
|
||||||
LeaveButtonDirective
|
LeaveButtonDirective,
|
||||||
|
SettingsPanelGeneralAdditionalElementsDirective,
|
||||||
|
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||||
} from '../../directives/template/internals.directive';
|
} from '../../directives/template/internals.directive';
|
||||||
import { OpenViduThemeService } from '../../services/theme/theme.service';
|
import { OpenViduThemeService } from '../../services/theme/theme.service';
|
||||||
|
import { E2eeService } from '../../services/e2ee/e2ee.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The **VideoconferenceComponent** is the parent of all OpenVidu components.
|
* The **VideoconferenceComponent** is the parent of all OpenVidu components.
|
||||||
|
|
@ -374,6 +377,38 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
return this._externalLayoutAdditionalElements;
|
return this._externalLayoutAdditionalElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _externalSettingsPanelGeneralAdditionalElements?: SettingsPanelGeneralAdditionalElementsDirective;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@ContentChild(SettingsPanelGeneralAdditionalElementsDirective)
|
||||||
|
set externalSettingsPanelGeneralAdditionalElements(value: SettingsPanelGeneralAdditionalElementsDirective) {
|
||||||
|
this._externalSettingsPanelGeneralAdditionalElements = value;
|
||||||
|
this.setupTemplates();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
get externalSettingsPanelGeneralAdditionalElements(): SettingsPanelGeneralAdditionalElementsDirective | undefined {
|
||||||
|
return this._externalSettingsPanelGeneralAdditionalElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _externalToolbarMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@ContentChild(ToolbarMoreOptionsAdditionalMenuItemsDirective)
|
||||||
|
set externalToolbarMoreOptionsAdditionalMenuItems(value: ToolbarMoreOptionsAdditionalMenuItemsDirective) {
|
||||||
|
this._externalToolbarMoreOptionsAdditionalMenuItems = value;
|
||||||
|
this.setupTemplates();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
get externalToolbarMoreOptionsAdditionalMenuItems(): ToolbarMoreOptionsAdditionalMenuItemsDirective | undefined {
|
||||||
|
return this._externalToolbarMoreOptionsAdditionalMenuItems;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
@ -476,6 +511,14 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
ovLayoutAdditionalElementsTemplate: TemplateRef<any>;
|
ovLayoutAdditionalElementsTemplate: TemplateRef<any>;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
ovSettingsPanelGeneralAdditionalElementsTemplate: TemplateRef<any>;
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
ovToolbarMoreOptionsAdditionalMenuItemsTemplate: TemplateRef<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -712,7 +755,8 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
private actionService: ActionService,
|
private actionService: ActionService,
|
||||||
private libService: OpenViduComponentsConfigService,
|
private libService: OpenViduComponentsConfigService,
|
||||||
private templateManagerService: TemplateManagerService,
|
private templateManagerService: TemplateManagerService,
|
||||||
private themeService: OpenViduThemeService
|
private themeService: OpenViduThemeService,
|
||||||
|
private e2eeService: E2eeService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('VideoconferenceComponent');
|
this.log = this.loggerSrv.get('VideoconferenceComponent');
|
||||||
|
|
||||||
|
|
@ -784,7 +828,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
layout: this.externalLayout,
|
layout: this.externalLayout,
|
||||||
stream: this.externalStream,
|
stream: this.externalStream,
|
||||||
preJoin: this.externalPreJoin,
|
preJoin: this.externalPreJoin,
|
||||||
layoutAdditionalElements: this.externalLayoutAdditionalElements
|
layoutAdditionalElements: this.externalLayoutAdditionalElements,
|
||||||
|
settingsPanelGeneralAdditionalElements: this.externalSettingsPanelGeneralAdditionalElements,
|
||||||
|
toolbarMoreOptionsAdditionalMenuItems: this.externalToolbarMoreOptionsAdditionalMenuItems
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultTemplates: DefaultTemplates = {
|
const defaultTemplates: DefaultTemplates = {
|
||||||
|
|
@ -859,6 +905,12 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
if (this.templateConfig.layoutAdditionalElementsTemplate) {
|
if (this.templateConfig.layoutAdditionalElementsTemplate) {
|
||||||
assignIfChanged('ovLayoutAdditionalElementsTemplate', this.templateConfig.layoutAdditionalElementsTemplate);
|
assignIfChanged('ovLayoutAdditionalElementsTemplate', this.templateConfig.layoutAdditionalElementsTemplate);
|
||||||
}
|
}
|
||||||
|
if (this.templateConfig.settingsPanelGeneralAdditionalElementsTemplate) {
|
||||||
|
assignIfChanged('ovSettingsPanelGeneralAdditionalElementsTemplate', this.templateConfig.settingsPanelGeneralAdditionalElementsTemplate);
|
||||||
|
}
|
||||||
|
if (this.templateConfig.toolbarMoreOptionsAdditionalMenuItemsTemplate) {
|
||||||
|
assignIfChanged('ovToolbarMoreOptionsAdditionalMenuItemsTemplate', this.templateConfig.toolbarMoreOptionsAdditionalMenuItemsTemplate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1042,9 +1094,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
|
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe(async (name: string) => {
|
||||||
if (name) {
|
if (name) {
|
||||||
this.latestParticipantName = name;
|
this.latestParticipantName = await this.e2eeService.decrypt(name);
|
||||||
this.storageSrv.setParticipantName(name);
|
this.storageSrv.setParticipantName(name);
|
||||||
|
|
||||||
// If we're waiting for a participant name to proceed with joining, do it now
|
// If we're waiting for a participant name to proceed with joining, do it now
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
export class CdkOverlayContainer extends OverlayContainer {
|
export class CdkOverlayContainer extends OverlayContainer {
|
||||||
private readonly cdkContainerClass: string = '.cdk-overlay-container';
|
private readonly cdkContainerClass: string = '.cdk-overlay-container';
|
||||||
private defaultSelector = 'body';
|
private defaultSelector = 'body';
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ import {
|
||||||
RecordingActivityViewRecordingsButtonDirective,
|
RecordingActivityViewRecordingsButtonDirective,
|
||||||
RecordingActivityShowRecordingsListDirective,
|
RecordingActivityShowRecordingsListDirective,
|
||||||
ToolbarRoomNameDirective,
|
ToolbarRoomNameDirective,
|
||||||
ShowThemeSelectorDirective
|
ShowThemeSelectorDirective,
|
||||||
|
E2EEKeyDirective
|
||||||
} from './internals.directive';
|
} from './internals.directive';
|
||||||
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
|
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
|
||||||
import {
|
import {
|
||||||
|
|
@ -113,7 +114,8 @@ const directives = [
|
||||||
RecordingActivityViewRecordingsButtonDirective,
|
RecordingActivityViewRecordingsButtonDirective,
|
||||||
RecordingActivityShowRecordingsListDirective,
|
RecordingActivityShowRecordingsListDirective,
|
||||||
ToolbarRoomNameDirective,
|
ToolbarRoomNameDirective,
|
||||||
ShowThemeSelectorDirective
|
ShowThemeSelectorDirective,
|
||||||
|
E2EEKeyDirective
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,10 @@ export class FallbackLogoDirective implements OnInit {
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LayoutRemoteParticipantsDirective {
|
export class LayoutRemoteParticipantsDirective {
|
||||||
|
private _ovRemoteParticipants: ParticipantModel[] | undefined;
|
||||||
|
|
||||||
@Input() set ovRemoteParticipants(value: ParticipantModel[] | undefined) {
|
@Input() set ovRemoteParticipants(value: ParticipantModel[] | undefined) {
|
||||||
|
this._ovRemoteParticipants = value;
|
||||||
this.update(value);
|
this.update(value);
|
||||||
}
|
}
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -71,7 +74,7 @@ export class LayoutRemoteParticipantsDirective {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.update(this.ovRemoteParticipants);
|
this.update(this._ovRemoteParticipants);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(value: ParticipantModel[] | undefined) {
|
update(value: ParticipantModel[] | undefined) {
|
||||||
|
|
@ -570,3 +573,51 @@ export class ShowThemeSelectorDirective implements AfterViewInit, OnDestroy {
|
||||||
this.libService.updateGeneralConfig({ showThemeSelector: value });
|
this.libService.updateGeneralConfig({ showThemeSelector: value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* The **e2eeKey** directive allows to configure end-to-end encryption for the videoconference.
|
||||||
|
* When provided, the room will be configured with E2EE using an external key provider.
|
||||||
|
*
|
||||||
|
* Default: `undefined`
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* <ov-videoconference [e2eeKey]="yourEncryptionKey"></ov-videoconference>
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: 'ov-videoconference[e2eeKey]',
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class E2EEKeyDirective implements AfterViewInit, OnDestroy {
|
||||||
|
@Input() set e2eeKey(value: string | undefined) {
|
||||||
|
this._value = value;
|
||||||
|
this.update(this._value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _value: string | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public elementRef: ElementRef,
|
||||||
|
private libService: OpenViduComponentsConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.update(this._value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private clear() {
|
||||||
|
this._value = undefined;
|
||||||
|
this.update(this._value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(value: string | undefined) {
|
||||||
|
// Only update if value is valid (not undefined, not null, not empty string)
|
||||||
|
const validValue = value && value.trim() !== '' ? value.trim() : undefined;
|
||||||
|
this.libService.updateGeneralConfig({ e2eeKey: validValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -535,8 +535,10 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
update(value: string) {
|
update(participantName: string) {
|
||||||
if (value) this.libService.updateGeneralConfig({ participantName: value });
|
if (participantName) {
|
||||||
|
this.libService.updateGeneralConfig({ participantName });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@
|
||||||
* ```
|
* ```
|
||||||
* <!--ovPreJoin-end-tutorial-->
|
* <!--ovPreJoin-end-tutorial-->
|
||||||
*
|
*
|
||||||
* For a detailed tutorial on customizing the pre-join component, please visit [this link](https://openvidu.io/latest/docs/tutorials/angular-components/openvidu-custom-prejoin/).
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
|
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||||
|
|
@ -308,3 +308,73 @@ export class ParticipantPanelParticipantBadgeDirective {
|
||||||
public container: ViewContainerRef
|
public container: ViewContainerRef
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ***ovSettingsPanelGeneralAdditionalElements** directive allows you to inject custom HTML or Angular templates
|
||||||
|
* into the general section of the settings panel.
|
||||||
|
* This enables you to add custom controls, information, or UI elements to extend the settings panel functionality.
|
||||||
|
*
|
||||||
|
* Usage example:
|
||||||
|
* ```html
|
||||||
|
* <ov-videoconference>
|
||||||
|
* <ng-container *ovSettingsPanelGeneralAdditionalElements>
|
||||||
|
* <div class="custom-settings-section">
|
||||||
|
* <mat-list>
|
||||||
|
* <mat-list-item>
|
||||||
|
* <mat-icon matListItemIcon>tune</mat-icon>
|
||||||
|
* <div matListItemTitle>Custom Setting</div>
|
||||||
|
* <mat-slide-toggle matListItemMeta [(ngModel)]="customSetting"></mat-slide-toggle>
|
||||||
|
* </mat-list-item>
|
||||||
|
* </mat-list>
|
||||||
|
* </div>
|
||||||
|
* </ng-container>
|
||||||
|
* </ov-videoconference>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[ovSettingsPanelGeneralAdditionalElements]',
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class SettingsPanelGeneralAdditionalElementsDirective {
|
||||||
|
constructor(
|
||||||
|
public template: TemplateRef<any>,
|
||||||
|
public container: ViewContainerRef
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ***ovToolbarMoreOptionsAdditionalMenuItems** directive allows you to inject custom HTML or Angular templates
|
||||||
|
* into the "more options" menu (three dots button) of the toolbar.
|
||||||
|
* This enables you to add custom menu items to extend the toolbar functionality.
|
||||||
|
*
|
||||||
|
* Usage example:
|
||||||
|
* ```html
|
||||||
|
* <ov-videoconference>
|
||||||
|
* <ng-container *ovToolbarMoreOptionsAdditionalMenuItems>
|
||||||
|
* <button mat-menu-item (click)="onCustomAction()">
|
||||||
|
* <mat-icon>star</mat-icon>
|
||||||
|
* <span>Custom Action</span>
|
||||||
|
* </button>
|
||||||
|
* <mat-divider></mat-divider>
|
||||||
|
* <button mat-menu-item (click)="onAnotherAction()">
|
||||||
|
* <mat-icon>info</mat-icon>
|
||||||
|
* <span>Another Action</span>
|
||||||
|
* </button>
|
||||||
|
* </ng-container>
|
||||||
|
* </ov-videoconference>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[ovToolbarMoreOptionsAdditionalMenuItems]',
|
||||||
|
standalone: false
|
||||||
|
})
|
||||||
|
export class ToolbarMoreOptionsAdditionalMenuItemsDirective {
|
||||||
|
constructor(
|
||||||
|
public template: TemplateRef<any>,
|
||||||
|
public container: ViewContainerRef
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,9 @@ import {
|
||||||
ParticipantPanelAfterLocalParticipantDirective,
|
ParticipantPanelAfterLocalParticipantDirective,
|
||||||
ParticipantPanelParticipantBadgeDirective,
|
ParticipantPanelParticipantBadgeDirective,
|
||||||
PreJoinDirective,
|
PreJoinDirective,
|
||||||
LeaveButtonDirective
|
LeaveButtonDirective,
|
||||||
|
SettingsPanelGeneralAdditionalElementsDirective,
|
||||||
|
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||||
} from './internals.directive';
|
} from './internals.directive';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
@ -40,7 +42,9 @@ import {
|
||||||
PreJoinDirective,
|
PreJoinDirective,
|
||||||
ParticipantPanelAfterLocalParticipantDirective,
|
ParticipantPanelAfterLocalParticipantDirective,
|
||||||
LayoutAdditionalElementsDirective,
|
LayoutAdditionalElementsDirective,
|
||||||
ParticipantPanelParticipantBadgeDirective
|
ParticipantPanelParticipantBadgeDirective,
|
||||||
|
SettingsPanelGeneralAdditionalElementsDirective,
|
||||||
|
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||||
// BackgroundEffectsPanelDirective
|
// BackgroundEffectsPanelDirective
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
|
@ -60,7 +64,9 @@ import {
|
||||||
PreJoinDirective,
|
PreJoinDirective,
|
||||||
ParticipantPanelAfterLocalParticipantDirective,
|
ParticipantPanelAfterLocalParticipantDirective,
|
||||||
LayoutAdditionalElementsDirective,
|
LayoutAdditionalElementsDirective,
|
||||||
ParticipantPanelParticipantBadgeDirective
|
ParticipantPanelParticipantBadgeDirective,
|
||||||
|
SettingsPanelGeneralAdditionalElementsDirective,
|
||||||
|
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||||
// BackgroundEffectsPanelDirective
|
// BackgroundEffectsPanelDirective
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "消息已发送",
|
"MESSAGE_SENT_NOTIFICATION": "消息已发送",
|
||||||
"OPEN_CHAT": "打开"
|
"OPEN_CHAT": "打开"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "活动"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "参与者",
|
"TITLE": "参与者",
|
||||||
"CAMERA": "摄像头",
|
"CAMERA": "摄像头",
|
||||||
|
|
@ -175,6 +178,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "加载视频时发生错误。",
|
"MEDIA_ERR_GENERIC": "加载视频时发生错误。",
|
||||||
"MEDIA_ERR_NETWORK": "网络错误导致视频下载中途失败。",
|
"MEDIA_ERR_NETWORK": "网络错误导致视频下载中途失败。",
|
||||||
"MEDIA_ERR_DECODE": "由于损坏问题或视频使用了您的浏览器不支持的功能,视频播放被中止。",
|
"MEDIA_ERR_DECODE": "由于损坏问题或视频使用了您的浏览器不支持的功能,视频播放被中止。",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "流媒体录制错误:无法访问 S3。"
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "流媒体录制错误:无法访问 S3。",
|
||||||
|
"E2EE_ERROR_TITLE": "房间密码错误",
|
||||||
|
"E2EE_ERROR_CONTENT": "此参与者使用了不同的安全密钥。无法显示视频。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "Nachricht gesendet",
|
"MESSAGE_SENT_NOTIFICATION": "Nachricht gesendet",
|
||||||
"OPEN_CHAT": "ÖFFNEN"
|
"OPEN_CHAT": "ÖFFNEN"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "Aktivitäten"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "Teilnehmer",
|
"TITLE": "Teilnehmer",
|
||||||
"CAMERA": "KAMERA",
|
"CAMERA": "KAMERA",
|
||||||
|
|
@ -170,6 +173,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "Beim Laden des Videos ist ein Fehler aufgetreten.",
|
"MEDIA_ERR_GENERIC": "Beim Laden des Videos ist ein Fehler aufgetreten.",
|
||||||
"MEDIA_ERR_NETWORK": "Ein Netzwerkfehler führte dazu, dass der Video-Download teilweise fehlschlug.",
|
"MEDIA_ERR_NETWORK": "Ein Netzwerkfehler führte dazu, dass der Video-Download teilweise fehlschlug.",
|
||||||
"MEDIA_ERR_DECODE": "Die Videowiedergabe wurde aufgrund eines Korruptionsproblems oder weil das Video Funktionen verwendet, die Ihr Browser nicht unterstützt, abgebrochen.",
|
"MEDIA_ERR_DECODE": "Die Videowiedergabe wurde aufgrund eines Korruptionsproblems oder weil das Video Funktionen verwendet, die Ihr Browser nicht unterstützt, abgebrochen.",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Fehler beim Streaming der Aufnahme: S3 konnte nicht erreicht werden."
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Fehler beim Streaming der Aufnahme: S3 konnte nicht erreicht werden.",
|
||||||
|
"E2EE_ERROR_TITLE": "Raum-Passwortfehler",
|
||||||
|
"E2EE_ERROR_CONTENT": "Dieser Teilnehmer verwendet einen anderen Sicherheitsschlüssel. Video kann nicht angezeigt werden."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@
|
||||||
"MUTE": "Mute",
|
"MUTE": "Mute",
|
||||||
"UNMUTE": "Unmute"
|
"UNMUTE": "Unmute"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "Activities"
|
||||||
|
},
|
||||||
"SETTINGS": {
|
"SETTINGS": {
|
||||||
"TITLE": "Settings",
|
"TITLE": "Settings",
|
||||||
"GENERAL": "General",
|
"GENERAL": "General",
|
||||||
|
|
@ -179,6 +182,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "An error occurred while loading the video.",
|
"MEDIA_ERR_GENERIC": "An error occurred while loading the video.",
|
||||||
"MEDIA_ERR_NETWORK": "A network error caused the video download to fail part-way.",
|
"MEDIA_ERR_NETWORK": "A network error caused the video download to fail part-way.",
|
||||||
"MEDIA_ERR_DECODE": "The video playback was aborted due to a corruption problem or because the video used features your browser did not support.",
|
"MEDIA_ERR_DECODE": "The video playback was aborted due to a corruption problem or because the video used features your browser did not support.",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Error streaming recording: S3 could not be reached."
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Error streaming recording: S3 could not be reached.",
|
||||||
|
"E2EE_ERROR_TITLE": "Room password error",
|
||||||
|
"E2EE_ERROR_CONTENT": "This participant is using a different encryption key. Video cannot be displayed."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "mensaje enviado",
|
"MESSAGE_SENT_NOTIFICATION": "mensaje enviado",
|
||||||
"OPEN_CHAT": "ABRIR"
|
"OPEN_CHAT": "ABRIR"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "Actividades"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "Participantes",
|
"TITLE": "Participantes",
|
||||||
"CAMERA": "CÁMARA",
|
"CAMERA": "CÁMARA",
|
||||||
|
|
@ -174,6 +177,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "Ocurrió un error al cargar el video.",
|
"MEDIA_ERR_GENERIC": "Ocurrió un error al cargar el video.",
|
||||||
"MEDIA_ERR_NETWORK": "Un error de red causó que la descarga del video fallara a mitad de camino.",
|
"MEDIA_ERR_NETWORK": "Un error de red causó que la descarga del video fallara a mitad de camino.",
|
||||||
"MEDIA_ERR_DECODE": "La reproducción del video se interrumpió debido a un problema de corrupción o porque el video utiliza características que su navegador no soporta.",
|
"MEDIA_ERR_DECODE": "La reproducción del video se interrumpió debido a un problema de corrupción o porque el video utiliza características que su navegador no soporta.",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Error al transmitir la grabación: no se pudo conectar con S3."
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Error al transmitir la grabación: no se pudo conectar con S3.",
|
||||||
|
"E2EE_ERROR_TITLE": "Error de contraseña de sala",
|
||||||
|
"E2EE_ERROR_CONTENT": "Este participante está utilizando una clave de cifrado diferente. No se puede mostrar el video."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "message envoyé",
|
"MESSAGE_SENT_NOTIFICATION": "message envoyé",
|
||||||
"OPEN_CHAT": "OUVRIR"
|
"OPEN_CHAT": "OUVRIR"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "Activités"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "Participants",
|
"TITLE": "Participants",
|
||||||
"CAMERA": "CAMÉRA",
|
"CAMERA": "CAMÉRA",
|
||||||
|
|
@ -175,6 +178,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "Une erreur s'est produite lors du chargement de la vidéo.",
|
"MEDIA_ERR_GENERIC": "Une erreur s'est produite lors du chargement de la vidéo.",
|
||||||
"MEDIA_ERR_NETWORK": "Une erreur de réseau a causé l'échec du téléchargement de la vidéo en cours de route.",
|
"MEDIA_ERR_NETWORK": "Une erreur de réseau a causé l'échec du téléchargement de la vidéo en cours de route.",
|
||||||
"MEDIA_ERR_DECODE": "La lecture de la vidéo a été interrompue en raison d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités que votre navigateur ne prend pas en charge.",
|
"MEDIA_ERR_DECODE": "La lecture de la vidéo a été interrompue en raison d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités que votre navigateur ne prend pas en charge.",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Erreur de diffusion de l'enregistrement : S3 n'a pas pu être atteint."
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Erreur de diffusion de l'enregistrement : S3 n'a pas pu être atteint.",
|
||||||
|
"E2EE_ERROR_TITLE": "Erreur de mot de passe de la salle",
|
||||||
|
"E2EE_ERROR_CONTENT": "Ce participant utilise une clé de sécurité différente. La vidéo ne peut pas être affichée."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया",
|
"MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया",
|
||||||
"OPEN_CHAT": "खोलें"
|
"OPEN_CHAT": "खोलें"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "गतिविधियाँ"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "सदस्य",
|
"TITLE": "सदस्य",
|
||||||
"CAMERA": "कैमरा",
|
"CAMERA": "कैमरा",
|
||||||
|
|
@ -175,6 +178,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "वीडियो लोड करते समय एक त्रुटि हुई।",
|
"MEDIA_ERR_GENERIC": "वीडियो लोड करते समय एक त्रुटि हुई।",
|
||||||
"MEDIA_ERR_NETWORK": "नेटवर्क त्रुटि के कारण वीडियो डाउनलोड बीच में ही विफल हो गया।",
|
"MEDIA_ERR_NETWORK": "नेटवर्क त्रुटि के कारण वीडियो डाउनलोड बीच में ही विफल हो गया।",
|
||||||
"MEDIA_ERR_DECODE": "वीडियो प्लेबैक को एक भ्रष्टाचार समस्या या क्योंकि वीडियो ने आपके ब्राउज़र द्वारा समर्थित नहीं की गई सुविधाओं का उपयोग किया था, के कारण रोक दिया गया था।",
|
"MEDIA_ERR_DECODE": "वीडियो प्लेबैक को एक भ्रष्टाचार समस्या या क्योंकि वीडियो ने आपके ब्राउज़र द्वारा समर्थित नहीं की गई सुविधाओं का उपयोग किया था, के कारण रोक दिया गया था।",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "स्ट्रीमिंग रिकॉर्डिंग में त्रुटि: S3 तक पहुंचा नहीं जा सका।"
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "स्ट्रीमिंग रिकॉर्डिंग में त्रुटि: S3 तक पहुंचा नहीं जा सका।",
|
||||||
|
"E2EE_ERROR_TITLE": "कक्ष पासवर्ड त्रुटि",
|
||||||
|
"E2EE_ERROR_CONTENT": "यह प्रतिभागी एक अलग सुरक्षा कुंजी का उपयोग कर रहा है। वीडियो प्रदर्शित नहीं किया जा सकता।"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "messaggio inviato",
|
"MESSAGE_SENT_NOTIFICATION": "messaggio inviato",
|
||||||
"OPEN_CHAT": "APRI"
|
"OPEN_CHAT": "APRI"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "Attività"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "Partecipanti",
|
"TITLE": "Partecipanti",
|
||||||
"CAMERA": "CAMERA",
|
"CAMERA": "CAMERA",
|
||||||
|
|
@ -175,6 +178,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "Si è verificato un errore durante il caricamento del video.",
|
"MEDIA_ERR_GENERIC": "Si è verificato un errore durante il caricamento del video.",
|
||||||
"MEDIA_ERR_NETWORK": "Un errore di rete ha causato l'interruzione del download del video a metà strada.",
|
"MEDIA_ERR_NETWORK": "Un errore di rete ha causato l'interruzione del download del video a metà strada.",
|
||||||
"MEDIA_ERR_DECODE": "La riproduzione del video è stata interrotta a causa di un problema di corruzione o perché il video utilizzava funzionalità non supportate dal tuo browser.",
|
"MEDIA_ERR_DECODE": "La riproduzione del video è stata interrotta a causa di un problema di corruzione o perché il video utilizzava funzionalità non supportate dal tuo browser.",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Errore nello streaming della registrazione: impossibile raggiungere S3."
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Errore nello streaming della registrazione: impossibile raggiungere S3.",
|
||||||
|
"E2EE_ERROR_TITLE": "Errore password della stanza",
|
||||||
|
"E2EE_ERROR_CONTENT": "Questo partecipante sta utilizzando una chiave di crittografia diversa. Il video non può essere visualizzato."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "メッセージを送信しました",
|
"MESSAGE_SENT_NOTIFICATION": "メッセージを送信しました",
|
||||||
"OPEN_CHAT": "開く"
|
"OPEN_CHAT": "開く"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "アクティビティ"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "参加者",
|
"TITLE": "参加者",
|
||||||
"CAMERA": "カメラ",
|
"CAMERA": "カメラ",
|
||||||
|
|
@ -175,6 +178,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "ビデオの読み込み中にエラーが発生しました。",
|
"MEDIA_ERR_GENERIC": "ビデオの読み込み中にエラーが発生しました。",
|
||||||
"MEDIA_ERR_NETWORK": "ネットワークエラーにより、ビデオのダウンロードが途中で失敗しました。",
|
"MEDIA_ERR_NETWORK": "ネットワークエラーにより、ビデオのダウンロードが途中で失敗しました。",
|
||||||
"MEDIA_ERR_DECODE": "破損の問題またはビデオがブラウザでサポートされていない機能を使用したために、ビデオの再生が中止されました。",
|
"MEDIA_ERR_DECODE": "破損の問題またはビデオがブラウザでサポートされていない機能を使用したために、ビデオの再生が中止されました。",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "録画のストリーミングエラー:S3にアクセスできませんでした。"
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "録画のストリーミングエラー:S3にアクセスできませんでした。",
|
||||||
|
"E2EE_ERROR_TITLE": "ルームパスワードエラー",
|
||||||
|
"E2EE_ERROR_CONTENT": "この参加者は異なるセキュリティキーを使用しています。ビデオを表示できません。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "bericht verzonden",
|
"MESSAGE_SENT_NOTIFICATION": "bericht verzonden",
|
||||||
"OPEN_CHAT": "OPENEN"
|
"OPEN_CHAT": "OPENEN"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "Activiteiten"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "Deelnemers",
|
"TITLE": "Deelnemers",
|
||||||
"CAMERA": "CAMERA",
|
"CAMERA": "CAMERA",
|
||||||
|
|
@ -175,6 +178,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "Er is een fout opgetreden bij het laden van de video.",
|
"MEDIA_ERR_GENERIC": "Er is een fout opgetreden bij het laden van de video.",
|
||||||
"MEDIA_ERR_NETWORK": "Een netwerkfout heeft ertoe geleid dat het downloaden van de video halverwege is mislukt.",
|
"MEDIA_ERR_NETWORK": "Een netwerkfout heeft ertoe geleid dat het downloaden van de video halverwege is mislukt.",
|
||||||
"MEDIA_ERR_DECODE": "Het afspelen van de video is afgebroken vanwege een corruptieprobleem of omdat de video functies gebruikte die uw browser niet ondersteunde.",
|
"MEDIA_ERR_DECODE": "Het afspelen van de video is afgebroken vanwege een corruptieprobleem of omdat de video functies gebruikte die uw browser niet ondersteunde.",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Fout bij het streamen van de opname: S3 kon niet worden bereikt."
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Fout bij het streamen van de opname: S3 kon niet worden bereikt.",
|
||||||
|
"E2EE_ERROR_TITLE": "Kamerwachtwoordfout",
|
||||||
|
"E2EE_ERROR_CONTENT": "Deze deelnemer gebruikt een andere beveiligingssleutel. Video kan niet worden weergegeven."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@
|
||||||
"MESSAGE_SENT_NOTIFICATION": "mensagem enviada",
|
"MESSAGE_SENT_NOTIFICATION": "mensagem enviada",
|
||||||
"OPEN_CHAT": "ABRIR"
|
"OPEN_CHAT": "ABRIR"
|
||||||
},
|
},
|
||||||
|
"ACTIVITIES": {
|
||||||
|
"TITLE": "Atividades"
|
||||||
|
},
|
||||||
"PARTICIPANTS": {
|
"PARTICIPANTS": {
|
||||||
"TITLE": "Participantes",
|
"TITLE": "Participantes",
|
||||||
"CAMERA": "CÂMERA",
|
"CAMERA": "CÂMERA",
|
||||||
|
|
@ -176,6 +179,8 @@
|
||||||
"MEDIA_ERR_GENERIC": "Ocorreu um erro ao carregar o vídeo.",
|
"MEDIA_ERR_GENERIC": "Ocorreu um erro ao carregar o vídeo.",
|
||||||
"MEDIA_ERR_NETWORK": "Um erro de rede fez com que o download do vídeo falhasse parcialmente.",
|
"MEDIA_ERR_NETWORK": "Um erro de rede fez com que o download do vídeo falhasse parcialmente.",
|
||||||
"MEDIA_ERR_DECODE": "A reprodução do vídeo foi interrompida devido a um problema de corrupção ou porque o vídeo usou recursos que o seu navegador não suportava.",
|
"MEDIA_ERR_DECODE": "A reprodução do vídeo foi interrompida devido a um problema de corrupção ou porque o vídeo usou recursos que o seu navegador não suportava.",
|
||||||
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Erro ao transmitir a gravação: não foi possível acessar o S3."
|
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Erro ao transmitir a gravação: não foi possível acessar o S3.",
|
||||||
|
"E2EE_ERROR_TITLE": "Erro de senha da sala",
|
||||||
|
"E2EE_ERROR_CONTENT": "Este participante está usando uma chave de criptografia diferente. O vídeo não pode ser exibido."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@ export class ParticipantModel {
|
||||||
private room: Room | undefined;
|
private room: Room | undefined;
|
||||||
private speaking: boolean = false;
|
private speaking: boolean = false;
|
||||||
private customVideoTrack: Partial<ParticipantTrackPublication>;
|
private customVideoTrack: Partial<ParticipantTrackPublication>;
|
||||||
|
private _hasEncryptionError: boolean = false;
|
||||||
|
private _decryptedName: string | undefined;
|
||||||
|
|
||||||
constructor(props: ParticipantProperties) {
|
constructor(props: ParticipantProperties) {
|
||||||
this.participant = props.participant;
|
this.participant = props.participant;
|
||||||
|
|
@ -170,8 +172,16 @@ export class ParticipantModel {
|
||||||
* @returns string
|
* @returns string
|
||||||
*/
|
*/
|
||||||
get name(): string | undefined {
|
get name(): string | undefined {
|
||||||
return this.participant.name;
|
return this._decryptedName ?? this.participant.name;
|
||||||
// return this.identity;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the room name where the participant is.
|
||||||
|
* @return string | undefined
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
get roomName(): string | undefined {
|
||||||
|
return this.room?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -550,4 +560,31 @@ export class ParticipantModel {
|
||||||
setMutedForcibly(muted: boolean) {
|
setMutedForcibly(muted: boolean) {
|
||||||
this.tracks.forEach((track) => (track.isMutedForcibly = muted));
|
this.tracks.forEach((track) => (track.isMutedForcibly = muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether this participant has an encryption error.
|
||||||
|
* This indicates that the participant cannot decrypt the video stream due to an incorrect encryption key.
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
get hasEncryptionError(): boolean {
|
||||||
|
return this._hasEncryptionError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the encryption error state for this participant.
|
||||||
|
* @param hasError - Whether the participant has an encryption error
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
setEncryptionError(hasError: boolean) {
|
||||||
|
this._hasEncryptionError = hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the decrypted name for this participant.
|
||||||
|
* @param decryptedName - The decrypted participant name
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
setDecryptedName(decryptedName: string | undefined) {
|
||||||
|
this._decryptedName = decryptedName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import { VideoconferenceComponent } from './components/videoconference/videoconf
|
||||||
|
|
||||||
import { AdminDashboardComponent } from './admin/admin-dashboard/admin-dashboard.component';
|
import { AdminDashboardComponent } from './admin/admin-dashboard/admin-dashboard.component';
|
||||||
import { AdminLoginComponent } from './admin/admin-login/admin-login.component';
|
import { AdminLoginComponent } from './admin/admin-login/admin-login.component';
|
||||||
import { AvatarProfileComponent } from './components/avatar-profile/avatar-profile.component';
|
|
||||||
// import { CaptionsComponent } from './components/captions/captions.component';
|
// import { CaptionsComponent } from './components/captions/captions.component';
|
||||||
import { ProFeatureDialogTemplateComponent } from './components/dialogs/pro-feature-dialog.component';
|
import { ProFeatureDialogTemplateComponent } from './components/dialogs/pro-feature-dialog.component';
|
||||||
import { ActivitiesPanelComponent } from './components/panel/activities-panel/activities-panel.component';
|
import { ActivitiesPanelComponent } from './components/panel/activities-panel/activities-panel.component';
|
||||||
|
|
@ -48,6 +47,7 @@ import { OpenViduComponentsDirectiveModule } from './directives/template/openvid
|
||||||
import { AppMaterialModule } from './openvidu-components-angular.material.module';
|
import { AppMaterialModule } from './openvidu-components-angular.material.module';
|
||||||
import { ThemeSelectorComponent } from './components/settings/theme-selector/theme-selector.component';
|
import { ThemeSelectorComponent } from './components/settings/theme-selector/theme-selector.component';
|
||||||
import { LandscapeWarningComponent } from './components/landscape-warning/landscape-warning.component';
|
import { LandscapeWarningComponent } from './components/landscape-warning/landscape-warning.component';
|
||||||
|
import { VideoPosterComponent } from './components/video-poster/video-poster.component';
|
||||||
|
|
||||||
const publicComponents = [
|
const publicComponents = [
|
||||||
AdminDashboardComponent,
|
AdminDashboardComponent,
|
||||||
|
|
@ -74,7 +74,7 @@ const privateComponents = [
|
||||||
ProFeatureDialogTemplateComponent,
|
ProFeatureDialogTemplateComponent,
|
||||||
RecordingDialogComponent,
|
RecordingDialogComponent,
|
||||||
DeleteDialogComponent,
|
DeleteDialogComponent,
|
||||||
AvatarProfileComponent,
|
VideoPosterComponent,
|
||||||
MediaElementComponent,
|
MediaElementComponent,
|
||||||
VideoDevicesComponent,
|
VideoDevicesComponent,
|
||||||
AudioDevicesComponent,
|
AudioDevicesComponent,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { GlobalConfigService } from './services/config/global-config.service';
|
||||||
import { OpenViduComponentsConfigService } from './services/config/directive-config.service';
|
import { OpenViduComponentsConfigService } from './services/config/directive-config.service';
|
||||||
import { OpenViduComponentsUiModule } from './openvidu-components-angular-ui.module';
|
import { OpenViduComponentsUiModule } from './openvidu-components-angular-ui.module';
|
||||||
import { ViewportService } from './services/viewport/viewport.service';
|
import { ViewportService } from './services/viewport/viewport.service';
|
||||||
|
import { E2eeService } from './services/e2ee/e2ee.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [OpenViduComponentsUiModule],
|
imports: [OpenViduComponentsUiModule],
|
||||||
|
|
@ -38,7 +39,7 @@ export class OpenViduComponentsModule {
|
||||||
BroadcastingService,
|
BroadcastingService,
|
||||||
// CaptionService,
|
// CaptionService,
|
||||||
CdkOverlayContainer,
|
CdkOverlayContainer,
|
||||||
{ provide: OverlayContainer, useClass: CdkOverlayContainer },
|
{ provide: OverlayContainer, useExisting: CdkOverlayContainer },
|
||||||
ChatService,
|
ChatService,
|
||||||
DeviceService,
|
DeviceService,
|
||||||
DocumentService,
|
DocumentService,
|
||||||
|
|
@ -52,6 +53,7 @@ export class OpenViduComponentsModule {
|
||||||
StorageService,
|
StorageService,
|
||||||
VirtualBackgroundService,
|
VirtualBackgroundService,
|
||||||
ViewportService,
|
ViewportService,
|
||||||
|
E2eeService,
|
||||||
provideHttpClient(withInterceptorsFromDi())
|
provideHttpClient(withInterceptorsFromDi())
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { INotificationOptions } from '../../models/notification-options.model';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ActionServiceMock {
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
launchNotification(options: INotificationOptions, callback): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
openConnectionDialog(titleMessage: string, descriptionMessage: string, allowClose = true) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
closeConnectionDialog() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +1,128 @@
|
||||||
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
import { of } from 'rxjs';
|
||||||
|
import { delay } from 'rxjs/operators';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
|
||||||
import { ActionService } from './action.service';
|
import { ActionService } from './action.service';
|
||||||
import { TranslateService } from '../translate/translate.service';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { TranslateServiceMock } from '../translate/translate.service.mock';
|
import { MatDialogMock } from '../../../test-helpers/action.service.mock';
|
||||||
|
import { TranslateServiceMock } from '../../../test-helpers/translate.service.mock';
|
||||||
|
|
||||||
export class MatDialogMock {
|
describe('ActionService (characterization)', () => {
|
||||||
open() {
|
|
||||||
return { close: () => {} } as MatDialogRef<any>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ActionService', () => {
|
|
||||||
let service: ActionService;
|
let service: ActionService;
|
||||||
let dialog: MatDialog;
|
let dialog: MatDialogMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [MatSnackBarModule],
|
imports: [MatSnackBarModule],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: MatDialog, useClass: MatDialogMock },
|
{ provide: MatDialog, useClass: MatDialogMock },
|
||||||
{ provide: TranslateService, useClass: TranslateServiceMock },
|
{ provide: 'TranslateService', useClass: TranslateServiceMock },
|
||||||
{ provide: 'OPENVIDU_COMPONENTS_CONFIG', useValue: { production: false } }
|
{ provide: 'OPENVIDU_COMPONENTS_CONFIG', useValue: { production: false } }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
service = TestBed.inject(ActionService);
|
service = TestBed.inject(ActionService);
|
||||||
dialog = TestBed.inject(MatDialog);
|
dialog = TestBed.inject(MatDialog) as unknown as MatDialogMock;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('opens a connection dialog when requested', () => {
|
||||||
expect(service).toBeTruthy();
|
const spy = spyOn(dialog, 'open').and.callThrough();
|
||||||
|
|
||||||
|
service.openConnectionDialog('Title', 'Description', false);
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
// observable behavior: a MatDialogRef was created (do not assert internal state)
|
||||||
|
expect(dialog.lastRef).toBeTruthy();
|
||||||
|
expect(typeof dialog.lastRef!.close).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open connection dialog', fakeAsync(() => {
|
it('does not open a new dialog if one is already open (repeated calls)', () => {
|
||||||
const dialogSpy = spyOn(dialog, 'open').and.callThrough();
|
const spy = spyOn(dialog, 'open').and.callThrough();
|
||||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
|
||||||
expect(dialogSpy).toHaveBeenCalled();
|
|
||||||
expect(service['isConnectionDialogOpen']).toBeTrue();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not open connection dialog if one is already open', () => {
|
service.openConnectionDialog('Title', 'Description', false);
|
||||||
service['isConnectionDialogOpen'] = true;
|
// repeated calls simulate concurrent/repeated user attempts
|
||||||
const dialogSpy = spyOn(dialog, 'open').and.callThrough();
|
service.openConnectionDialog('Title', 'Description', false);
|
||||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
service.openConnectionDialog('Title', 'Description', false);
|
||||||
|
|
||||||
expect(dialogSpy).not.toHaveBeenCalled();
|
// observed behavior: open called only once
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close connection dialog and reset state', fakeAsync(() => {
|
it('closes the opened dialog when requested and allows opening a new one afterwards', fakeAsync(() => {
|
||||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
const openSpy = spyOn(dialog, 'open').and.callThrough();
|
||||||
|
|
||||||
tick(2000);
|
service.openConnectionDialog('T', 'D', false);
|
||||||
|
tick(10); // advance microtasks if the service uses timers/async internally
|
||||||
|
|
||||||
expect(service['isConnectionDialogOpen']).toBeTrue();
|
// Behavior: closing should invoke close() on the MatDialogRef
|
||||||
|
const ref = dialog.lastRef!;
|
||||||
|
expect(ref).toBeTruthy();
|
||||||
|
expect(ref.close).not.toHaveBeenCalled();
|
||||||
|
|
||||||
service.closeConnectionDialog();
|
service.closeConnectionDialog();
|
||||||
|
expect(ref.close).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
expect(service['isConnectionDialogOpen']).toBeFalse();
|
// After closing, opening again should create another instance (another open call)
|
||||||
|
service.openConnectionDialog('T', 'D', false);
|
||||||
|
expect(openSpy).toHaveBeenCalledTimes(2);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should open connection dialog only once', fakeAsync(() => {
|
it('handles rapid consecutive calls by creating a single dialog (reentrancy protection)', fakeAsync(() => {
|
||||||
// Spy on the dialog open method
|
const spy = spyOn(dialog, 'open').and.callThrough();
|
||||||
const dialogSpy = spyOn(dialog, 'open').and.callThrough();
|
|
||||||
|
|
||||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
// several almost-simultaneous calls
|
||||||
// Verify that the dialog has been called only once
|
service.openConnectionDialog('T', 'D', false);
|
||||||
expect(dialogSpy).toHaveBeenCalledTimes(1);
|
service.openConnectionDialog('T', 'D', false);
|
||||||
expect(service['isConnectionDialogOpen']).toBeTrue();
|
tick(0);
|
||||||
|
service.openConnectionDialog('T', 'D', false);
|
||||||
|
tick(0);
|
||||||
|
|
||||||
// Try to open the dialog again
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
}));
|
||||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
|
||||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
|
||||||
|
|
||||||
// Verify that the dialog has been called only once
|
it('launchNotification uses snackbar and triggers callback on action', fakeAsync(() => {
|
||||||
expect(dialogSpy).toHaveBeenCalledTimes(1);
|
const snackBar = TestBed.inject(
|
||||||
|
(window as any).ng && (window as any).ng.material
|
||||||
|
? (window as any).ng.material.MatSnackBar
|
||||||
|
: (require('@angular/material/snack-bar') as any).MatSnackBar
|
||||||
|
) as any;
|
||||||
|
// Fallback: inject via TestBed
|
||||||
|
const snack = TestBed.inject(MatSnackBar);
|
||||||
|
const openSpy = spyOn(snack, 'open').and.returnValue({ onAction: () => of(null).pipe(delay(0)) } as any);
|
||||||
|
|
||||||
|
const callback = jasmine.createSpy('callback');
|
||||||
|
service.launchNotification({ message: 'hello', buttonActionText: 'OK' }, callback);
|
||||||
|
// allow the deferred observable to emit
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(openSpy).toHaveBeenCalled();
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('openDeleteRecordingDialog calls success callback when dialog closes with true', fakeAsync(() => {
|
||||||
|
const success = jasmine.createSpy('success');
|
||||||
|
service.openDeleteRecordingDialog(success);
|
||||||
|
// MatDialogRefMock.afterClosed returns of(true) so the subscription should call the callback
|
||||||
|
tick();
|
||||||
|
expect(success).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('openRecordingPlayerDialog triggers error handler when dialog returns manageError', fakeAsync(() => {
|
||||||
|
// Arrange: make dialog.open return a ref that afterClosed emits an object with manageError:true
|
||||||
|
const returnRef = {
|
||||||
|
afterClosed: () => ({ subscribe: (fn: any) => fn({ manageError: true, error: { code: 1 } }) }),
|
||||||
|
close: jasmine.createSpy('close')
|
||||||
|
} as any;
|
||||||
|
const openSpy = spyOn(dialog, 'open').and.returnValue(returnRef);
|
||||||
|
const handleSpy = spyOn<any>(service as any, 'handleRecordingPlayerError').and.callThrough();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.openRecordingPlayerDialog('someSrc', true);
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(openSpy).toHaveBeenCalled();
|
||||||
|
expect(handleSpy).toHaveBeenCalled();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,8 @@ export class ActionService {
|
||||||
private dialogRef:
|
private dialogRef:
|
||||||
| MatDialogRef<DialogTemplateComponent | RecordingDialogComponent | DeleteDialogComponent | ProFeatureDialogTemplateComponent>
|
| MatDialogRef<DialogTemplateComponent | RecordingDialogComponent | DeleteDialogComponent | ProFeatureDialogTemplateComponent>
|
||||||
| undefined;
|
| undefined;
|
||||||
private dialogSubscription: Subscription;
|
|
||||||
private connectionDialogRef: MatDialogRef<DialogTemplateComponent> | undefined;
|
private connectionDialogRef: MatDialogRef<DialogTemplateComponent> | undefined;
|
||||||
private isConnectionDialogOpen: boolean = false;
|
private isConnectionDialogOpen = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private snackBar: MatSnackBar,
|
private snackBar: MatSnackBar,
|
||||||
|
|
@ -29,7 +28,7 @@ export class ActionService {
|
||||||
private translateService: TranslateService
|
private translateService: TranslateService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
launchNotification(options: INotificationOptions, callback): void {
|
launchNotification(options: INotificationOptions, callback?: () => void): void {
|
||||||
if (!options.config) {
|
if (!options.config) {
|
||||||
options.config = {
|
options.config = {
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
|
|
@ -41,28 +40,23 @@ export class ActionService {
|
||||||
|
|
||||||
const notification = this.snackBar.open(options.message, options.buttonActionText, options.config);
|
const notification = this.snackBar.open(options.message, options.buttonActionText, options.config);
|
||||||
if (callback) {
|
if (callback) {
|
||||||
notification.onAction().subscribe(() => {
|
// subscribe and complete immediately after calling callback
|
||||||
|
const sub = notification.onAction().subscribe(() => {
|
||||||
|
sub.unsubscribe();
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openDialog(titleMessage: string, descriptionMessage: string, allowClose = true) {
|
openDialog(titleMessage: string, descriptionMessage: string, allowClose = true) {
|
||||||
try {
|
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} catch (error) {
|
|
||||||
} finally {
|
|
||||||
const config: MatDialogConfig = {
|
const config: MatDialogConfig = {
|
||||||
minWidth: '250px',
|
minWidth: '250px',
|
||||||
data: { title: titleMessage, description: descriptionMessage, showActionButtons: allowClose },
|
data: { title: titleMessage, description: descriptionMessage, showActionButtons: allowClose },
|
||||||
disableClose: !allowClose
|
disableClose: !allowClose
|
||||||
};
|
};
|
||||||
this.dialogRef = this.dialog.open(DialogTemplateComponent, config);
|
this.dialogRef = this.dialog.open(DialogTemplateComponent, config);
|
||||||
this.dialogSubscription = this.dialogRef.afterClosed().subscribe((result) => {
|
this.dialogRef.afterClosed().subscribe(() => (this.dialogRef = undefined));
|
||||||
this.dialogRef = undefined;
|
|
||||||
if (this.dialogSubscription) this.dialogSubscription.unsubscribe();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openConnectionDialog(titleMessage: string, descriptionMessage: string, allowClose = false) {
|
openConnectionDialog(titleMessage: string, descriptionMessage: string, allowClose = false) {
|
||||||
|
|
@ -75,47 +69,44 @@ export class ActionService {
|
||||||
|
|
||||||
this.connectionDialogRef = this.dialog.open(DialogTemplateComponent, config);
|
this.connectionDialogRef = this.dialog.open(DialogTemplateComponent, config);
|
||||||
this.isConnectionDialogOpen = true;
|
this.isConnectionDialogOpen = true;
|
||||||
}
|
this.connectionDialogRef.afterClosed().subscribe(() => {
|
||||||
|
this.isConnectionDialogOpen = false;
|
||||||
openDeleteRecordingDialog(succsessCallback) {
|
this.connectionDialogRef = undefined;
|
||||||
try {
|
|
||||||
this.closeDialog();
|
|
||||||
} catch (error) {
|
|
||||||
} finally {
|
|
||||||
this.dialogRef = this.dialog.open(DeleteDialogComponent);
|
|
||||||
|
|
||||||
this.dialogSubscription = this.dialogRef.afterClosed().subscribe((result) => {
|
|
||||||
if (result) {
|
|
||||||
succsessCallback();
|
|
||||||
if (this.dialogSubscription) this.dialogSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openDeleteRecordingDialog(successCallback: () => void) {
|
||||||
|
this.closeDialog();
|
||||||
|
this.dialogRef = this.dialog.open(DeleteDialogComponent);
|
||||||
|
this.dialogRef.afterClosed().subscribe((result) => {
|
||||||
|
if (result) {
|
||||||
|
successCallback();
|
||||||
|
}
|
||||||
|
this.dialogRef = undefined;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openRecordingPlayerDialog(src: string, allowClose = true) {
|
openRecordingPlayerDialog(src: string, allowClose = true) {
|
||||||
try {
|
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} catch (error) {
|
|
||||||
} finally {
|
|
||||||
const config: MatDialogConfig = {
|
const config: MatDialogConfig = {
|
||||||
minWidth: '250px',
|
minWidth: '250px',
|
||||||
data: { src, showActionButtons: allowClose },
|
data: { src, showActionButtons: allowClose },
|
||||||
disableClose: !allowClose
|
disableClose: !allowClose
|
||||||
};
|
};
|
||||||
this.dialogRef = this.dialog.open(RecordingDialogComponent, config);
|
this.dialogRef = this.dialog.open(RecordingDialogComponent, config);
|
||||||
|
this.dialogRef.afterClosed().subscribe((data: { manageError: boolean; error: MediaError | null }) => {
|
||||||
this.dialogSubscription = this.dialogRef.afterClosed().subscribe((data: { manageError: boolean; error: MediaError | null }) => {
|
if (data && data.manageError) {
|
||||||
if (data.manageError) {
|
|
||||||
this.handleRecordingPlayerError(data.error);
|
this.handleRecordingPlayerError(data.error);
|
||||||
}
|
}
|
||||||
if (this.dialogSubscription) this.dialogSubscription.unsubscribe();
|
this.dialogRef = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
this.dialogRef?.close();
|
if (this.dialogRef) {
|
||||||
|
this.dialogRef.close();
|
||||||
|
this.dialogRef = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeConnectionDialog() {
|
closeConnectionDialog() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { ChatService } from './chat.service';
|
||||||
|
import { LoggerService } from '../logger/logger.service';
|
||||||
|
import { ParticipantService } from '../participant/participant.service';
|
||||||
|
import { PanelService } from '../panel/panel.service';
|
||||||
|
import { ActionService } from '../action/action.service';
|
||||||
|
import { TranslateService } from '../translate/translate.service';
|
||||||
|
import { E2eeService } from '../e2ee/e2ee.service';
|
||||||
|
import { DataTopic } from '../../models/data-topic.model';
|
||||||
|
import { ChatMessage } from '../../models/chat.model';
|
||||||
|
|
||||||
|
class AudioDouble {
|
||||||
|
play = jasmine.createSpy('play').and.returnValue(Promise.resolve());
|
||||||
|
volume = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ChatService', () => {
|
||||||
|
let service: ChatService;
|
||||||
|
let loggerInstance: { d: jasmine.Spy; i: jasmine.Spy; e: jasmine.Spy };
|
||||||
|
let loggerServiceMock: { get: jasmine.Spy };
|
||||||
|
let participantServiceMock: { publishData: jasmine.Spy; getMyName: jasmine.Spy };
|
||||||
|
let panelServiceMock: { isChatPanelOpened: jasmine.Spy; togglePanel: jasmine.Spy };
|
||||||
|
let actionServiceMock: { launchNotification: jasmine.Spy };
|
||||||
|
let translateServiceMock: { translate: jasmine.Spy };
|
||||||
|
let e2eeServiceMock: { encrypt: jasmine.Spy };
|
||||||
|
let audioFactorySpy: jasmine.Spy;
|
||||||
|
let audioInstance: AudioDouble;
|
||||||
|
let originalAudio: typeof Audio;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
originalAudio = (window as any).Audio;
|
||||||
|
audioFactorySpy = jasmine.createSpy('Audio').and.callFake(() => {
|
||||||
|
audioInstance = new AudioDouble();
|
||||||
|
return audioInstance;
|
||||||
|
});
|
||||||
|
(window as any).Audio = audioFactorySpy;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
(window as any).Audio = originalAudio;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
audioFactorySpy.calls.reset();
|
||||||
|
|
||||||
|
loggerInstance = {
|
||||||
|
d: jasmine.createSpy('d'),
|
||||||
|
i: jasmine.createSpy('i'),
|
||||||
|
e: jasmine.createSpy('e')
|
||||||
|
};
|
||||||
|
loggerServiceMock = {
|
||||||
|
get: jasmine.createSpy('get').and.returnValue(loggerInstance)
|
||||||
|
};
|
||||||
|
participantServiceMock = {
|
||||||
|
publishData: jasmine.createSpy('publishData').and.resolveTo(undefined),
|
||||||
|
getMyName: jasmine.createSpy('getMyName').and.returnValue('alice')
|
||||||
|
};
|
||||||
|
panelServiceMock = {
|
||||||
|
isChatPanelOpened: jasmine.createSpy('isChatPanelOpened').and.returnValue(true),
|
||||||
|
togglePanel: jasmine.createSpy('togglePanel')
|
||||||
|
};
|
||||||
|
actionServiceMock = {
|
||||||
|
launchNotification: jasmine.createSpy('launchNotification')
|
||||||
|
};
|
||||||
|
translateServiceMock = {
|
||||||
|
translate: jasmine.createSpy('translate').and.callFake((key: string) => `${key}_translated`)
|
||||||
|
};
|
||||||
|
e2eeServiceMock = {
|
||||||
|
encrypt: jasmine.createSpy('encrypt').and.callFake(async (plain: Uint8Array) => plain)
|
||||||
|
};
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ChatService,
|
||||||
|
{ provide: LoggerService, useValue: loggerServiceMock },
|
||||||
|
{ provide: ParticipantService, useValue: participantServiceMock },
|
||||||
|
{ provide: PanelService, useValue: panelServiceMock },
|
||||||
|
{ provide: ActionService, useValue: actionServiceMock },
|
||||||
|
{ provide: TranslateService, useValue: translateServiceMock },
|
||||||
|
{ provide: E2eeService, useValue: e2eeServiceMock }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(ChatService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds remote message without notification when chat panel is open', async () => {
|
||||||
|
const emissions: ChatMessage[][] = [];
|
||||||
|
const sub = service.chatMessages$.subscribe((messages) => emissions.push(messages));
|
||||||
|
|
||||||
|
await service.addRemoteMessage('Hello world', 'Bob');
|
||||||
|
|
||||||
|
expect(emissions.at(-1)).toEqual([{ isLocal: false, participantName: 'Bob', message: 'Hello world' }]);
|
||||||
|
expect(actionServiceMock.launchNotification).not.toHaveBeenCalled();
|
||||||
|
expect(audioInstance.play).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds remote message and triggers notification with sound when chat panel is closed', async () => {
|
||||||
|
panelServiceMock.isChatPanelOpened.and.returnValue(false);
|
||||||
|
const emissions: ChatMessage[][] = [];
|
||||||
|
const sub = service.chatMessages$.subscribe((messages) => emissions.push(messages));
|
||||||
|
|
||||||
|
await service.addRemoteMessage('Hi there', 'Bob');
|
||||||
|
|
||||||
|
expect(actionServiceMock.launchNotification).toHaveBeenCalled();
|
||||||
|
const notificationArgs = actionServiceMock.launchNotification.calls.mostRecent().args[0];
|
||||||
|
expect(notificationArgs.message).toContain('BOB');
|
||||||
|
expect(notificationArgs.buttonActionText).toBe('PANEL.CHAT.OPEN_CHAT_translated');
|
||||||
|
expect(audioInstance.play).toHaveBeenCalled();
|
||||||
|
expect(emissions.at(-1)?.length).toBe(1);
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not send empty messages', async () => {
|
||||||
|
await service.sendMessage(' ');
|
||||||
|
|
||||||
|
expect(e2eeServiceMock.encrypt).not.toHaveBeenCalled();
|
||||||
|
expect(participantServiceMock.publishData).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encrypts, publishes and stores local messages', async () => {
|
||||||
|
const emissions: ChatMessage[][] = [];
|
||||||
|
const sub = service.chatMessages$.subscribe((messages) => emissions.push(messages));
|
||||||
|
|
||||||
|
await service.sendMessage('Hello world');
|
||||||
|
|
||||||
|
expect(e2eeServiceMock.encrypt).toHaveBeenCalled();
|
||||||
|
expect(participantServiceMock.publishData).toHaveBeenCalled();
|
||||||
|
const [, publishOptions] = participantServiceMock.publishData.calls.mostRecent().args;
|
||||||
|
expect(publishOptions).toEqual({ topic: DataTopic.CHAT, reliable: true });
|
||||||
|
expect(emissions.at(-1)).toEqual([{ isLocal: true, participantName: 'alice', message: 'Hello world' }]);
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
it('logs and rethrows errors when encryption fails', async () => {
|
||||||
|
const error = new Error('encryption failed');
|
||||||
|
e2eeServiceMock.encrypt.and.callFake(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectAsync(service.sendMessage('fail')).toBeRejectedWith(error);
|
||||||
|
expect(loggerInstance.e).toHaveBeenCalledWith('Error sending chat message:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -13,6 +13,7 @@ import { PanelService } from '../panel/panel.service';
|
||||||
import { ParticipantService } from '../participant/participant.service';
|
import { ParticipantService } from '../participant/participant.service';
|
||||||
import { PanelType } from '../../models/panel.model';
|
import { PanelType } from '../../models/panel.model';
|
||||||
import { TranslateService } from '../translate/translate.service';
|
import { TranslateService } from '../translate/translate.service';
|
||||||
|
import { E2eeService } from '../e2ee/e2ee.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -21,7 +22,7 @@ import { TranslateService } from '../translate/translate.service';
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ChatService {
|
export class ChatService {
|
||||||
messagesObs: Observable<ChatMessage[]>;
|
chatMessages$: Observable<ChatMessage[]>;
|
||||||
private messageSound: HTMLAudioElement;
|
private messageSound: HTMLAudioElement;
|
||||||
private _messageList = <BehaviorSubject<ChatMessage[]>>new BehaviorSubject<ChatMessage[]>([]);
|
private _messageList = <BehaviorSubject<ChatMessage[]>>new BehaviorSubject<ChatMessage[]>([]);
|
||||||
private messageList: ChatMessage[] = [];
|
private messageList: ChatMessage[] = [];
|
||||||
|
|
@ -31,10 +32,11 @@ export class ChatService {
|
||||||
private participantService: ParticipantService,
|
private participantService: ParticipantService,
|
||||||
private panelService: PanelService,
|
private panelService: PanelService,
|
||||||
private actionService: ActionService,
|
private actionService: ActionService,
|
||||||
private translateService: TranslateService
|
private translateService: TranslateService,
|
||||||
|
private e2eeService: E2eeService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('ChatService');
|
this.log = this.loggerSrv.get('ChatService');
|
||||||
this.messagesObs = this._messageList.asObservable();
|
this.chatMessages$ = this._messageList.asObservable();
|
||||||
this.messageSound = new Audio(
|
this.messageSound = new Audio(
|
||||||
'data:audio/wav;base64,SUQzAwAAAAAAekNPTU0AAAAmAAAAAAAAAFJlY29yZGVkIG9uIDI3LjAxLjIwMjEgaW4gRWRpc29uLkNPTU0AAAAmAAAAWFhYAFJlY29yZGVkIG9uIDI3LjAxLjIwMjEgaW4gRWRpc29uLlRYWFgAAAAQAAAAU29mdHdhcmUARWRpc29u//uQxAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAAJAAALNABMTExMTExMTExMTGxsbGxsbGxsbGxsiIiIiIiIiIiIiIijo6Ojo6Ojo6Ojo76+vr6+vr6+vr6+1NTU1NTU1NTU1NTk5OTk5OTk5OTk5PX19fX19fX19fX1//////////////8AAAA8TEFNRTMuMTAwBK8AAAAAAAAAABUgJAadQQABzAAACzQeSO05AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//vAxAAABsADb7QQACOOLW3/NaBQzcKNbIACRU4IPh+H1Bhx+D7xQH4IHBIcLh+D/KOk4PwQcUOfy7/5c/IQQdrP8p1g+/////4YmoaJIwUxAFESnqIkyedtyHBoIBBD8xRVFILBVXBA8OKGuWmpLAiIAgcMHgAiQM8uBWBHlMp9xxWyoxCaksudVh8KBx50YE0aK0syZbR704cguOpoXYAqcWGp2LDxF/YFSUFkYWDpfFqiICYsMX7nYBeBwqWVu/eWkW9sxXVlRstdTUjZp2R1qWXSSnooIdGHXZlVt/VA7kSkOMsgTHdzVqrds5Sqe3Kqamq8ytRR2V2unJ5+Ua5TV8qW5jlnW3u7DOvu5Z1a1rC5hWwzy1rD8KXWeW/y3hjrPLe61NvKVWix61qlzMpXARASAAS9weVYFrKBrMWqu6jjUZ7fTbfURVYa/M7yswHEFcSLKLxqmslA6BeR7roKj6JqOin0zpcOsgrR+x0kUiko0SNUDpLOuSprSMjVJNz6/rpOpNHRUlRNVImJq6lJPd3dE1b0ldExPFbgZMgYOwaBR942K9XsCn9m9lwgoQgAACZu3yILcRAQaUpwkvPr+a6+6KdVuq9gQIb1U7y4HjTa7HGscIisVOM5lXYFkWydyDBYmjp7oKgOUYUacqINdIqIEMd0FBAWiz/UyMqbMzMchf7XOtKFoSXM8QcfQaNlmA8HQ0tbXsD56lKDIvZ3XYxS3vulF0MAQQnvwnBXQfZPwLwVAMkYoSghSkIpckFJOBBNJZmYhE4E7P58SGQAgjVRZ1ZtNmo2rHq7nz3mS2U6OiXGtkhZehWmijBt/3d1TGcQEq42sxqOUFEQVDwWBY0tRsAioZKw6WJhg69O6pJra3XaSp791mB2IASQldhZLfOAk7DIgCXxTHo0nWBshqN0Y84zMGzCMKRtYGbVvz7WAVC5NzrmykQLIlrfN2qHXQ6Z/qUmDKX/+3DE1gAPtQ9b/YaAIeAiqv2GFazATncobkc9EAAkvb9pnMjVsk3wQhM9Llh+HCIRFERd4sLROgTPOK2jHfzHpU382nQFIAgACc2fGGBODtNkTQqhIzJHrH4NFkIEcw6PKxgocFSm3CrgiDYp1tMRSzjwCVaVDj43vWr7jWiC4oaHsHa27zUKxJKDNef/jXeGuxlKTY2dTwOFKA+y6l2TnRhImDKhYQgEia822x5Zt6y5b96ngYYjIBDeuCFQwnowEHFcp3F3Q2yFFZLvS54JdWCn+lVJXjs1V1u3qntRpyU8I7Uq3/ay03bW1ndLf92/uUxpELIO44f3Kr6CBbEYW5dOlWo5LKwRnMbRHsUId8KFVgUFXg+GEpWg9Vv41YxbN1tuymfD5Cr/3HMVUhALLdtDLQpBOv1r//tgxO0ADnD3V+ekTeG+G6q9hY30qMDU1SSNOegcyOxBoQ6FNCdLvxHr23ta0sU9ysR0WbGp8xM0j1rmy6Zr61vFbVi1920wDjexZD1Z+TpXaAnGC+1gfGlRYSgYUZSeasoiXkDdS8A7z2CJdo8X3+M5NAxThdP9vO5OpACoq7KA8i6CmgsiBNQ+BkMg9yVkFKk5DiSpPVTGZJJ0XCtvGs0fKYhJ1Sb+MYfbrmtaZw+f619TVxvG5msonGaUczjGdaJoY6OcuBcGi5RYaShYxh1TNgZGJNCzgoXIN4rdR1pV0JWhFmfyldXv/JcJhgBTctuDPFGOdFAmCeC4h9ncJKcguVzY//tgxPOADhDdUeelDSHXH6o9gwrUqD7gLVJhOFnCIov6lFMYCyLz5OtEnP0OCssoUOxoKYq1NRqMpI7E75LkV8jKdIyOCknQELjSQSIuahE0OjfSySUn1W63D7/HmEXCJq83cxwh1KJ2/AANk0J/F+vsgcl1QRtTY1iDZMF0eTtOv4KncRPWe0b0xGlbTjXSib4W3AlD5TypIs2e3aqryiyIkpcxOMeN3GtGH12uYqkWhO0dqSlA9aq6uwhmNp3cAAinFeOC6lmceR2EiGUjwM14WJE5cVj0Ss0zW1vY5ZjnpSSL3Fs/V2kvm3VN90v4Zn8/mlRoVZF07uiFRV3nb+Mxz9LI//tgxPgADjytT+w8beHFFen88ZuE0l3ZwRBEJJwAAVdxwnIVhoQyXVGAWpKYIQ8VhfxuratfsU5ID7+4IOeoYj0s3vrerQYt1oo8FPA5Yi/j+ig7Cprmx3iziji76xilapmKJEQCVJQABLYVBTxyRmsXMv5AC/C2TmwTQviGYc5ILASBakpWy1I4As5Z++CQmtb3UTMNv1opus8JJzSpx8Pgv8Ul6ktLZ3Oy7QEI6CIkVHyXmE+tt69/P0V0lIuLQGmhCSAQCJEiVa9VVEiSQV+QU26Kx/Tv/EGG5PBQoapMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//tQxP4ADRjPT+ekbSFXFqi9h5k0qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv/7MMT8AAmom0HsMTJpKRLmPPYiUKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv/7MMTzgElMmTfnpNJo1AimfPSZgaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv/7EMTWA8AAAaQAAAAgAAA0gAAABKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'
|
'data:audio/wav;base64,SUQzAwAAAAAAekNPTU0AAAAmAAAAAAAAAFJlY29yZGVkIG9uIDI3LjAxLjIwMjEgaW4gRWRpc29uLkNPTU0AAAAmAAAAWFhYAFJlY29yZGVkIG9uIDI3LjAxLjIwMjEgaW4gRWRpc29uLlRYWFgAAAAQAAAAU29mdHdhcmUARWRpc29u//uQxAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAAJAAALNABMTExMTExMTExMTGxsbGxsbGxsbGxsiIiIiIiIiIiIiIijo6Ojo6Ojo6Ojo76+vr6+vr6+vr6+1NTU1NTU1NTU1NTk5OTk5OTk5OTk5PX19fX19fX19fX1//////////////8AAAA8TEFNRTMuMTAwBK8AAAAAAAAAABUgJAadQQABzAAACzQeSO05AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//vAxAAABsADb7QQACOOLW3/NaBQzcKNbIACRU4IPh+H1Bhx+D7xQH4IHBIcLh+D/KOk4PwQcUOfy7/5c/IQQdrP8p1g+/////4YmoaJIwUxAFESnqIkyedtyHBoIBBD8xRVFILBVXBA8OKGuWmpLAiIAgcMHgAiQM8uBWBHlMp9xxWyoxCaksudVh8KBx50YE0aK0syZbR704cguOpoXYAqcWGp2LDxF/YFSUFkYWDpfFqiICYsMX7nYBeBwqWVu/eWkW9sxXVlRstdTUjZp2R1qWXSSnooIdGHXZlVt/VA7kSkOMsgTHdzVqrds5Sqe3Kqamq8ytRR2V2unJ5+Ua5TV8qW5jlnW3u7DOvu5Z1a1rC5hWwzy1rD8KXWeW/y3hjrPLe61NvKVWix61qlzMpXARASAAS9weVYFrKBrMWqu6jjUZ7fTbfURVYa/M7yswHEFcSLKLxqmslA6BeR7roKj6JqOin0zpcOsgrR+x0kUiko0SNUDpLOuSprSMjVJNz6/rpOpNHRUlRNVImJq6lJPd3dE1b0ldExPFbgZMgYOwaBR942K9XsCn9m9lwgoQgAACZu3yILcRAQaUpwkvPr+a6+6KdVuq9gQIb1U7y4HjTa7HGscIisVOM5lXYFkWydyDBYmjp7oKgOUYUacqINdIqIEMd0FBAWiz/UyMqbMzMchf7XOtKFoSXM8QcfQaNlmA8HQ0tbXsD56lKDIvZ3XYxS3vulF0MAQQnvwnBXQfZPwLwVAMkYoSghSkIpckFJOBBNJZmYhE4E7P58SGQAgjVRZ1ZtNmo2rHq7nz3mS2U6OiXGtkhZehWmijBt/3d1TGcQEq42sxqOUFEQVDwWBY0tRsAioZKw6WJhg69O6pJra3XaSp791mB2IASQldhZLfOAk7DIgCXxTHo0nWBshqN0Y84zMGzCMKRtYGbVvz7WAVC5NzrmykQLIlrfN2qHXQ6Z/qUmDKX/+3DE1gAPtQ9b/YaAIeAiqv2GFazATncobkc9EAAkvb9pnMjVsk3wQhM9Llh+HCIRFERd4sLROgTPOK2jHfzHpU382nQFIAgACc2fGGBODtNkTQqhIzJHrH4NFkIEcw6PKxgocFSm3CrgiDYp1tMRSzjwCVaVDj43vWr7jWiC4oaHsHa27zUKxJKDNef/jXeGuxlKTY2dTwOFKA+y6l2TnRhImDKhYQgEia822x5Zt6y5b96ngYYjIBDeuCFQwnowEHFcp3F3Q2yFFZLvS54JdWCn+lVJXjs1V1u3qntRpyU8I7Uq3/ay03bW1ndLf92/uUxpELIO44f3Kr6CBbEYW5dOlWo5LKwRnMbRHsUId8KFVgUFXg+GEpWg9Vv41YxbN1tuymfD5Cr/3HMVUhALLdtDLQpBOv1r//tgxO0ADnD3V+ekTeG+G6q9hY30qMDU1SSNOegcyOxBoQ6FNCdLvxHr23ta0sU9ysR0WbGp8xM0j1rmy6Zr61vFbVi1920wDjexZD1Z+TpXaAnGC+1gfGlRYSgYUZSeasoiXkDdS8A7z2CJdo8X3+M5NAxThdP9vO5OpACoq7KA8i6CmgsiBNQ+BkMg9yVkFKk5DiSpPVTGZJJ0XCtvGs0fKYhJ1Sb+MYfbrmtaZw+f619TVxvG5msonGaUczjGdaJoY6OcuBcGi5RYaShYxh1TNgZGJNCzgoXIN4rdR1pV0JWhFmfyldXv/JcJhgBTctuDPFGOdFAmCeC4h9ncJKcguVzY//tgxPOADhDdUeelDSHXH6o9gwrUqD7gLVJhOFnCIov6lFMYCyLz5OtEnP0OCssoUOxoKYq1NRqMpI7E75LkV8jKdIyOCknQELjSQSIuahE0OjfSySUn1W63D7/HmEXCJq83cxwh1KJ2/AANk0J/F+vsgcl1QRtTY1iDZMF0eTtOv4KncRPWe0b0xGlbTjXSib4W3AlD5TypIs2e3aqryiyIkpcxOMeN3GtGH12uYqkWhO0dqSlA9aq6uwhmNp3cAAinFeOC6lmceR2EiGUjwM14WJE5cVj0Ss0zW1vY5ZjnpSSL3Fs/V2kvm3VN90v4Zn8/mlRoVZF07uiFRV3nb+Mxz9LI//tgxPgADjytT+w8beHFFen88ZuE0l3ZwRBEJJwAAVdxwnIVhoQyXVGAWpKYIQ8VhfxuratfsU5ID7+4IOeoYj0s3vrerQYt1oo8FPA5Yi/j+ig7Cprmx3iziji76xilapmKJEQCVJQABLYVBTxyRmsXMv5AC/C2TmwTQviGYc5ILASBakpWy1I4As5Z++CQmtb3UTMNv1opus8JJzSpx8Pgv8Ul6ktLZ3Oy7QEI6CIkVHyXmE+tt69/P0V0lIuLQGmhCSAQCJEiVa9VVEiSQV+QU26Kx/Tv/EGG5PBQoapMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//tQxP4ADRjPT+ekbSFXFqi9h5k0qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv/7MMT8AAmom0HsMTJpKRLmPPYiUKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv/7MMTzgElMmTfnpNJo1AimfPSZgaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv/7EMTWA8AAAaQAAAAgAAA0gAAABKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'
|
||||||
);
|
);
|
||||||
|
|
@ -60,13 +62,32 @@ export class ChatService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a chat message through the data channel.
|
||||||
|
* If E2EE is enabled, the message will be encrypted before sending.
|
||||||
|
*
|
||||||
|
* @param message The message text to send
|
||||||
|
*/
|
||||||
async sendMessage(message: string) {
|
async sendMessage(message: string) {
|
||||||
message = message.replace(/ +(?= )/g, '');
|
const plainTextMessage = message.replace(/ +(?= )/g, '');
|
||||||
if (message !== '' && message !== ' ') {
|
if (plainTextMessage !== '' && plainTextMessage !== ' ') {
|
||||||
const strData = JSON.stringify({ message });
|
try {
|
||||||
const data: Uint8Array = new TextEncoder().encode(strData);
|
// Create message payload
|
||||||
await this.participantService.publishData(data, { topic: DataTopic.CHAT, reliable: true });
|
const payload = JSON.stringify({ message: plainTextMessage });
|
||||||
this.addMessage(message, true, this.participantService.getMyName()!);
|
const plainData: Uint8Array = new TextEncoder().encode(payload);
|
||||||
|
|
||||||
|
// Encrypt data if E2EE is enabled (Uint8Array → Uint8Array)
|
||||||
|
const dataToSend: Uint8Array = await this.e2eeService.encrypt(plainData);
|
||||||
|
|
||||||
|
// Send through data channel
|
||||||
|
await this.participantService.publishData(dataToSend, { topic: DataTopic.CHAT, reliable: true });
|
||||||
|
|
||||||
|
// Add to local message list
|
||||||
|
this.addMessage(plainTextMessage, true, this.participantService.getMyName()!);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.e('Error sending chat message:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ interface GeneralConfig {
|
||||||
showDisconnectionDialog: boolean;
|
showDisconnectionDialog: boolean;
|
||||||
showThemeSelector: boolean;
|
showThemeSelector: boolean;
|
||||||
recordingStreamBaseUrl: string;
|
recordingStreamBaseUrl: string;
|
||||||
|
e2eeKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -302,7 +303,8 @@ export class OpenViduComponentsConfigService {
|
||||||
prejoinDisplayParticipantName: true,
|
prejoinDisplayParticipantName: true,
|
||||||
showDisconnectionDialog: true,
|
showDisconnectionDialog: true,
|
||||||
showThemeSelector: false,
|
showThemeSelector: false,
|
||||||
recordingStreamBaseUrl: 'call/api/recordings'
|
recordingStreamBaseUrl: 'call/api/recordings',
|
||||||
|
e2eeKey: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
private toolbarConfig = this.createToolbarConfigItem({
|
private toolbarConfig = this.createToolbarConfigItem({
|
||||||
|
|
@ -413,6 +415,11 @@ export class OpenViduComponentsConfigService {
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
shareReplay(1)
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
e2eeKey$: Observable<string | undefined> = this.generalConfig.observable$.pipe(
|
||||||
|
map((config) => config.e2eeKey),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
|
||||||
// Stream observables
|
// Stream observables
|
||||||
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled));
|
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled));
|
||||||
|
|
@ -565,6 +572,10 @@ export class OpenViduComponentsConfigService {
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getE2EEKey(): string | undefined {
|
||||||
|
return this.generalConfig.subject.getValue().e2eeKey;
|
||||||
|
}
|
||||||
|
|
||||||
// Stream configuration methods
|
// Stream configuration methods
|
||||||
|
|
||||||
isVideoEnabled(): boolean {
|
isVideoEnabled(): boolean {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
import { Inject, Injectable, DOCUMENT } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { ParticipantFactoryFunction, OpenViduComponentsConfig } from '../../config/openvidu-components-angular.config';
|
import { ParticipantFactoryFunction, OpenViduComponentsConfig } from '../../config/openvidu-components-angular.config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,7 +17,6 @@ export class GlobalConfigService {
|
||||||
@Inject(DOCUMENT) private document: Document
|
@Inject(DOCUMENT) private document: Document
|
||||||
) {
|
) {
|
||||||
this.configuration = config;
|
this.configuration = config;
|
||||||
console.log(this.configuration);
|
|
||||||
if (this.isProduction()) console.log('OpenVidu Angular Production Mode');
|
if (this.isProduction()) console.log('OpenVidu Angular Production Mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -179,23 +179,15 @@ export class DeviceService {
|
||||||
* @returns A promise that resolves to an array of `MediaDeviceInfo` objects representing the available local devices.
|
* @returns A promise that resolves to an array of `MediaDeviceInfo` objects representing the available local devices.
|
||||||
*/
|
*/
|
||||||
private async getLocalDevices(): Promise<MediaDeviceInfo[]> {
|
private async getLocalDevices(): Promise<MediaDeviceInfo[]> {
|
||||||
// Forcing media permissions request.
|
const strategies = this.getPermissionStrategies();
|
||||||
const strategies = [
|
|
||||||
{ audio: true, video: true },
|
|
||||||
{ audio: true, video: false },
|
|
||||||
{ audio: false, video: true }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const strategy of strategies) {
|
for (const strategy of strategies) {
|
||||||
try {
|
try {
|
||||||
this.log.d(`Trying strategy: audio=${strategy.audio}, video=${strategy.video}`);
|
this.log.d(`Trying strategy: audio=${strategy.audio}, video=${strategy.video}`);
|
||||||
const localTracks = await createLocalTracks(strategy);
|
const devices = await this.tryPermissionStrategy(strategy);
|
||||||
localTracks.forEach((track) => track.stop());
|
if (devices) {
|
||||||
|
return this.filterValidDevices(devices);
|
||||||
// Permission granted
|
}
|
||||||
const devices = this.platformSrv.isFirefox() ? await this.getMediaDevicesFirefox() : await Room.getLocalDevices();
|
|
||||||
|
|
||||||
return devices.filter((d: MediaDeviceInfo) => d.label && d.deviceId && d.deviceId !== 'default');
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.log.w(`Strategy failed: audio=${strategy.audio}, video=${strategy.video}`, error);
|
this.log.w(`Strategy failed: audio=${strategy.audio}, video=${strategy.video}`, error);
|
||||||
|
|
||||||
|
|
@ -209,6 +201,38 @@ export class DeviceService {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Get the list of permission strategies to try
|
||||||
|
*/
|
||||||
|
protected getPermissionStrategies(): Array<{ audio: boolean; video: boolean }> {
|
||||||
|
return [
|
||||||
|
{ audio: true, video: true },
|
||||||
|
{ audio: true, video: false },
|
||||||
|
{ audio: false, video: true }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Try a specific permission strategy and return devices if successful
|
||||||
|
*/
|
||||||
|
protected async tryPermissionStrategy(strategy: { audio: boolean; video: boolean }): Promise<MediaDeviceInfo[] | null> {
|
||||||
|
const localTracks = await createLocalTracks(strategy);
|
||||||
|
localTracks.forEach((track) => track.stop());
|
||||||
|
|
||||||
|
// Permission granted
|
||||||
|
return this.platformSrv.isFirefox() ? await this.getMediaDevicesFirefox() : await Room.getLocalDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Filter devices to remove default and invalid entries
|
||||||
|
*/
|
||||||
|
protected filterValidDevices(devices: MediaDeviceInfo[]): MediaDeviceInfo[] {
|
||||||
|
return devices.filter((d: MediaDeviceInfo) => d.label && d.deviceId && d.deviceId !== 'default');
|
||||||
|
}
|
||||||
|
|
||||||
private async getMediaDevicesFirefox(): Promise<MediaDeviceInfo[]> {
|
private async getMediaDevicesFirefox(): Promise<MediaDeviceInfo[]> {
|
||||||
// Firefox requires to get user media to get the devices
|
// Firefox requires to get user media to get the devices
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||||
|
|
@ -219,6 +243,19 @@ export class DeviceService {
|
||||||
this.log.w('All permission strategies failed, trying device enumeration without permissions');
|
this.log.w('All permission strategies failed, trying device enumeration without permissions');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
return await this.handleFallbackByErrorType(error);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.e('Complete failure getting devices', error);
|
||||||
|
this.deviceAccessDeniedError = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Handle fallback based on error type
|
||||||
|
*/
|
||||||
|
protected async handleFallbackByErrorType(error: any): Promise<MediaDeviceInfo[]> {
|
||||||
if (error?.name === 'NotReadableError' || error?.name === 'AbortError') {
|
if (error?.name === 'NotReadableError' || error?.name === 'AbortError') {
|
||||||
this.log.w('Device busy, using enumerateDevices() instead');
|
this.log.w('Device busy, using enumerateDevices() instead');
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
@ -229,10 +266,5 @@ export class DeviceService {
|
||||||
this.deviceAccessDeniedError = true;
|
this.deviceAccessDeniedError = true;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
|
||||||
this.log.e('Complete failure getting devices', error);
|
|
||||||
this.deviceAccessDeniedError = true;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
import { DocumentService } from './document.service';
|
||||||
|
import { LayoutClass } from '../../models/layout.model';
|
||||||
|
|
||||||
|
describe('DocumentService', () => {
|
||||||
|
let service: DocumentService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new DocumentService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSmallElement', () => {
|
||||||
|
it('should return true if element has SMALL_ELEMENT class', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = LayoutClass.SMALL_ELEMENT;
|
||||||
|
expect(service.isSmallElement(element)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if element does not have SMALL_ELEMENT class', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = 'other-class';
|
||||||
|
expect(service.isSmallElement(element)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if element is null', () => {
|
||||||
|
expect(service.isSmallElement(null as any)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if element has SMALL_ELEMENT class combined with other classes', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = `some-class ${LayoutClass.SMALL_ELEMENT} another-class`;
|
||||||
|
expect(service.isSmallElement(element)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if SMALL_ELEMENT is at the beginning of className', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = `${LayoutClass.SMALL_ELEMENT} another-class`;
|
||||||
|
expect(service.isSmallElement(element)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if SMALL_ELEMENT is at the end of className', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = `some-class ${LayoutClass.SMALL_ELEMENT}`;
|
||||||
|
expect(service.isSmallElement(element)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleFullscreen', () => {
|
||||||
|
let mockDocument: any;
|
||||||
|
let mockElement: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockElement = {
|
||||||
|
requestFullscreen: jasmine.createSpy('requestFullscreen'),
|
||||||
|
msRequestFullscreen: jasmine.createSpy('msRequestFullscreen'),
|
||||||
|
mozRequestFullScreen: jasmine.createSpy('mozRequestFullScreen'),
|
||||||
|
webkitRequestFullscreen: jasmine.createSpy('webkitRequestFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
mockDocument = {
|
||||||
|
fullscreenElement: null,
|
||||||
|
mozFullScreenElement: null,
|
||||||
|
webkitFullscreenElement: null,
|
||||||
|
msFullscreenElement: null,
|
||||||
|
exitFullscreen: jasmine.createSpy('exitFullscreen'),
|
||||||
|
msExitFullscreen: jasmine.createSpy('msExitFullscreen'),
|
||||||
|
mozCancelFullScreen: jasmine.createSpy('mozCancelFullScreen'),
|
||||||
|
webkitExitFullscreen: jasmine.createSpy('webkitExitFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn<any>(service, 'getDocument').and.returnValue(mockDocument);
|
||||||
|
spyOn<any>(service, 'getElementById').and.returnValue(mockElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should request fullscreen when not in fullscreen mode', () => {
|
||||||
|
spyOn<any>(service, 'isInFullscreen').and.returnValue(false);
|
||||||
|
const requestSpy = spyOn<any>(service, 'requestFullscreen');
|
||||||
|
|
||||||
|
service.toggleFullscreen('test-element');
|
||||||
|
|
||||||
|
expect(service['getElementById']).toHaveBeenCalledWith('test-element');
|
||||||
|
expect(requestSpy).toHaveBeenCalledWith(mockElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit fullscreen when in fullscreen mode', () => {
|
||||||
|
spyOn<any>(service, 'isInFullscreen').and.returnValue(true);
|
||||||
|
const exitSpy = spyOn<any>(service, 'exitFullscreen');
|
||||||
|
|
||||||
|
service.toggleFullscreen('test-element');
|
||||||
|
|
||||||
|
expect(exitSpy).toHaveBeenCalledWith(mockDocument);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isInFullscreen', () => {
|
||||||
|
it('should return false when no fullscreen element', () => {
|
||||||
|
const mockDocument = {
|
||||||
|
fullscreenElement: null,
|
||||||
|
mozFullScreenElement: null,
|
||||||
|
webkitFullscreenElement: null,
|
||||||
|
msFullscreenElement: null
|
||||||
|
};
|
||||||
|
spyOn<any>(service, 'getDocument').and.returnValue(mockDocument);
|
||||||
|
|
||||||
|
expect(service['isInFullscreen']()).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when fullscreenElement is set', () => {
|
||||||
|
const mockElement = document.createElement('div');
|
||||||
|
const mockDocument = {
|
||||||
|
fullscreenElement: mockElement,
|
||||||
|
mozFullScreenElement: null,
|
||||||
|
webkitFullscreenElement: null,
|
||||||
|
msFullscreenElement: null
|
||||||
|
};
|
||||||
|
spyOn<any>(service, 'getDocument').and.returnValue(mockDocument);
|
||||||
|
|
||||||
|
expect(service['isInFullscreen']()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when mozFullScreenElement is set', () => {
|
||||||
|
const mockElement = document.createElement('div');
|
||||||
|
const mockDocument = {
|
||||||
|
fullscreenElement: null,
|
||||||
|
mozFullScreenElement: mockElement,
|
||||||
|
webkitFullscreenElement: null,
|
||||||
|
msFullscreenElement: null
|
||||||
|
};
|
||||||
|
spyOn<any>(service, 'getDocument').and.returnValue(mockDocument);
|
||||||
|
|
||||||
|
expect(service['isInFullscreen']()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when webkitFullscreenElement is set', () => {
|
||||||
|
const mockElement = document.createElement('div');
|
||||||
|
const mockDocument = {
|
||||||
|
fullscreenElement: null,
|
||||||
|
mozFullScreenElement: null,
|
||||||
|
webkitFullscreenElement: mockElement,
|
||||||
|
msFullscreenElement: null
|
||||||
|
};
|
||||||
|
spyOn<any>(service, 'getDocument').and.returnValue(mockDocument);
|
||||||
|
|
||||||
|
expect(service['isInFullscreen']()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when msFullscreenElement is set', () => {
|
||||||
|
const mockElement = document.createElement('div');
|
||||||
|
const mockDocument = {
|
||||||
|
fullscreenElement: null,
|
||||||
|
mozFullScreenElement: null,
|
||||||
|
webkitFullscreenElement: null,
|
||||||
|
msFullscreenElement: mockElement
|
||||||
|
};
|
||||||
|
spyOn<any>(service, 'getDocument').and.returnValue(mockDocument);
|
||||||
|
|
||||||
|
expect(service['isInFullscreen']()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('requestFullscreen', () => {
|
||||||
|
it('should call requestFullscreen when available', () => {
|
||||||
|
const mockElement = {
|
||||||
|
requestFullscreen: jasmine.createSpy('requestFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['requestFullscreen'](mockElement);
|
||||||
|
|
||||||
|
expect(mockElement.requestFullscreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call msRequestFullscreen when requestFullscreen not available', () => {
|
||||||
|
const mockElement = {
|
||||||
|
requestFullscreen: undefined,
|
||||||
|
msRequestFullscreen: jasmine.createSpy('msRequestFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['requestFullscreen'](mockElement);
|
||||||
|
|
||||||
|
expect(mockElement.msRequestFullscreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call mozRequestFullScreen when standard methods not available', () => {
|
||||||
|
const mockElement = {
|
||||||
|
requestFullscreen: undefined,
|
||||||
|
msRequestFullscreen: undefined,
|
||||||
|
mozRequestFullScreen: jasmine.createSpy('mozRequestFullScreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['requestFullscreen'](mockElement);
|
||||||
|
|
||||||
|
expect(mockElement.mozRequestFullScreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call webkitRequestFullscreen when other methods not available', () => {
|
||||||
|
const mockElement = {
|
||||||
|
requestFullscreen: undefined,
|
||||||
|
msRequestFullscreen: undefined,
|
||||||
|
mozRequestFullScreen: undefined,
|
||||||
|
webkitRequestFullscreen: jasmine.createSpy('webkitRequestFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['requestFullscreen'](mockElement);
|
||||||
|
|
||||||
|
expect(mockElement.webkitRequestFullscreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null element gracefully', () => {
|
||||||
|
expect(() => service['requestFullscreen'](null)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined element gracefully', () => {
|
||||||
|
expect(() => service['requestFullscreen'](undefined)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exitFullscreen', () => {
|
||||||
|
it('should call exitFullscreen when available', () => {
|
||||||
|
const mockDocument = {
|
||||||
|
exitFullscreen: jasmine.createSpy('exitFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['exitFullscreen'](mockDocument);
|
||||||
|
|
||||||
|
expect(mockDocument.exitFullscreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call msExitFullscreen when exitFullscreen not available', () => {
|
||||||
|
const mockDocument = {
|
||||||
|
exitFullscreen: undefined,
|
||||||
|
msExitFullscreen: jasmine.createSpy('msExitFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['exitFullscreen'](mockDocument);
|
||||||
|
|
||||||
|
expect(mockDocument.msExitFullscreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call mozCancelFullScreen when standard methods not available', () => {
|
||||||
|
const mockDocument = {
|
||||||
|
exitFullscreen: undefined,
|
||||||
|
msExitFullscreen: undefined,
|
||||||
|
mozCancelFullScreen: jasmine.createSpy('mozCancelFullScreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['exitFullscreen'](mockDocument);
|
||||||
|
|
||||||
|
expect(mockDocument.mozCancelFullScreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call webkitExitFullscreen when other methods not available', () => {
|
||||||
|
const mockDocument = {
|
||||||
|
exitFullscreen: undefined,
|
||||||
|
msExitFullscreen: undefined,
|
||||||
|
mozCancelFullScreen: undefined,
|
||||||
|
webkitExitFullscreen: jasmine.createSpy('webkitExitFullscreen')
|
||||||
|
};
|
||||||
|
|
||||||
|
service['exitFullscreen'](mockDocument);
|
||||||
|
|
||||||
|
expect(mockDocument.webkitExitFullscreen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null document gracefully', () => {
|
||||||
|
expect(() => service['exitFullscreen'](null)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined document gracefully', () => {
|
||||||
|
expect(() => service['exitFullscreen'](undefined)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDocument and getElementById', () => {
|
||||||
|
it('should return window.document by default', () => {
|
||||||
|
const doc = service['getDocument']();
|
||||||
|
expect(doc).toBe(window.document);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return element from document', () => {
|
||||||
|
const testElement = document.createElement('div');
|
||||||
|
testElement.id = 'test-element-id';
|
||||||
|
document.body.appendChild(testElement);
|
||||||
|
|
||||||
|
const element = service['getElementById']('test-element-id');
|
||||||
|
|
||||||
|
expect(element).toBe(testElement);
|
||||||
|
document.body.removeChild(testElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,24 +11,75 @@ export class DocumentService {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
toggleFullscreen(elementId: string) {
|
toggleFullscreen(elementId: string) {
|
||||||
const document: any = window.document;
|
const document: any = this.getDocument();
|
||||||
const fs = document.getElementById(elementId);
|
const fs = this.getElementById(elementId);
|
||||||
if (
|
|
||||||
!document.fullscreenElement &&
|
if (this.isInFullscreen()) {
|
||||||
!document.mozFullScreenElement &&
|
this.exitFullscreen(document);
|
||||||
!document.webkitFullscreenElement &&
|
|
||||||
!document.msFullscreenElement
|
|
||||||
) {
|
|
||||||
if (fs.requestFullscreen) {
|
|
||||||
fs.requestFullscreen();
|
|
||||||
} else if (fs.msRequestFullscreen) {
|
|
||||||
fs.msRequestFullscreen();
|
|
||||||
} else if (fs.mozRequestFullScreen) {
|
|
||||||
fs.mozRequestFullScreen();
|
|
||||||
} else if (fs.webkitRequestFullscreen) {
|
|
||||||
fs.webkitRequestFullscreen();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
this.requestFullscreen(fs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSmallElement(element: HTMLElement | Element): boolean {
|
||||||
|
return element?.className.includes(LayoutClass.SMALL_ELEMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Get the document object (can be overridden for testing)
|
||||||
|
*/
|
||||||
|
protected getDocument(): any {
|
||||||
|
return window.document;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Get element by ID (can be overridden for testing)
|
||||||
|
*/
|
||||||
|
protected getElementById(elementId: string): any {
|
||||||
|
return this.getDocument().getElementById(elementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Check if currently in fullscreen mode
|
||||||
|
*/
|
||||||
|
protected isInFullscreen(): boolean {
|
||||||
|
const document: any = this.getDocument();
|
||||||
|
return !!(
|
||||||
|
document.fullscreenElement ||
|
||||||
|
document.mozFullScreenElement ||
|
||||||
|
document.webkitFullscreenElement ||
|
||||||
|
document.msFullscreenElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Request fullscreen on element using vendor-specific methods
|
||||||
|
*/
|
||||||
|
protected requestFullscreen(element: any): void {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
if (element.requestFullscreen) {
|
||||||
|
element.requestFullscreen();
|
||||||
|
} else if (element.msRequestFullscreen) {
|
||||||
|
element.msRequestFullscreen();
|
||||||
|
} else if (element.mozRequestFullScreen) {
|
||||||
|
element.mozRequestFullScreen();
|
||||||
|
} else if (element.webkitRequestFullscreen) {
|
||||||
|
element.webkitRequestFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Exit fullscreen using vendor-specific methods
|
||||||
|
*/
|
||||||
|
protected exitFullscreen(document: any): void {
|
||||||
|
if (!document) return;
|
||||||
|
|
||||||
if (document.exitFullscreen) {
|
if (document.exitFullscreen) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
} else if (document.msExitFullscreen) {
|
} else if (document.msExitFullscreen) {
|
||||||
|
|
@ -40,8 +91,3 @@ export class DocumentService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isSmallElement(element: HTMLElement | Element): boolean {
|
|
||||||
return element?.className.includes(LayoutClass.SMALL_ELEMENT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { E2eeService } from './e2ee.service';
|
||||||
|
import { OpenViduComponentsConfigService } from '../config/directive-config.service';
|
||||||
|
import { OpenViduComponentsConfigServiceMock } from '../../../test-helpers/mocks';
|
||||||
|
import * as livekit from 'livekit-client';
|
||||||
|
|
||||||
|
describe('E2eeService', () => {
|
||||||
|
let service: E2eeService;
|
||||||
|
let configMock: OpenViduComponentsConfigServiceMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configMock = new OpenViduComponentsConfigServiceMock();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
E2eeService,
|
||||||
|
{ provide: OpenViduComponentsConfigService, useValue: configMock }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(E2eeService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created with E2EE disabled by default', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
expect(service.isEnabled).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encrypt returns original string when E2EE disabled', async () => {
|
||||||
|
const input = 'hello world';
|
||||||
|
const out = await service.encrypt(input);
|
||||||
|
expect(out).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setE2EEKey enables service when deriveEncryptionKey succeeds', async () => {
|
||||||
|
// Spy the private deriveEncryptionKey to simulate successful key derivation
|
||||||
|
spyOn<any>(service as any, 'deriveEncryptionKey').and.callFake(async (passphrase: string) => {
|
||||||
|
// Simulate setting encryptionKey
|
||||||
|
(service as any).encryptionKey = {} as CryptoKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call setE2EEKey with a value
|
||||||
|
await service.setE2EEKey('my-secret');
|
||||||
|
|
||||||
|
expect((service as any).isE2EEEnabled).toBeTrue();
|
||||||
|
expect((service as any).encryptionKey).toBeDefined();
|
||||||
|
expect(service.isEnabled).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearCache empties decryption cache and ngOnDestroy clears and completes', () => {
|
||||||
|
// Populate cache
|
||||||
|
(service as any).decryptionCache.set('a', 'b');
|
||||||
|
expect((service as any).decryptionCache.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
service.clearCache();
|
||||||
|
expect((service as any).decryptionCache.size).toBe(0);
|
||||||
|
|
||||||
|
// Re-add and call ngOnDestroy
|
||||||
|
(service as any).decryptionCache.set('x', 'y');
|
||||||
|
service.ngOnDestroy();
|
||||||
|
expect((service as any).decryptionCache.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setE2EEKey calls deriveEncryptionKey and applies result on success (via spy)', async () => {
|
||||||
|
spyOn<any>(service as any, 'deriveEncryptionKey').and.callFake(async (passphrase: string) => {
|
||||||
|
(service as any).encryptionKey = {} as CryptoKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.setE2EEKey('passphrase');
|
||||||
|
|
||||||
|
expect((service as any).encryptionKey).toBeDefined();
|
||||||
|
expect((service as any).isE2EEEnabled).toBeTrue();
|
||||||
|
expect(service.isEnabled).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setE2EEKey handles deriveEncryptionKey failure (via spy) and leaves encryptionKey undefined', async () => {
|
||||||
|
// Simulate deriveEncryptionKey handling the error internally (doesn't throw) and leaving encryptionKey undefined
|
||||||
|
spyOn<any>(service as any, 'deriveEncryptionKey').and.callFake(async () => {
|
||||||
|
(service as any).encryptionKey = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.setE2EEKey('bad');
|
||||||
|
|
||||||
|
expect((service as any).encryptionKey).toBeUndefined();
|
||||||
|
expect((service as any).isE2EEEnabled).toBeTrue();
|
||||||
|
expect(service.isEnabled).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encrypt and decrypt support binary Uint8Array paths', async () => {
|
||||||
|
(service as any).isE2EEEnabled = true;
|
||||||
|
(service as any).encryptionKey = {} as CryptoKey;
|
||||||
|
|
||||||
|
// Fake encrypt returns payload buffer
|
||||||
|
const payload = new Uint8Array([10, 11, 12]).buffer;
|
||||||
|
spyOn((window.crypto as any).subtle, 'encrypt').and.callFake(() => Promise.resolve(payload));
|
||||||
|
spyOn((window.crypto as any), 'getRandomValues').and.callFake((arr: Uint8Array) => {
|
||||||
|
for (let i = 0; i < arr.length; i++) arr[i] = i + 2;
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = new TextEncoder().encode('binary-data');
|
||||||
|
const encrypted = await service.encrypt(input) as Uint8Array;
|
||||||
|
expect(encrypted instanceof Uint8Array).toBeTrue();
|
||||||
|
// encrypted should contain iv (12) + payload
|
||||||
|
expect(encrypted.length).toBeGreaterThan(12);
|
||||||
|
|
||||||
|
// Now fake decrypt to return original input buffer
|
||||||
|
spyOn((window.crypto as any).subtle, 'decrypt').and.callFake(() => Promise.resolve(input.buffer));
|
||||||
|
|
||||||
|
// Create combined iv + payload similar to encrypt output
|
||||||
|
const iv = new Uint8Array(12);
|
||||||
|
for (let i = 0; i < iv.length; i++) iv[i] = i + 2;
|
||||||
|
const combined = new Uint8Array(iv.length + input.length);
|
||||||
|
combined.set(iv, 0);
|
||||||
|
combined.set(input, iv.length);
|
||||||
|
|
||||||
|
const decrypted = await service.decrypt(combined) as Uint8Array;
|
||||||
|
expect(decrypted instanceof Uint8Array).toBeTrue();
|
||||||
|
expect(new TextDecoder().decode(decrypted)).toBe('binary-data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decryptOrMask returns masked outputs when decryption fails for string and binary', async () => {
|
||||||
|
// Force enabled and provide a key so decrypt will be attempted
|
||||||
|
spyOnProperty(service, 'isEnabled', 'get').and.returnValue(true);
|
||||||
|
(service as any).encryptionKey = {} as CryptoKey;
|
||||||
|
|
||||||
|
// For string: provide base64 that will lead decrypt to throw
|
||||||
|
const fakeBase64 = btoa('garbage');
|
||||||
|
spyOn((window.crypto as any).subtle, 'decrypt').and.callFake(() => Promise.reject(new Error('fail')));
|
||||||
|
|
||||||
|
const maskedStr = await service.decryptOrMask(fakeBase64, undefined, 'MASKED');
|
||||||
|
expect(maskedStr).toBe('MASKED');
|
||||||
|
|
||||||
|
// For binary: provide Uint8Array that will make decrypt fail
|
||||||
|
const fakeBinary = new Uint8Array([1, 2, 3, 4]);
|
||||||
|
const maskedBin = await service.decryptOrMask(fakeBinary, undefined, 'BLANK') as Uint8Array;
|
||||||
|
expect(new TextDecoder().decode(maskedBin)).toBe('BLANK');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('encrypt and decrypt flow when enabled uses Web Crypto and caches decrypted strings', async () => {
|
||||||
|
// Enable E2EE and set a dummy encryptionKey
|
||||||
|
(service as any).isE2EEEnabled = true;
|
||||||
|
(service as any).encryptionKey = {} as CryptoKey;
|
||||||
|
|
||||||
|
// Stub crypto.subtle.encrypt to return a small payload buffer
|
||||||
|
const fakeEncryptedPayload = new Uint8Array([9, 8, 7]).buffer;
|
||||||
|
spyOn((window.crypto as any).subtle, 'encrypt').and.callFake(() => Promise.resolve(fakeEncryptedPayload));
|
||||||
|
|
||||||
|
// Stub getRandomValues to return predictable IV (12 bytes)
|
||||||
|
spyOn((window.crypto as any), 'getRandomValues').and.callFake((arr: Uint8Array) => {
|
||||||
|
for (let i = 0; i < arr.length; i++) arr[i] = i + 1; // 1..12
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encrypt a string -> should return base64 string
|
||||||
|
const plain = 'hello-e2ee';
|
||||||
|
const encrypted = await service.encrypt(plain) as string;
|
||||||
|
expect(typeof encrypted).toBe('string');
|
||||||
|
expect(encrypted.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Now stub crypto.subtle.decrypt to return decrypted buffer matching original plain
|
||||||
|
spyOn((window.crypto as any).subtle, 'decrypt').and.callFake(() => Promise.resolve(new TextEncoder().encode(plain).buffer));
|
||||||
|
|
||||||
|
// Call decrypt with the base64 returned by encrypt
|
||||||
|
const decrypted = await service.decrypt(encrypted, 'participant1') as string;
|
||||||
|
expect(decrypted).toBe(plain);
|
||||||
|
|
||||||
|
// Call decrypt again with same input -> should hit cache and not call crypto.subtle.decrypt again
|
||||||
|
const decryptSpy = (window.crypto as any).subtle.decrypt as jasmine.Spy;
|
||||||
|
decryptSpy.calls.reset();
|
||||||
|
const decrypted2 = await service.decrypt(encrypted, 'participant1') as string;
|
||||||
|
expect(decrypted2).toBe(plain);
|
||||||
|
expect(decryptSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrypt throws when encryptionKey is not initialized but isEnabled forced true', async () => {
|
||||||
|
(service as any).isE2EEEnabled = true;
|
||||||
|
(service as any).encryptionKey = undefined;
|
||||||
|
|
||||||
|
// Force the isEnabled getter to return true so we hit the encryptionKey missing branch
|
||||||
|
spyOnProperty(service, 'isEnabled', 'get').and.returnValue(true);
|
||||||
|
|
||||||
|
await expectAsync(service.decrypt(new Uint8Array([1, 2, 3]))).toBeRejectedWithError(/E2EE decryption not available/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decryptOrMask returns masked value when key missing and returns input when not base64', async () => {
|
||||||
|
// Case: E2EE disabled -> returns input
|
||||||
|
(service as any).isE2EEEnabled = false;
|
||||||
|
const txt = 'not-encrypted';
|
||||||
|
expect(await service.decryptOrMask(txt)).toBe(txt);
|
||||||
|
|
||||||
|
// Case: E2EE enabled but encryptionKey missing -> since isEnabled is false, decryptOrMask returns original input
|
||||||
|
(service as any).isE2EEEnabled = true;
|
||||||
|
(service as any).encryptionKey = undefined;
|
||||||
|
const maskedWhenNotEnabled = await service.decryptOrMask(txt, undefined, 'MASK');
|
||||||
|
expect(maskedWhenNotEnabled).toBe(txt);
|
||||||
|
|
||||||
|
// If we force isEnabled to true but encryptionKey missing, decryptOrMask should return the mask
|
||||||
|
spyOnProperty(service, 'isEnabled', 'get').and.returnValue(true);
|
||||||
|
(service as any).encryptionKey = undefined;
|
||||||
|
const masked = await service.decryptOrMask(txt, undefined, 'MASK');
|
||||||
|
expect(masked).toBe('MASK');
|
||||||
|
|
||||||
|
// Case: input not base64 -> when enabled and key present, should return input unchanged
|
||||||
|
(service as any).encryptionKey = {} as CryptoKey;
|
||||||
|
// restore isEnabled behavior to rely on actual getter
|
||||||
|
(service as any).isE2EEEnabled = true;
|
||||||
|
const notBase64 = 'this is not base64!';
|
||||||
|
expect(await service.decryptOrMask(notBase64)).toBe(notBase64);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,337 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { OpenViduComponentsConfigService } from '../config/directive-config.service';
|
||||||
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
import { createKeyMaterialFromString, deriveKeys } from 'livekit-client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Independent E2EE Service for encrypting and decrypting text-based content
|
||||||
|
* (chat messages, participant names, metadata, etc.)
|
||||||
|
*
|
||||||
|
* This service uses LiveKit's key derivation utilities combined with Web Crypto API:
|
||||||
|
* - Uses createKeyMaterialFromString from livekit-client for key material generation (PBKDF2)
|
||||||
|
* - Uses deriveKeys from livekit-client for key derivation (HKDF)
|
||||||
|
* - Uses Web Crypto API (AES-GCM) for actual encryption/decryption
|
||||||
|
* - Generates random IV for each encryption operation
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class E2eeService {
|
||||||
|
private static readonly ENCRYPTION_ALGORITHM = 'AES-GCM';
|
||||||
|
private static readonly IV_LENGTH = 12;
|
||||||
|
private static readonly SALT = 'livekit-e2ee-data'; // Salt for HKDF key derivation
|
||||||
|
|
||||||
|
private decryptionCache = new Map<string, string>();
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private isE2EEEnabled = false;
|
||||||
|
|
||||||
|
private encryptionKey: CryptoKey | undefined;
|
||||||
|
|
||||||
|
constructor(protected configService: OpenViduComponentsConfigService) {
|
||||||
|
// Monitor E2EE key changes
|
||||||
|
this.configService.e2eeKey$.pipe(takeUntil(this.destroy$)).subscribe(async (key: any) => {
|
||||||
|
await this.setE2EEKey(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setE2EEKey(key: string | null): Promise<void> {
|
||||||
|
if (key) {
|
||||||
|
this.isE2EEEnabled = true;
|
||||||
|
this.decryptionCache.clear();
|
||||||
|
|
||||||
|
await this.deriveEncryptionKey(key);
|
||||||
|
} else {
|
||||||
|
this.isE2EEEnabled = false;
|
||||||
|
this.encryptionKey = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives encryption key from passphrase using LiveKit's key derivation utilities
|
||||||
|
* @param passphrase The E2EE passphrase
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async deriveEncryptionKey(passphrase: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Use LiveKit's createKeyMaterialFromString (PBKDF2)
|
||||||
|
const keyMaterial = await createKeyMaterialFromString(passphrase);
|
||||||
|
|
||||||
|
// Use LiveKit's deriveKeys to get encryption key (HKDF)
|
||||||
|
const derivedKeys = await deriveKeys(keyMaterial, E2eeService.SALT);
|
||||||
|
|
||||||
|
// Store the encryption key for use in encrypt/decrypt operations
|
||||||
|
this.encryptionKey = derivedKeys.encryptionKey;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to derive encryption key:', error);
|
||||||
|
this.encryptionKey = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if E2EE is currently enabled and encryption key is ready
|
||||||
|
*/
|
||||||
|
get isEnabled(): boolean {
|
||||||
|
return this.isE2EEEnabled && !!this.encryptionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random initialization vector for encryption
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private static generateIV(): Uint8Array {
|
||||||
|
return crypto.getRandomValues(new Uint8Array(E2eeService.IV_LENGTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts text content using Web Crypto API with LiveKit-derived keys.
|
||||||
|
* Returns base64-encoded string suitable for metadata/names.
|
||||||
|
*
|
||||||
|
* @param text Plain text to encrypt
|
||||||
|
* @returns Encrypted text in base64 format, or original text if E2EE is disabled
|
||||||
|
*/
|
||||||
|
async encrypt(text: string): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts binary data using Web Crypto API with LiveKit-derived keys.
|
||||||
|
* Returns Uint8Array suitable for data channels.
|
||||||
|
*
|
||||||
|
* @param data Plain data to encrypt
|
||||||
|
* @returns Encrypted data as Uint8Array, or original data if E2EE is disabled
|
||||||
|
*/
|
||||||
|
async encrypt(data: Uint8Array): Promise<Uint8Array>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of encrypt overloads
|
||||||
|
*/
|
||||||
|
async encrypt(input: string | Uint8Array): Promise<string | Uint8Array> {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isString = typeof input === 'string';
|
||||||
|
if (isString && !input) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.encryptionKey) {
|
||||||
|
console.warn('E2EE encryption not available: CryptoKey not initialized. Returning unencrypted data.');
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert string to Uint8Array if needed
|
||||||
|
const data = isString ? new TextEncoder().encode(input as string) : (input as Uint8Array);
|
||||||
|
|
||||||
|
// Generate a random IV for this encryption
|
||||||
|
const iv = E2eeService.generateIV();
|
||||||
|
|
||||||
|
// Encrypt the data using Web Crypto API with AES-GCM
|
||||||
|
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: E2eeService.ENCRYPTION_ALGORITHM,
|
||||||
|
iv: iv as BufferSource
|
||||||
|
},
|
||||||
|
this.encryptionKey,
|
||||||
|
data as BufferSource
|
||||||
|
);
|
||||||
|
|
||||||
|
const encryptedData = new Uint8Array(encryptedBuffer);
|
||||||
|
|
||||||
|
// Combine IV + encrypted payload for transport
|
||||||
|
// Format: [iv(12 bytes)][payload(variable)]
|
||||||
|
const combined = new Uint8Array(iv.length + encryptedData.length);
|
||||||
|
combined.set(iv, 0);
|
||||||
|
combined.set(encryptedData, iv.length);
|
||||||
|
|
||||||
|
// Return as base64 for strings, Uint8Array for binary data
|
||||||
|
return isString ? btoa(String.fromCharCode(...combined)) : combined;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('E2EE encryption failed:', error);
|
||||||
|
// Return original input if encryption fails
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts text content from base64 format using Web Crypto API.
|
||||||
|
* Suitable for decrypting participant names, metadata, etc.
|
||||||
|
*
|
||||||
|
* @param encryptedText Encrypted text in base64 format
|
||||||
|
* @param participantIdentity Identity of the participant who encrypted the content (optional, used for caching)
|
||||||
|
* @returns Decrypted plain text, or throws error if decryption fails
|
||||||
|
*/
|
||||||
|
async decrypt(encryptedText: string, participantIdentity?: string): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts binary data from Uint8Array using Web Crypto API.
|
||||||
|
* Suitable for decrypting data channel messages.
|
||||||
|
*
|
||||||
|
* If E2EE is not enabled, returns the original encryptedData.
|
||||||
|
*
|
||||||
|
* @param encryptedData Encrypted data as Uint8Array (format: [iv][payload])
|
||||||
|
* @param participantIdentity Identity of the participant who encrypted the content (optional)
|
||||||
|
* @returns Decrypted data as Uint8Array
|
||||||
|
*/
|
||||||
|
async decrypt(encryptedData: Uint8Array, participantIdentity?: string): Promise<Uint8Array>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of decrypt overloads
|
||||||
|
*/
|
||||||
|
async decrypt(input: string | Uint8Array, participantIdentity?: string): Promise<string | Uint8Array> {
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isString = typeof input === 'string';
|
||||||
|
if (isString && !input) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache for strings (caching binary data would be too memory intensive)
|
||||||
|
if (isString) {
|
||||||
|
const cacheKey = `${participantIdentity || 'unknown'}:${input}`;
|
||||||
|
if (this.decryptionCache.has(cacheKey)) {
|
||||||
|
return this.decryptionCache.get(cacheKey)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.encryptionKey) {
|
||||||
|
throw new Error('E2EE decryption not available: CryptoKey not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert to Uint8Array if string (base64)
|
||||||
|
const combined = isString ? Uint8Array.from(atob(input as string), (c) => c.charCodeAt(0)) : (input as Uint8Array);
|
||||||
|
|
||||||
|
// Extract components: iv(12) + payload(variable)
|
||||||
|
const iv = combined.slice(0, E2eeService.IV_LENGTH);
|
||||||
|
const payload = combined.slice(E2eeService.IV_LENGTH);
|
||||||
|
|
||||||
|
// Decrypt the data using Web Crypto API with AES-GCM
|
||||||
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: E2eeService.ENCRYPTION_ALGORITHM,
|
||||||
|
iv: iv as BufferSource
|
||||||
|
},
|
||||||
|
this.encryptionKey,
|
||||||
|
payload as BufferSource
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptedData = new Uint8Array(decryptedBuffer);
|
||||||
|
|
||||||
|
// Return as string or Uint8Array depending on input type
|
||||||
|
if (isString) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const result = decoder.decode(decryptedData);
|
||||||
|
|
||||||
|
// Cache successful string decryption
|
||||||
|
const cacheKey = `${participantIdentity || 'unknown'}:${input}`;
|
||||||
|
this.decryptionCache.set(cacheKey, result);
|
||||||
|
|
||||||
|
// Limit cache size to prevent memory issues
|
||||||
|
if (this.decryptionCache.size > 1000) {
|
||||||
|
const firstKey = this.decryptionCache.keys().next().value;
|
||||||
|
if (firstKey) {
|
||||||
|
this.decryptionCache.delete(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return decryptedData;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('E2EE decryption failed (wrong key or corrupted data):', error);
|
||||||
|
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to decrypt text content. If decryption fails or E2EE is not enabled,
|
||||||
|
* returns a masked string to indicate unavailable content.
|
||||||
|
*
|
||||||
|
* @param encryptedText Encrypted text in base64 format
|
||||||
|
* @param participantIdentity Identity of the participant (optional, used for caching)
|
||||||
|
* @param maskText Custom mask text to show on failure (default: '******')
|
||||||
|
* @returns Decrypted text or masked value if decryption fails
|
||||||
|
*/
|
||||||
|
async decryptOrMask(encryptedText: string, participantIdentity?: string, maskText?: string): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to decrypt binary data. If decryption fails or E2EE is not enabled,
|
||||||
|
* returns the maskText encoded as Uint8Array to indicate unavailable content.
|
||||||
|
*
|
||||||
|
* @param encryptedData Encrypted data as Uint8Array
|
||||||
|
* @param participantIdentity Identity of the participant (optional)
|
||||||
|
* @param maskText Custom mask text to show on failure (default: '******')
|
||||||
|
* @returns Decrypted data or encoded maskText as Uint8Array if decryption fails
|
||||||
|
*/
|
||||||
|
async decryptOrMask(encryptedData: Uint8Array, participantIdentity?: string, maskText?: string): Promise<Uint8Array>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of decryptOrMask overloads
|
||||||
|
*/
|
||||||
|
async decryptOrMask(
|
||||||
|
input: string | Uint8Array,
|
||||||
|
participantIdentity?: string,
|
||||||
|
maskText: string = '******'
|
||||||
|
): Promise<string | Uint8Array> {
|
||||||
|
const isString = typeof input === 'string';
|
||||||
|
|
||||||
|
// If E2EE is not enabled, return original input
|
||||||
|
if (!this.isEnabled) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If encryption key is not available, return masked value
|
||||||
|
if (!this.encryptionKey) {
|
||||||
|
return isString ? maskText : new TextEncoder().encode(maskText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If input is empty, return as-is
|
||||||
|
if ((isString && !input) || (!isString && input.length === 0)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For strings, check if it's valid base64 before attempting decryption
|
||||||
|
if (isString) {
|
||||||
|
try {
|
||||||
|
atob(input as string);
|
||||||
|
} catch {
|
||||||
|
// Not base64, likely not encrypted - return original
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt decryption
|
||||||
|
return await this.decrypt(input as any, participantIdentity);
|
||||||
|
} catch (error) {
|
||||||
|
// Decryption failed - return masked value
|
||||||
|
if (isString) {
|
||||||
|
console.warn('E2EE: Failed to decrypt content, returning masked value:', error);
|
||||||
|
return maskText;
|
||||||
|
} else {
|
||||||
|
console.warn('E2EE: Failed to decrypt binary data, returning encoded mask text:', error);
|
||||||
|
return new TextEncoder().encode(maskText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the decryption cache.
|
||||||
|
* Should be called when E2EE key changes or when leaving a room.
|
||||||
|
*/
|
||||||
|
clearCache(): void {
|
||||||
|
this.decryptionCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup on service destroy
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ILogger } from '../../models/logger.model';
|
import { BackgroundProcessor, /*BackgroundProcessorWrapper,*/ SwitchBackgroundProcessorOptions } from '@livekit/track-processors';
|
||||||
import { DeviceService } from '../device/device.service';
|
|
||||||
import { LoggerService } from '../logger/logger.service';
|
|
||||||
import {
|
import {
|
||||||
AudioCaptureOptions,
|
AudioCaptureOptions,
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
CreateLocalTracksOptions,
|
CreateLocalTracksOptions,
|
||||||
|
E2EEOptions,
|
||||||
|
ExternalE2EEKeyProvider,
|
||||||
LocalAudioTrack,
|
LocalAudioTrack,
|
||||||
LocalTrack,
|
LocalTrack,
|
||||||
LocalVideoTrack,
|
LocalVideoTrack,
|
||||||
|
|
@ -16,8 +16,14 @@ import {
|
||||||
VideoPresets,
|
VideoPresets,
|
||||||
createLocalTracks
|
createLocalTracks
|
||||||
} from 'livekit-client';
|
} from 'livekit-client';
|
||||||
|
import { ILogger } from '../../models/logger.model';
|
||||||
|
import { OpenViduComponentsConfigService } from '../config/directive-config.service';
|
||||||
|
import { DeviceService } from '../device/device.service';
|
||||||
|
import { LoggerService } from '../logger/logger.service';
|
||||||
import { StorageService } from '../storage/storage.service';
|
import { StorageService } from '../storage/storage.service';
|
||||||
|
|
||||||
|
// TODO: Remove this once livekit-client exports it
|
||||||
|
type BackgroundProcessorWrapper = ReturnType<typeof BackgroundProcessor>;
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
|
|
@ -31,6 +37,7 @@ export class OpenViduService {
|
||||||
// private _isSttReady: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
// private _isSttReady: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
private room: Room;
|
private room: Room;
|
||||||
|
private keyProvider: ExternalE2EEKeyProvider | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -47,16 +54,24 @@ export class OpenViduService {
|
||||||
private livekitUrl = '';
|
private livekitUrl = '';
|
||||||
private log: ILogger;
|
private log: ILogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Background processor for video tracks. Initialized in disabled mode.
|
||||||
|
* This processor is shared between prejoin and in-room states.
|
||||||
|
*/
|
||||||
|
private backgroundProcessor: BackgroundProcessorWrapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private loggerSrv: LoggerService,
|
private loggerSrv: LoggerService,
|
||||||
private deviceService: DeviceService,
|
private deviceService: DeviceService,
|
||||||
private storageService: StorageService
|
private storageService: StorageService,
|
||||||
|
private configService: OpenViduComponentsConfigService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('OpenViduService');
|
this.log = this.loggerSrv.get('OpenViduService');
|
||||||
// this.isSttReadyObs = this._isSttReady.asObservable();
|
// this.isSttReadyObs = this._isSttReady.asObservable();
|
||||||
|
this.backgroundProcessor = BackgroundProcessor({ mode: 'disabled' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,14 +79,25 @@ export class OpenViduService {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
initRoom(): void {
|
initRoom(): void {
|
||||||
// If room already exists, don't recreate it
|
// Check if E2EE configuration needs to be applied
|
||||||
if (this.room) {
|
const e2eeKey = this.configService.getE2EEKey();
|
||||||
|
const needsE2EEConfig = e2eeKey && e2eeKey.trim() !== '' && !this.keyProvider;
|
||||||
|
|
||||||
|
// If room already exists and doesn't need E2EE reconfiguration, don't recreate it
|
||||||
|
if (this.room && !needsE2EEConfig) {
|
||||||
this.log.d('Room already initialized, skipping re-initialization');
|
this.log.d('Room already initialized, skipping re-initialization');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If room exists but needs E2EE configuration, we need to recreate it
|
||||||
|
if (this.room && needsE2EEConfig) {
|
||||||
|
this.log.d('Room needs E2EE configuration, recreating room');
|
||||||
|
this.room = null as any;
|
||||||
|
}
|
||||||
|
|
||||||
const videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
|
const videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
|
||||||
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
|
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
|
||||||
|
|
||||||
const roomOptions: RoomOptions = {
|
const roomOptions: RoomOptions = {
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
dynacast: true,
|
dynacast: true,
|
||||||
|
|
@ -93,17 +119,47 @@ export class OpenViduService {
|
||||||
stopLocalTrackOnUnpublish: true,
|
stopLocalTrackOnUnpublish: true,
|
||||||
disconnectOnPageLeave: true
|
disconnectOnPageLeave: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Configure E2EE if key is provided and keyProvider exists
|
||||||
|
if (needsE2EEConfig) {
|
||||||
|
// Create worker using the copied livekit-client e2ee worker from assets
|
||||||
|
roomOptions.encryption = this.buildE2EEOptions();
|
||||||
|
// !This config enables the data channel encryption
|
||||||
|
// (roomOptions as any).encryption = this.buildE2EEOptions();
|
||||||
|
}
|
||||||
|
|
||||||
this.room = new Room(roomOptions);
|
this.room = new Room(roomOptions);
|
||||||
this.log.d('Room initialized successfully');
|
this.log.d('Room initialized successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildE2EEOptions(): E2EEOptions {
|
||||||
|
this.log.d('Configuring E2EE with provided key');
|
||||||
|
this.keyProvider = new ExternalE2EEKeyProvider();
|
||||||
|
// Create worker using the copied livekit-client e2ee worker from assets
|
||||||
|
return {
|
||||||
|
keyProvider: this.keyProvider,
|
||||||
|
worker: new Worker('./assets/livekit/livekit-client.e2ee.worker.mjs', { type: 'module' })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects local participant to the room
|
* Connects local participant to the room
|
||||||
*/
|
*/
|
||||||
async connectRoom(): Promise<void> {
|
async connectRoom(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// Configure E2EE if key provider was initialized
|
||||||
|
if (this.keyProvider) {
|
||||||
|
const e2eeKey = this.configService.getE2EEKey();
|
||||||
|
if (e2eeKey) {
|
||||||
|
this.log.d('Setting E2EE key and enabling encryption');
|
||||||
|
await this.keyProvider.setKey(e2eeKey);
|
||||||
|
await this.room.setE2EEEnabled(true);
|
||||||
|
this.log.d('E2EE successfully enabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
await this.room.connect(this.livekitUrl, this.livekitToken);
|
await this.room.connect(this.livekitUrl, this.livekitToken);
|
||||||
this.log.d(`Successfully connected to room ${this.room.name}`);
|
this.log.d(`Successfully connected to room ${this.room.name}`);
|
||||||
|
|
||||||
const participantName = this.storageService.getParticipantName();
|
const participantName = this.storageService.getParticipantName();
|
||||||
if (participantName) {
|
if (participantName) {
|
||||||
this.room.localParticipant.setName(participantName);
|
this.room.localParticipant.setName(participantName);
|
||||||
|
|
@ -219,6 +275,18 @@ export class OpenViduService {
|
||||||
return this.localTracks;
|
return this.localTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches the background mode on the local video track.
|
||||||
|
* Works both in prejoin and in-room states.
|
||||||
|
* @param options - The switch options (mode, blurRadius, imagePath)
|
||||||
|
* @returns Promise<void>
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
async switchBackgroundMode(options: SwitchBackgroundProcessorOptions): Promise<void> {
|
||||||
|
await this.backgroundProcessor.switchTo(options);
|
||||||
|
this.log.d('Background mode switched:', options);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
**/
|
**/
|
||||||
|
|
@ -291,6 +359,14 @@ export class OpenViduService {
|
||||||
newLocalTracks = await createLocalTracks(options);
|
newLocalTracks = await createLocalTracks(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply background processor to video track (initialized in disabled mode)
|
||||||
|
// This ensures the processor is attached before publishing for smooth transitions
|
||||||
|
const videoTrack = newLocalTracks.find((t) => t.kind === Track.Kind.Video) as LocalVideoTrack | undefined;
|
||||||
|
if (videoTrack) {
|
||||||
|
await videoTrack.setProcessor(this.backgroundProcessor);
|
||||||
|
this.log.d('Background processor applied to newly created video track');
|
||||||
|
}
|
||||||
|
|
||||||
// Mute tracks if devices are disabled
|
// Mute tracks if devices are disabled
|
||||||
if (!this.deviceService.isCameraEnabled()) {
|
if (!this.deviceService.isCameraEnabled()) {
|
||||||
newLocalTracks.find((t) => t.kind === Track.Kind.Video)?.mute();
|
newLocalTracks.find((t) => t.kind === Track.Kind.Video)?.mute();
|
||||||
|
|
@ -434,11 +510,10 @@ export class OpenViduService {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.e('Failed to create new video track:', error);
|
this.log.e('Failed to create new video track:', error);
|
||||||
throw new Error(`Failed to switch camera: ${error.message}`);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
throw new Error(`Failed to switch camera: ${message}`);
|
||||||
}
|
}
|
||||||
}
|
} /**
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches the microphone device when the room is not connected (prejoin page)
|
* Switches the microphone device when the room is not connected (prejoin page)
|
||||||
* @param deviceId new audio device to use
|
* @param deviceId new audio device to use
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -485,7 +560,8 @@ export class OpenViduService {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.e('Failed to create new audio track:', error);
|
this.log.e('Failed to create new audio track:', error);
|
||||||
throw new Error(`Failed to switch microphone: ${error.message}`);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
throw new Error(`Failed to switch microphone: ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { PanelService } from './panel.service';
|
||||||
|
import { LoggerService } from '../logger/logger.service';
|
||||||
|
import { PanelType, PanelSettingsOptions } from '../../models/panel.model';
|
||||||
|
import { PanelStatusInfo } from '../../models/panel.model';
|
||||||
|
import { LoggerServiceMock } from '../../../test-helpers/mocks';
|
||||||
|
|
||||||
|
describe('PanelService', () => {
|
||||||
|
let service: PanelService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
PanelService,
|
||||||
|
{ provide: LoggerService, useClass: LoggerServiceMock }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
service = TestBed.inject(PanelService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created and initially closed', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
expect(service.isPanelOpened()).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('panelStatusObs emits initial value and after toggle opens the CHAT panel', () => {
|
||||||
|
const emissions: PanelStatusInfo[] = [];
|
||||||
|
const sub = service.panelStatusObs.subscribe(v => emissions.push(v));
|
||||||
|
|
||||||
|
// initial emission
|
||||||
|
expect(emissions.length).toBe(1);
|
||||||
|
expect(emissions[0].isOpened).toBeFalse();
|
||||||
|
|
||||||
|
// open chat
|
||||||
|
service.togglePanel(PanelType.CHAT);
|
||||||
|
expect(service.isPanelOpened()).toBeTrue();
|
||||||
|
expect(service.isChatPanelOpened()).toBeTrue();
|
||||||
|
|
||||||
|
// verify an emission was pushed and panelType is CHAT
|
||||||
|
const last = emissions[emissions.length - 1];
|
||||||
|
expect(last.isOpened).toBeTrue();
|
||||||
|
expect(last.panelType).toBe(PanelType.CHAT);
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggling same panel closes it and toggling different panel sets previousPanelType', () => {
|
||||||
|
const emissions: PanelStatusInfo[] = [];
|
||||||
|
const sub = service.panelStatusObs.subscribe(v => emissions.push(v));
|
||||||
|
|
||||||
|
service.togglePanel(PanelType.PARTICIPANTS);
|
||||||
|
expect(service.isParticipantsPanelOpened()).toBeTrue();
|
||||||
|
|
||||||
|
// toggling same panel should close it
|
||||||
|
service.togglePanel(PanelType.PARTICIPANTS);
|
||||||
|
expect(service.isPanelOpened()).toBeFalse();
|
||||||
|
|
||||||
|
// open panel A then open panel B -> previousPanelType should be A
|
||||||
|
service.togglePanel(PanelType.ACTIVITIES);
|
||||||
|
expect(service.isActivitiesPanelOpened()).toBeTrue();
|
||||||
|
service.togglePanel(PanelType.SETTINGS);
|
||||||
|
expect(service.isSettingsPanelOpened()).toBeTrue();
|
||||||
|
|
||||||
|
const last = emissions[emissions.length - 1];
|
||||||
|
expect(last.previousPanelType).toBe(PanelType.ACTIVITIES);
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports external panels and subOptionType', () => {
|
||||||
|
const externalName = 'MY_EXTERNAL_PANEL';
|
||||||
|
const subOpt: PanelSettingsOptions | string = 'SOME_OPTION';
|
||||||
|
|
||||||
|
// open external
|
||||||
|
service.togglePanel(externalName, subOpt);
|
||||||
|
expect(service.isExternalPanelOpened()).toBeTrue();
|
||||||
|
|
||||||
|
// panelStatusObs should contain the external panel type and subOptionType
|
||||||
|
const emitted = [] as PanelStatusInfo[];
|
||||||
|
const s = service.panelStatusObs.subscribe(v => emitted.push(v));
|
||||||
|
// last pushed value
|
||||||
|
const last = emitted[emitted.length - 1];
|
||||||
|
expect(last.panelType).toBe(externalName);
|
||||||
|
expect(last.subOptionType).toBe(subOpt);
|
||||||
|
|
||||||
|
// toggling the same external panel closes it
|
||||||
|
service.togglePanel(externalName);
|
||||||
|
expect(service.isExternalPanelOpened()).toBeFalse();
|
||||||
|
|
||||||
|
s.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens and closes the background effects panel correctly', () => {
|
||||||
|
// Open background effects
|
||||||
|
service.togglePanel(PanelType.BACKGROUND_EFFECTS);
|
||||||
|
expect(service.isBackgroundEffectsPanelOpened()).toBeTrue();
|
||||||
|
|
||||||
|
// Verify panelStatusObs last emission has correct panelType
|
||||||
|
const emitted = [] as any[];
|
||||||
|
const sub = service.panelStatusObs.subscribe(v => emitted.push(v));
|
||||||
|
const last = emitted[emitted.length - 1];
|
||||||
|
expect(last.panelType).toBe(PanelType.BACKGROUND_EFFECTS);
|
||||||
|
|
||||||
|
// Close it
|
||||||
|
service.togglePanel(PanelType.BACKGROUND_EFFECTS);
|
||||||
|
expect(service.isBackgroundEffectsPanelOpened()).toBeFalse();
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closePanel and clear close the panel and reset state', () => {
|
||||||
|
service.togglePanel(PanelType.CHAT);
|
||||||
|
expect(service.isPanelOpened()).toBeTrue();
|
||||||
|
|
||||||
|
service.closePanel();
|
||||||
|
expect(service.isPanelOpened()).toBeFalse();
|
||||||
|
|
||||||
|
// open again and then clear
|
||||||
|
service.togglePanel(PanelType.CHAT);
|
||||||
|
expect(service.isPanelOpened()).toBeTrue();
|
||||||
|
service.clear();
|
||||||
|
expect(service.isPanelOpened()).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -14,7 +14,7 @@ export class PanelService {
|
||||||
panelStatusObs: Observable<PanelStatusInfo>;
|
panelStatusObs: Observable<PanelStatusInfo>;
|
||||||
private log: ILogger;
|
private log: ILogger;
|
||||||
private isExternalOpened: boolean = false;
|
private isExternalOpened: boolean = false;
|
||||||
private externalType: string;
|
private externalType: string = '';
|
||||||
private _panelOpened = <BehaviorSubject<PanelStatusInfo>>new BehaviorSubject({ isOpened: false });
|
private _panelOpened = <BehaviorSubject<PanelStatusInfo>>new BehaviorSubject({ isOpened: false });
|
||||||
private panelTypes: string[] = Object.values(PanelType);
|
private panelTypes: string[] = Object.values(PanelType);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
VideoPresets
|
VideoPresets
|
||||||
} from 'livekit-client';
|
} from 'livekit-client';
|
||||||
import { StorageService } from '../storage/storage.service';
|
import { StorageService } from '../storage/storage.service';
|
||||||
|
import { E2eeService } from '../e2ee/e2ee.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
@ -50,7 +51,8 @@ export class ParticipantService {
|
||||||
private directiveService: OpenViduComponentsConfigService,
|
private directiveService: OpenViduComponentsConfigService,
|
||||||
private openviduService: OpenViduService,
|
private openviduService: OpenViduService,
|
||||||
private storageSrv: StorageService,
|
private storageSrv: StorageService,
|
||||||
private loggerSrv: LoggerService
|
private loggerSrv: LoggerService,
|
||||||
|
private e2eeService: E2eeService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('ParticipantService');
|
this.log = this.loggerSrv.get('ParticipantService');
|
||||||
this.localParticipant$ = this.localParticipantBS.asObservable();
|
this.localParticipant$ = this.localParticipantBS.asObservable();
|
||||||
|
|
@ -284,6 +286,26 @@ export class ParticipantService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the encryption error state for a participant.
|
||||||
|
* This is called when a participant cannot decrypt video streams due to an incorrect encryption key.
|
||||||
|
* @param participantSid - The SID of the participant with the encryption error
|
||||||
|
* @param hasError - Whether the participant has an encryption error
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
setEncryptionError(participantSid: string, hasError: boolean) {
|
||||||
|
if (this.localParticipant?.sid === participantSid) {
|
||||||
|
this.localParticipant.setEncryptionError(hasError);
|
||||||
|
this.updateLocalParticipant();
|
||||||
|
} else {
|
||||||
|
const participant = this.remoteParticipants.find((p) => p.sid === participantSid);
|
||||||
|
if (participant) {
|
||||||
|
participant.setEncryptionError(hasError);
|
||||||
|
this.updateRemoteParticipants();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the local participant name.
|
* Returns the local participant name.
|
||||||
*/
|
*/
|
||||||
|
|
@ -533,11 +555,45 @@ export class ParticipantService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private newParticipant(props: ParticipantProperties) {
|
private newParticipant(props: ParticipantProperties): ParticipantModel {
|
||||||
|
let participant: ParticipantModel;
|
||||||
if (this.globalService.hasParticipantFactory()) {
|
if (this.globalService.hasParticipantFactory()) {
|
||||||
return this.globalService.getParticipantFactory().apply(this, [props]);
|
participant = this.globalService.getParticipantFactory().apply(this, [props]);
|
||||||
|
} else {
|
||||||
|
participant = new ParticipantModel(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt participant name asynchronously if E2EE is enabled
|
||||||
|
this.decryptParticipantName(participant);
|
||||||
|
|
||||||
|
return participant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the participant name if E2EE is enabled.
|
||||||
|
* Updates the participant model asynchronously.
|
||||||
|
* @param participant - The participant model to decrypt the name for
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async decryptParticipantName(participant: ParticipantModel): Promise<void> {
|
||||||
|
const originalName = participant.name;
|
||||||
|
if (!originalName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decryptedName = await this.e2eeService.decryptOrMask(originalName, participant.identity);
|
||||||
|
participant.setDecryptedName(decryptedName);
|
||||||
|
|
||||||
|
// Update observables to reflect the decrypted name
|
||||||
|
if (participant.isLocal) {
|
||||||
|
this.updateLocalParticipant();
|
||||||
|
} else {
|
||||||
|
this.updateRemoteParticipants();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log.w('Failed to decrypt participant name:', error);
|
||||||
}
|
}
|
||||||
return new ParticipantModel(props);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getScreenCaptureOptions(): ScreenShareCaptureOptions {
|
private getScreenCaptureOptions(): ScreenShareCaptureOptions {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ import {
|
||||||
PreJoinDirective,
|
PreJoinDirective,
|
||||||
ParticipantPanelAfterLocalParticipantDirective,
|
ParticipantPanelAfterLocalParticipantDirective,
|
||||||
LayoutAdditionalElementsDirective,
|
LayoutAdditionalElementsDirective,
|
||||||
LeaveButtonDirective
|
LeaveButtonDirective,
|
||||||
|
SettingsPanelGeneralAdditionalElementsDirective,
|
||||||
|
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||||
} from '../../directives/template/internals.directive';
|
} from '../../directives/template/internals.directive';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,6 +51,12 @@ export interface TemplateConfiguration {
|
||||||
streamTemplate: TemplateRef<any>;
|
streamTemplate: TemplateRef<any>;
|
||||||
layoutAdditionalElementsTemplate?: TemplateRef<any>;
|
layoutAdditionalElementsTemplate?: TemplateRef<any>;
|
||||||
|
|
||||||
|
// Settings panel templates
|
||||||
|
settingsPanelGeneralAdditionalElementsTemplate?: TemplateRef<any>;
|
||||||
|
|
||||||
|
// Toolbar templates
|
||||||
|
toolbarMoreOptionsAdditionalMenuItemsTemplate?: TemplateRef<any>;
|
||||||
|
|
||||||
// PreJoin template
|
// PreJoin template
|
||||||
preJoinTemplate?: TemplateRef<any>;
|
preJoinTemplate?: TemplateRef<any>;
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +80,7 @@ export interface ToolbarTemplateConfiguration {
|
||||||
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
|
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
|
||||||
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
|
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
|
||||||
toolbarLeaveButtonTemplate?: TemplateRef<any>;
|
toolbarLeaveButtonTemplate?: TemplateRef<any>;
|
||||||
|
toolbarMoreOptionsAdditionalMenuItemsTemplate?: TemplateRef<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -126,6 +135,8 @@ export interface ExternalDirectives {
|
||||||
stream?: StreamDirective;
|
stream?: StreamDirective;
|
||||||
preJoin?: PreJoinDirective;
|
preJoin?: PreJoinDirective;
|
||||||
layoutAdditionalElements?: LayoutAdditionalElementsDirective;
|
layoutAdditionalElements?: LayoutAdditionalElementsDirective;
|
||||||
|
settingsPanelGeneralAdditionalElements?: SettingsPanelGeneralAdditionalElementsDirective;
|
||||||
|
toolbarMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -208,6 +219,16 @@ export class TemplateManagerService {
|
||||||
config.layoutAdditionalElementsTemplate = externalDirectives.layoutAdditionalElements.template;
|
config.layoutAdditionalElementsTemplate = externalDirectives.layoutAdditionalElements.template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (externalDirectives.settingsPanelGeneralAdditionalElements) {
|
||||||
|
this.log.v('Setting EXTERNAL SETTINGS PANEL GENERAL ADDITIONAL ELEMENTS');
|
||||||
|
config.settingsPanelGeneralAdditionalElementsTemplate = externalDirectives.settingsPanelGeneralAdditionalElements.template;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (externalDirectives.toolbarMoreOptionsAdditionalMenuItems) {
|
||||||
|
this.log.v('Setting EXTERNAL TOOLBAR MORE OPTIONS ADDITIONAL MENU ITEMS');
|
||||||
|
config.toolbarMoreOptionsAdditionalMenuItemsTemplate = externalDirectives.toolbarMoreOptionsAdditionalMenuItems.template;
|
||||||
|
}
|
||||||
|
|
||||||
this.log.v('Template setup completed', config);
|
this.log.v('Template setup completed', config);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
@ -368,14 +389,16 @@ export class TemplateManagerService {
|
||||||
setupToolbarTemplates(
|
setupToolbarTemplates(
|
||||||
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
|
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
|
||||||
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective,
|
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective,
|
||||||
externalLeaveButton?: LeaveButtonDirective
|
externalLeaveButton?: LeaveButtonDirective,
|
||||||
|
externalMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||||
): ToolbarTemplateConfiguration {
|
): ToolbarTemplateConfiguration {
|
||||||
this.log.v('Setting up toolbar templates...');
|
this.log.v('Setting up toolbar templates...');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template,
|
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template,
|
||||||
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template,
|
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template,
|
||||||
toolbarLeaveButtonTemplate: externalLeaveButton?.template
|
toolbarLeaveButtonTemplate: externalLeaveButton?.template,
|
||||||
|
toolbarMoreOptionsAdditionalMenuItemsTemplate: externalMoreOptionsAdditionalMenuItems?.template
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { SwitchBackgroundProcessorOptions } from '@livekit/track-processors';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { BackgroundEffect, EffectType } from '../../models/background-effect.model';
|
import { BackgroundEffect, EffectType } from '../../models/background-effect.model';
|
||||||
import { ParticipantService } from '../participant/participant.service';
|
import { ILogger } from '../../models/logger.model';
|
||||||
|
import { LoggerService } from '../logger/logger.service';
|
||||||
import { OpenViduService } from '../openvidu/openvidu.service';
|
import { OpenViduService } from '../openvidu/openvidu.service';
|
||||||
import { StorageService } from '../storage/storage.service';
|
import { StorageService } from '../storage/storage.service';
|
||||||
import { LocalVideoTrack, Track } from 'livekit-client';
|
|
||||||
import { BackgroundBlur, BackgroundOptions, ProcessorWrapper, VirtualBackground } from '@livekit/track-processors';
|
|
||||||
import { LoggerService } from '../logger/logger.service';
|
|
||||||
import { ILogger } from '../../models/logger.model';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -47,8 +45,8 @@ export class VirtualBackgroundService {
|
||||||
private HARD_BLUR_INTENSITY = 60;
|
private HARD_BLUR_INTENSITY = 60;
|
||||||
|
|
||||||
private log: ILogger;
|
private log: ILogger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private participantService: ParticipantService,
|
|
||||||
private openviduService: OpenViduService,
|
private openviduService: OpenViduService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private loggerSrv: LoggerService
|
private loggerSrv: LoggerService
|
||||||
|
|
@ -71,162 +69,59 @@ export class VirtualBackgroundService {
|
||||||
if (!!bgId) {
|
if (!!bgId) {
|
||||||
const background = this.backgrounds.find((bg) => bg.id === bgId);
|
const background = this.backgrounds.find((bg) => bg.id === bgId);
|
||||||
if (background) {
|
if (background) {
|
||||||
this.applyBackground(background);
|
await this.applyBackground(background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a background effect to the local video track.
|
||||||
|
* Works both in prejoin (using OpenViduService's processor) and in-room states.
|
||||||
|
* The background processor is centralized in OpenViduService for consistency.
|
||||||
|
*/
|
||||||
async applyBackground(bg: BackgroundEffect) {
|
async applyBackground(bg: BackgroundEffect) {
|
||||||
// If the background is already applied, do nothing
|
// If the background is already applied, do nothing
|
||||||
if (this.backgroundIsAlreadyApplied(bg.id)) return;
|
if (this.backgroundIsAlreadyApplied(bg.id)) return;
|
||||||
|
|
||||||
const cameraTrack = this.getCameraTrack();
|
|
||||||
if (!cameraTrack) {
|
|
||||||
this.log.e('No camera track found. Cannot apply background.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If no effect is selected, remove the background
|
const options = this.getBackgroundOptions(bg);
|
||||||
if (bg.type === EffectType.NONE) {
|
await this.openviduService.switchBackgroundMode(options);
|
||||||
await this.removeBackground();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentProcessor = cameraTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
|
|
||||||
|
|
||||||
// Check if the background is the same type as the previous one
|
|
||||||
if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) {
|
|
||||||
await this.replaceBackground(currentProcessor, bg);
|
|
||||||
} else {
|
|
||||||
// If the background is different, remove the previous one and apply the new one
|
|
||||||
const newProcessor = this.getBackgroundProcessor(bg);
|
|
||||||
if (!newProcessor) {
|
|
||||||
this.log.e('No processor found for the background effect.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.applyProcessorToCameraTrack(cameraTrack, newProcessor);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.storageService.setBackground(bg.id);
|
this.storageService.setBackground(bg.id);
|
||||||
this.backgroundIdSelected.next(bg.id);
|
this.backgroundIdSelected.next(bg.id);
|
||||||
|
this.log.d('Background applied:', options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.e('Error applying background effect:', error);
|
this.log.e('Error applying background effect:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackgroundOptions(bg: BackgroundEffect): BackgroundOptions {
|
|
||||||
if (bg.type === EffectType.IMAGE && bg.src) {
|
|
||||||
return { imagePath: bg.src };
|
|
||||||
} else if (bg.type === EffectType.BLUR) {
|
|
||||||
return {
|
|
||||||
blurRadius: bg.id === 'soft_blur' ? this.SOFT_BLUR_INTENSITY : this.HARD_BLUR_INTENSITY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeBackground() {
|
async removeBackground() {
|
||||||
if (this.isBackgroundApplied()) {
|
if (this.isBackgroundApplied()) {
|
||||||
this.backgroundIdSelected.next('no_effect');
|
this.backgroundIdSelected.next('no_effect');
|
||||||
const cameraTrack = this.getCameraTrack();
|
|
||||||
if (cameraTrack) {
|
|
||||||
try {
|
try {
|
||||||
await cameraTrack.stopProcessor();
|
await this.openviduService.switchBackgroundMode({ mode: 'disabled' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.log.w('Error stopping processor:', e);
|
this.log.w('Error disabling processor:', e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.storageService.removeBackground();
|
this.storageService.removeBackground();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackgroundProcessor(bg: BackgroundEffect): ProcessorWrapper<BackgroundOptions> | undefined {
|
private getBackgroundOptions(bg: BackgroundEffect): SwitchBackgroundProcessorOptions {
|
||||||
switch (bg.type) {
|
if (bg.type === EffectType.NONE) {
|
||||||
case EffectType.IMAGE:
|
return { mode: 'disabled' };
|
||||||
if (bg.src) {
|
} else if (bg.type === EffectType.IMAGE && bg.src) {
|
||||||
return VirtualBackground(bg.src);
|
return { mode: 'virtual-background', imagePath: bg.src };
|
||||||
|
} else if (bg.type === EffectType.BLUR) {
|
||||||
|
return {
|
||||||
|
mode: 'background-blur',
|
||||||
|
blurRadius: bg.id === 'soft_blur' ? this.SOFT_BLUR_INTENSITY : this.HARD_BLUR_INTENSITY
|
||||||
|
};
|
||||||
}
|
}
|
||||||
break;
|
return { mode: 'disabled' };
|
||||||
case EffectType.BLUR:
|
|
||||||
if (bg.id === 'soft_blur') {
|
|
||||||
return BackgroundBlur(this.SOFT_BLUR_INTENSITY);
|
|
||||||
} else if (bg.id === 'hard_blur') {
|
|
||||||
return BackgroundBlur(this.HARD_BLUR_INTENSITY);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the camera track from either the published tracks (if in room) or local tracks (if in prejoin)
|
|
||||||
* @returns The camera LocalTrack or undefined if not found
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private getCameraTrack(): LocalVideoTrack | undefined {
|
|
||||||
// First, try to get from published tracks (when in room)
|
|
||||||
if (this.openviduService.isRoomConnected()) {
|
|
||||||
const localParticipant = this.participantService.getLocalParticipant();
|
|
||||||
const cameraTrackPublication = localParticipant?.cameraTracks?.[0];
|
|
||||||
if (cameraTrackPublication?.track) {
|
|
||||||
return cameraTrackPublication.track as LocalVideoTrack;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to local tracks (when in prejoin or tracks not yet published)
|
|
||||||
const localTracks = this.openviduService.getLocalTracks();
|
|
||||||
const cameraTrack = localTracks.find((track) => track.kind === Track.Kind.Video);
|
|
||||||
return cameraTrack as LocalVideoTrack | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a background processor to the camera track
|
|
||||||
* @param cameraTrack The camera track to apply the processor to
|
|
||||||
* @param processor The background processor to apply
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async applyProcessorToCameraTrack(cameraTrack: LocalVideoTrack, processor: ProcessorWrapper<BackgroundOptions>): Promise<void> {
|
|
||||||
await cameraTrack.setProcessor(processor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private backgroundIsAlreadyApplied(backgroundId: string): boolean {
|
private backgroundIsAlreadyApplied(backgroundId: string): boolean {
|
||||||
return backgroundId === this.backgroundIdSelected.getValue();
|
return backgroundId === this.backgroundIdSelected.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces the current background effect with a new one by updating the processor options.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param currentProcessor - The current processor wrapper that handles background effects
|
|
||||||
* @param bg - The new background effect to apply
|
|
||||||
* @returns A Promise that resolves when the background options have been updated
|
|
||||||
* @throws Will throw an error if updating the background options fails
|
|
||||||
*/
|
|
||||||
private async replaceBackground(currentProcessor: ProcessorWrapper<BackgroundOptions>, bg: BackgroundEffect) {
|
|
||||||
try {
|
|
||||||
const options = this.getBackgroundOptions(bg);
|
|
||||||
// Update the processor with the new options
|
|
||||||
await currentProcessor.updateTransformerOptions(options);
|
|
||||||
this.log.d('Background options updated:', options);
|
|
||||||
} catch (error) {
|
|
||||||
this.log.e('Error updating background options:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the currently selected background has the same effect type as the provided one.
|
|
||||||
*
|
|
||||||
* @param type - The effect type to compare with the currently selected background.
|
|
||||||
* @returns `true` if the currently selected background has the same effect type, `false` otherwise.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private hasSameTypeAsPreviousOne(type: EffectType): boolean {
|
|
||||||
const currentBgId = this.backgroundIdSelected.getValue();
|
|
||||||
const currentBg = this.backgrounds.find((b) => b.id === currentBgId);
|
|
||||||
const isSameEffectType = currentBg && currentBg.type === type;
|
|
||||||
return !!isSameEffectType;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const safeJsonParse = <T = any>(text: string): T | null => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(text) as T;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -66,8 +66,10 @@ export * from './lib/services/storage/storage.service';
|
||||||
export * from './lib/services/translate/translate.service';
|
export * from './lib/services/translate/translate.service';
|
||||||
export * from './lib/services/theme/theme.service';
|
export * from './lib/services/theme/theme.service';
|
||||||
export * from './lib/services/viewport/viewport.service';
|
export * from './lib/services/viewport/viewport.service';
|
||||||
|
export * from './lib/services/e2ee/e2ee.service';
|
||||||
//Modules
|
//Modules
|
||||||
export * from './lib/openvidu-components-angular.module';
|
export * from './lib/openvidu-components-angular.module';
|
||||||
|
export * from './lib/config/custom-cdk-overlay';
|
||||||
export * from './lib/openvidu-components-angular-ui.module';
|
export * from './lib/openvidu-components-angular-ui.module';
|
||||||
|
|
||||||
export * from 'livekit-client';
|
export * from 'livekit-client';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { Subject, of } from 'rxjs';
|
||||||
|
export class ActionServiceMock {
|
||||||
|
openConnectionDialog(title?: string, description?: string, allowClose?: boolean): void {}
|
||||||
|
closeConnectionDialog(): void {}
|
||||||
|
openDialog(title?: string, description?: string, allowClose?: boolean): void {}
|
||||||
|
openDeleteRecordingDialog(callback?: () => void): void {
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
openRecordingPlayerDialog(src?: string, allowClose?: boolean): void {}
|
||||||
|
launchNotification(options?: any, callback?: () => void): void {
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatDialogRefMock {
|
||||||
|
private closed$ = new Subject<boolean>();
|
||||||
|
// expose a jasmine spy for close so tests can assert it was called
|
||||||
|
close = jasmine.createSpy('close').and.callFake(() => {
|
||||||
|
// when close is called, emit and complete the closed observable
|
||||||
|
this.closed$.next(true);
|
||||||
|
this.closed$.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterClosed() {
|
||||||
|
// return an observable that only emits when close() is called
|
||||||
|
return this.closed$.asObservable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatDialogMock {
|
||||||
|
opens = 0;
|
||||||
|
lastRef: MatDialogRefMock | null = null;
|
||||||
|
|
||||||
|
open(component?: any) {
|
||||||
|
this.opens++;
|
||||||
|
// If the consumer opens the DeleteDialogComponent, return a ref that emits immediately
|
||||||
|
// (some tests expect afterClosed to already have emitted for confirm/delete dialogs)
|
||||||
|
if (component && component.name === 'DeleteDialogComponent') {
|
||||||
|
const immediateRef: any = {
|
||||||
|
close: jasmine.createSpy('close'),
|
||||||
|
afterClosed: () => of(true)
|
||||||
|
};
|
||||||
|
this.lastRef = immediateRef as unknown as MatDialogRefMock;
|
||||||
|
return immediateRef as unknown as MatDialogRef<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastRef = new MatDialogRefMock();
|
||||||
|
return this.lastRef as unknown as MatDialogRef<any>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
export class LoggerServiceMock {
|
||||||
|
get() {
|
||||||
|
return {
|
||||||
|
d: () => {},
|
||||||
|
i: () => {},
|
||||||
|
e: () => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OpenViduComponentsConfigServiceMock {
|
||||||
|
// Expose e2eeKey$ as a BehaviorSubject so tests can emit values
|
||||||
|
e2eeKey$ = new BehaviorSubject<string | null>(null);
|
||||||
|
|
||||||
|
getE2EEKey() {
|
||||||
|
return this.e2eeKey$.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateE2EEKey(value: string | null) {
|
||||||
|
this.e2eeKey$.next(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
export class TranslateServiceMock {
|
||||||
|
instant(key: string): string {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
get(key: string) {
|
||||||
|
return of(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"pretty": false,
|
"pretty": false,
|
||||||
// "skipLibCheck": true // Livekit track processors fails with typescript types checking
|
"skipLibCheck": true // TODO Livekit track processors fails with typescript types checking
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"compilationMode": "partial"
|
"compilationMode": "partial"
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,6 @@ services:
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
image: docker.io/busybox:1.37.0
|
image: docker.io/busybox:1.37.0
|
||||||
platform: linux/amd64
|
|
||||||
restart: "no"
|
restart: "no"
|
||||||
volumes:
|
volumes:
|
||||||
- minio-data:/minio
|
- minio-data:/minio
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
services:
|
services:
|
||||||
|
|
||||||
caddy-proxy:
|
caddy-proxy:
|
||||||
image: docker.io/openvidu/openvidu-caddy-local:3.4.0
|
image: docker.io/openvidu/openvidu-caddy-local:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
|
|
@ -19,6 +18,7 @@ services:
|
||||||
- MEET_INITIAL_API_KEY=${MEET_INITIAL_API_KEY:-meet-api-key}
|
- MEET_INITIAL_API_KEY=${MEET_INITIAL_API_KEY:-meet-api-key}
|
||||||
volumes:
|
volumes:
|
||||||
- scripts:/scripts
|
- scripts:/scripts
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
entrypoint: /bin/sh /scripts/entrypoint_caddy.sh
|
entrypoint: /bin/sh /scripts/entrypoint_caddy.sh
|
||||||
ports:
|
ports:
|
||||||
- 5443:5443
|
- 5443:5443
|
||||||
|
|
@ -31,13 +31,13 @@ services:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: docker.io/redis:7.4.4-alpine
|
image: docker.io/redis:8.2.2-alpine
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
command: >
|
command: >
|
||||||
redis-server
|
redis-server
|
||||||
--bind 0.0.0.0
|
--bind 0.0.0.0
|
||||||
|
|
@ -47,8 +47,7 @@ services:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
minio:
|
minio:
|
||||||
image: docker.io/openvidu/minio:2025.5.24-debian-12-r1
|
image: docker.io/openvidu/minio:2025.9.7-debian-12-r3
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 9000:9000
|
- 9000:9000
|
||||||
|
|
@ -58,21 +57,23 @@ services:
|
||||||
- MINIO_DEFAULT_BUCKETS=openvidu-appdata
|
- MINIO_DEFAULT_BUCKETS=openvidu-appdata
|
||||||
- MINIO_CONSOLE_SUBPATH=/minio-console
|
- MINIO_CONSOLE_SUBPATH=/minio-console
|
||||||
- MINIO_BROWSER_REDIRECT_URL=http://localhost:7880/minio-console
|
- MINIO_BROWSER_REDIRECT_URL=http://localhost:7880/minio-console
|
||||||
|
- MINIO_BROWSER=on
|
||||||
volumes:
|
volumes:
|
||||||
- minio-data:/bitnami/minio/data
|
- minio-data:/bitnami/minio/data
|
||||||
- minio-certs:/certs
|
- minio-certs:/certs
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
setup:
|
setup:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
mongo:
|
mongo:
|
||||||
image: docker.io/openvidu/mongodb:8.0.9
|
image: docker.io/openvidu/mongodb:8.0.15-r0
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- 27017:27017
|
- 27017:27017
|
||||||
volumes:
|
volumes:
|
||||||
- mongo-data:/bitnami/mongodb
|
- mongo-data:/bitnami/mongodb
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
environment:
|
environment:
|
||||||
- MONGODB_ROOT_USER=${MONGO_ADMIN_USERNAME:-mongoadmin}
|
- MONGODB_ROOT_USER=${MONGO_ADMIN_USERNAME:-mongoadmin}
|
||||||
- MONGODB_ROOT_PASSWORD=${MONGO_ADMIN_PASSWORD:-mongoadmin}
|
- MONGODB_ROOT_PASSWORD=${MONGO_ADMIN_PASSWORD:-mongoadmin}
|
||||||
|
|
@ -80,27 +81,26 @@ services:
|
||||||
- MONGODB_REPLICA_SET_MODE=primary
|
- MONGODB_REPLICA_SET_MODE=primary
|
||||||
- MONGODB_REPLICA_SET_NAME=rs0
|
- MONGODB_REPLICA_SET_NAME=rs0
|
||||||
- MONGODB_REPLICA_SET_KEY=devreplicasetkey
|
- MONGODB_REPLICA_SET_KEY=devreplicasetkey
|
||||||
- EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
|
|
||||||
depends_on:
|
depends_on:
|
||||||
setup:
|
setup:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
image: docker.io/openvidu/openvidu-dashboard:3.4.0
|
image: docker.io/openvidu/openvidu-dashboard:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- SERVER_PORT=5000
|
- SERVER_PORT=5000
|
||||||
- ADMIN_USERNAME=${DASHBOARD_ADMIN_USERNAME:-admin}
|
- ADMIN_USERNAME=${DASHBOARD_ADMIN_USERNAME:-admin}
|
||||||
- ADMIN_PASSWORD=${DASHBOARD_ADMIN_PASSWORD:-admin}
|
- ADMIN_PASSWORD=${DASHBOARD_ADMIN_PASSWORD:-admin}
|
||||||
- DATABASE_URL=mongodb://${MONGO_ADMIN_USERNAME:-mongoadmin}:${MONGO_ADMIN_PASSWORD:-mongoadmin}@mongo:27017/?replicaSet=rs0&readPreference=primaryPreferred
|
- DATABASE_URL=mongodb://${MONGO_ADMIN_USERNAME:-mongoadmin}:${MONGO_ADMIN_PASSWORD:-mongoadmin}@mongo:27017/?replicaSet=rs0&readPreference=primaryPreferred
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
setup:
|
setup:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
openvidu:
|
openvidu:
|
||||||
image: docker.io/openvidu/openvidu-server:3.4.0
|
image: docker.io/openvidu/openvidu-server:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
|
|
@ -115,13 +115,13 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- scripts:/scripts
|
- scripts:/scripts
|
||||||
- config:/config
|
- config:/config
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
setup:
|
setup:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
image: docker.io/openvidu/ingress:3.4.0
|
image: docker.io/openvidu/ingress:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
|
|
@ -133,13 +133,13 @@ services:
|
||||||
- INGRESS_CONFIG_FILE=/config/ingress.yaml
|
- INGRESS_CONFIG_FILE=/config/ingress.yaml
|
||||||
volumes:
|
volumes:
|
||||||
- config:/config
|
- config:/config
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
setup:
|
setup:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
egress:
|
egress:
|
||||||
image: docker.io/openvidu/egress:3.4.0
|
image: docker.io/openvidu/egress:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
|
|
@ -148,20 +148,20 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- config:/config
|
- config:/config
|
||||||
- egress-data:/home/egress/tmp
|
- egress-data:/home/egress/tmp
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
setup:
|
setup:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
operator:
|
operator:
|
||||||
image: docker.io/openvidu/openvidu-operator:3.4.0
|
image: docker.io/openvidu/openvidu-operator:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- agents-config:/agents-config
|
- agents-config:/agents-config
|
||||||
- operator-deployment:/deployment
|
- operator-deployment:/deployment
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
environment:
|
environment:
|
||||||
- PLATFORM=linux/amd64
|
|
||||||
- MODE=agent-manager-local
|
- MODE=agent-manager-local
|
||||||
- DEPLOYMENT_FILES_DIR=/deployment
|
- DEPLOYMENT_FILES_DIR=/deployment
|
||||||
- AGENTS_CONFIG_DIR=/agents-config
|
- AGENTS_CONFIG_DIR=/agents-config
|
||||||
|
|
@ -177,8 +177,7 @@ services:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
openvidu-meet:
|
openvidu-meet:
|
||||||
image: docker.io/openvidu/openvidu-meet:3.4.0
|
image: docker.io/openvidu/openvidu-meet:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
ports:
|
||||||
- 9080:6080
|
- 9080:6080
|
||||||
|
|
@ -209,16 +208,17 @@ services:
|
||||||
- MEET_REDIS_PORT=6379
|
- MEET_REDIS_PORT=6379
|
||||||
- MEET_REDIS_PASSWORD=${REDIS_PASSWORD:-redispassword}
|
- MEET_REDIS_PASSWORD=${REDIS_PASSWORD:-redispassword}
|
||||||
- MEET_REDIS_DB=0
|
- MEET_REDIS_DB=0
|
||||||
|
- MEET_MONGO_URI=mongodb://${MONGO_ADMIN_USERNAME:-mongoadmin}:${MONGO_ADMIN_PASSWORD:-mongoadmin}@mongo:27017/?replicaSet=rs0&readPreference=primaryPreferred
|
||||||
volumes:
|
volumes:
|
||||||
- scripts:/scripts
|
- scripts:/scripts
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
entrypoint: /bin/sh /scripts/entrypoint_openvidu_meet.sh
|
entrypoint: /bin/sh /scripts/entrypoint_openvidu_meet.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
setup:
|
setup:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
openvidu-meet-init:
|
openvidu-meet-init:
|
||||||
image: docker.io/openvidu/openvidu-operator:3.4.0
|
image: docker.io/openvidu/openvidu-operator:main
|
||||||
platform: linux/amd64
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- MODE=local-ready-check
|
- MODE=local-ready-check
|
||||||
|
|
@ -235,6 +235,7 @@ services:
|
||||||
- MEET_INITIAL_API_KEY=${MEET_INITIAL_API_KEY:-meet-api-key}
|
- MEET_INITIAL_API_KEY=${MEET_INITIAL_API_KEY:-meet-api-key}
|
||||||
volumes:
|
volumes:
|
||||||
- scripts:/scripts
|
- scripts:/scripts
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
entrypoint: /bin/sh /scripts/entrypoint_ready_check.sh
|
entrypoint: /bin/sh /scripts/entrypoint_ready_check.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
- caddy-proxy
|
- caddy-proxy
|
||||||
|
|
|
||||||
|
|
@ -81,42 +81,27 @@ Parameters:
|
||||||
Type: String
|
Type: String
|
||||||
Default: c6a.xlarge
|
Default: c6a.xlarge
|
||||||
AllowedValues:
|
AllowedValues:
|
||||||
- t2.large
|
- t3.nano
|
||||||
- t2.xlarge
|
- t3.micro
|
||||||
- t2.2xlarge
|
- t3.small
|
||||||
- t3.medium
|
- t3.medium
|
||||||
- t3.large
|
- t3.large
|
||||||
- t3.xlarge
|
- t3.xlarge
|
||||||
- t3.2xlarge
|
- t3.2xlarge
|
||||||
- m4.large
|
- t3a.nano
|
||||||
- m4.xlarge
|
- t3a.micro
|
||||||
- m4.2xlarge
|
- t3a.small
|
||||||
- m4.4xlarge
|
- t3a.medium
|
||||||
- m4.10xlarge
|
- t3a.large
|
||||||
- m4.16xlarge
|
- t3a.xlarge
|
||||||
- m5.large
|
- t3a.2xlarge
|
||||||
- m5.xlarge
|
- t4g.nano
|
||||||
- m5.2xlarge
|
- t4g.micro
|
||||||
- m5.4xlarge
|
- t4g.small
|
||||||
- m5.8xlarge
|
- t4g.medium
|
||||||
- m5.12xlarge
|
- t4g.large
|
||||||
- m5.16xlarge
|
- t4g.xlarge
|
||||||
- m5.24xlarge
|
- t4g.2xlarge
|
||||||
- m6i.large
|
|
||||||
- m6i.xlarge
|
|
||||||
- m6i.2xlarge
|
|
||||||
- m6i.4xlarge
|
|
||||||
- m6i.8xlarge
|
|
||||||
- m6i.12xlarge
|
|
||||||
- m6i.16xlarge
|
|
||||||
- m6i.24xlarge
|
|
||||||
- m6i.32xlarge
|
|
||||||
- m6i.metal
|
|
||||||
- c4.large
|
|
||||||
- c4.xlarge
|
|
||||||
- c4.2xlarge
|
|
||||||
- c4.4xlarge
|
|
||||||
- c4.8xlarge
|
|
||||||
- c5.large
|
- c5.large
|
||||||
- c5.xlarge
|
- c5.xlarge
|
||||||
- c5.2xlarge
|
- c5.2xlarge
|
||||||
|
|
@ -125,6 +110,39 @@ Parameters:
|
||||||
- c5.12xlarge
|
- c5.12xlarge
|
||||||
- c5.18xlarge
|
- c5.18xlarge
|
||||||
- c5.24xlarge
|
- c5.24xlarge
|
||||||
|
- c5.metal
|
||||||
|
- c5a.large
|
||||||
|
- c5a.xlarge
|
||||||
|
- c5a.2xlarge
|
||||||
|
- c5a.4xlarge
|
||||||
|
- c5a.8xlarge
|
||||||
|
- c5a.12xlarge
|
||||||
|
- c5a.16xlarge
|
||||||
|
- c5a.24xlarge
|
||||||
|
- c5ad.large
|
||||||
|
- c5ad.xlarge
|
||||||
|
- c5ad.2xlarge
|
||||||
|
- c5ad.4xlarge
|
||||||
|
- c5ad.8xlarge
|
||||||
|
- c5ad.12xlarge
|
||||||
|
- c5ad.16xlarge
|
||||||
|
- c5ad.24xlarge
|
||||||
|
- c5d.large
|
||||||
|
- c5d.xlarge
|
||||||
|
- c5d.2xlarge
|
||||||
|
- c5d.4xlarge
|
||||||
|
- c5d.9xlarge
|
||||||
|
- c5d.12xlarge
|
||||||
|
- c5d.18xlarge
|
||||||
|
- c5d.24xlarge
|
||||||
|
- c5d.metal
|
||||||
|
- c5n.large
|
||||||
|
- c5n.xlarge
|
||||||
|
- c5n.2xlarge
|
||||||
|
- c5n.4xlarge
|
||||||
|
- c5n.9xlarge
|
||||||
|
- c5n.18xlarge
|
||||||
|
- c5n.metal
|
||||||
- c6a.large
|
- c6a.large
|
||||||
- c6a.xlarge
|
- c6a.xlarge
|
||||||
- c6a.2xlarge
|
- c6a.2xlarge
|
||||||
|
|
@ -136,6 +154,32 @@ Parameters:
|
||||||
- c6a.32xlarge
|
- c6a.32xlarge
|
||||||
- c6a.48xlarge
|
- c6a.48xlarge
|
||||||
- c6a.metal
|
- c6a.metal
|
||||||
|
- c6g.medium
|
||||||
|
- c6g.large
|
||||||
|
- c6g.xlarge
|
||||||
|
- c6g.2xlarge
|
||||||
|
- c6g.4xlarge
|
||||||
|
- c6g.8xlarge
|
||||||
|
- c6g.12xlarge
|
||||||
|
- c6g.16xlarge
|
||||||
|
- c6g.metal
|
||||||
|
- c6gd.medium
|
||||||
|
- c6gd.large
|
||||||
|
- c6gd.xlarge
|
||||||
|
- c6gd.2xlarge
|
||||||
|
- c6gd.4xlarge
|
||||||
|
- c6gd.8xlarge
|
||||||
|
- c6gd.12xlarge
|
||||||
|
- c6gd.16xlarge
|
||||||
|
- c6gd.metal
|
||||||
|
- c6gn.medium
|
||||||
|
- c6gn.large
|
||||||
|
- c6gn.xlarge
|
||||||
|
- c6gn.2xlarge
|
||||||
|
- c6gn.4xlarge
|
||||||
|
- c6gn.8xlarge
|
||||||
|
- c6gn.12xlarge
|
||||||
|
- c6gn.16xlarge
|
||||||
- c6i.large
|
- c6i.large
|
||||||
- c6i.xlarge
|
- c6i.xlarge
|
||||||
- c6i.2xlarge
|
- c6i.2xlarge
|
||||||
|
|
@ -146,6 +190,26 @@ Parameters:
|
||||||
- c6i.24xlarge
|
- c6i.24xlarge
|
||||||
- c6i.32xlarge
|
- c6i.32xlarge
|
||||||
- c6i.metal
|
- c6i.metal
|
||||||
|
- c6id.large
|
||||||
|
- c6id.xlarge
|
||||||
|
- c6id.2xlarge
|
||||||
|
- c6id.4xlarge
|
||||||
|
- c6id.8xlarge
|
||||||
|
- c6id.12xlarge
|
||||||
|
- c6id.16xlarge
|
||||||
|
- c6id.24xlarge
|
||||||
|
- c6id.32xlarge
|
||||||
|
- c6id.metal
|
||||||
|
- c6in.large
|
||||||
|
- c6in.xlarge
|
||||||
|
- c6in.2xlarge
|
||||||
|
- c6in.4xlarge
|
||||||
|
- c6in.8xlarge
|
||||||
|
- c6in.12xlarge
|
||||||
|
- c6in.16xlarge
|
||||||
|
- c6in.24xlarge
|
||||||
|
- c6in.32xlarge
|
||||||
|
- c6in.metal
|
||||||
- c7a.medium
|
- c7a.medium
|
||||||
- c7a.large
|
- c7a.large
|
||||||
- c7a.xlarge
|
- c7a.xlarge
|
||||||
|
|
@ -158,6 +222,40 @@ Parameters:
|
||||||
- c7a.32xlarge
|
- c7a.32xlarge
|
||||||
- c7a.48xlarge
|
- c7a.48xlarge
|
||||||
- c7a.metal-48xl
|
- c7a.metal-48xl
|
||||||
|
- c7g.medium
|
||||||
|
- c7g.large
|
||||||
|
- c7g.xlarge
|
||||||
|
- c7g.2xlarge
|
||||||
|
- c7g.4xlarge
|
||||||
|
- c7g.8xlarge
|
||||||
|
- c7g.12xlarge
|
||||||
|
- c7g.16xlarge
|
||||||
|
- c7g.metal
|
||||||
|
- c7gd.medium
|
||||||
|
- c7gd.large
|
||||||
|
- c7gd.xlarge
|
||||||
|
- c7gd.2xlarge
|
||||||
|
- c7gd.4xlarge
|
||||||
|
- c7gd.8xlarge
|
||||||
|
- c7gd.12xlarge
|
||||||
|
- c7gd.16xlarge
|
||||||
|
- c7gd.metal
|
||||||
|
- c7gn.medium
|
||||||
|
- c7gn.large
|
||||||
|
- c7gn.xlarge
|
||||||
|
- c7gn.2xlarge
|
||||||
|
- c7gn.4xlarge
|
||||||
|
- c7gn.8xlarge
|
||||||
|
- c7gn.12xlarge
|
||||||
|
- c7gn.16xlarge
|
||||||
|
- c7gn.metal
|
||||||
|
- c7i-flex.large
|
||||||
|
- c7i-flex.xlarge
|
||||||
|
- c7i-flex.2xlarge
|
||||||
|
- c7i-flex.4xlarge
|
||||||
|
- c7i-flex.8xlarge
|
||||||
|
- c7i-flex.12xlarge
|
||||||
|
- c7i-flex.16xlarge
|
||||||
- c7i.large
|
- c7i.large
|
||||||
- c7i.xlarge
|
- c7i.xlarge
|
||||||
- c7i.2xlarge
|
- c7i.2xlarge
|
||||||
|
|
@ -169,20 +267,77 @@ Parameters:
|
||||||
- c7i.48xlarge
|
- c7i.48xlarge
|
||||||
- c7i.metal-24xl
|
- c7i.metal-24xl
|
||||||
- c7i.metal-48xl
|
- c7i.metal-48xl
|
||||||
- c5n.large
|
- c8g.medium
|
||||||
- c5n.xlarge
|
- c8g.large
|
||||||
- c5n.2xlarge
|
- c8g.xlarge
|
||||||
- c5n.4xlarge
|
- c8g.2xlarge
|
||||||
- c5n.9xlarge
|
- c8g.4xlarge
|
||||||
- c5n.18xlarge
|
- c8g.8xlarge
|
||||||
- m5n.large
|
- c8g.12xlarge
|
||||||
- m5n.xlarge
|
- c8g.16xlarge
|
||||||
- m5n.2xlarge
|
- c8g.24xlarge
|
||||||
- m5n.4xlarge
|
- c8g.48xlarge
|
||||||
- m5n.8xlarge
|
- c8g.metal-24xl
|
||||||
- m5n.12xlarge
|
- c8g.metal-48xl
|
||||||
- m5n.16xlarge
|
- m6a.large
|
||||||
- m5n.24xlarge
|
- m6a.xlarge
|
||||||
|
- m6a.2xlarge
|
||||||
|
- m6a.4xlarge
|
||||||
|
- m6a.8xlarge
|
||||||
|
- m6a.12xlarge
|
||||||
|
- m6a.16xlarge
|
||||||
|
- m6a.24xlarge
|
||||||
|
- m6a.32xlarge
|
||||||
|
- m6a.48xlarge
|
||||||
|
- m6a.metal
|
||||||
|
- m6g.medium
|
||||||
|
- m6g.large
|
||||||
|
- m6g.xlarge
|
||||||
|
- m6g.2xlarge
|
||||||
|
- m6g.4xlarge
|
||||||
|
- m6g.8xlarge
|
||||||
|
- m6g.12xlarge
|
||||||
|
- m6g.16xlarge
|
||||||
|
- m6g.metal
|
||||||
|
- m6gd.medium
|
||||||
|
- m6gd.large
|
||||||
|
- m6gd.xlarge
|
||||||
|
- m6gd.2xlarge
|
||||||
|
- m6gd.4xlarge
|
||||||
|
- m6gd.8xlarge
|
||||||
|
- m6gd.12xlarge
|
||||||
|
- m6gd.16xlarge
|
||||||
|
- m6gd.metal
|
||||||
|
- m6i.large
|
||||||
|
- m6i.xlarge
|
||||||
|
- m6i.2xlarge
|
||||||
|
- m6i.4xlarge
|
||||||
|
- m6i.8xlarge
|
||||||
|
- m6i.12xlarge
|
||||||
|
- m6i.16xlarge
|
||||||
|
- m6i.24xlarge
|
||||||
|
- m6i.32xlarge
|
||||||
|
- m6i.metal
|
||||||
|
- m6id.large
|
||||||
|
- m6id.xlarge
|
||||||
|
- m6id.2xlarge
|
||||||
|
- m6id.4xlarge
|
||||||
|
- m6id.8xlarge
|
||||||
|
- m6id.12xlarge
|
||||||
|
- m6id.16xlarge
|
||||||
|
- m6id.24xlarge
|
||||||
|
- m6id.32xlarge
|
||||||
|
- m6id.metal
|
||||||
|
- m6idn.large
|
||||||
|
- m6idn.xlarge
|
||||||
|
- m6idn.2xlarge
|
||||||
|
- m6idn.4xlarge
|
||||||
|
- m6idn.8xlarge
|
||||||
|
- m6idn.12xlarge
|
||||||
|
- m6idn.16xlarge
|
||||||
|
- m6idn.24xlarge
|
||||||
|
- m6idn.32xlarge
|
||||||
|
- m6idn.metal
|
||||||
- m6in.large
|
- m6in.large
|
||||||
- m6in.xlarge
|
- m6in.xlarge
|
||||||
- m6in.2xlarge
|
- m6in.2xlarge
|
||||||
|
|
@ -192,14 +347,67 @@ Parameters:
|
||||||
- m6in.16xlarge
|
- m6in.16xlarge
|
||||||
- m6in.24xlarge
|
- m6in.24xlarge
|
||||||
- m6in.32xlarge
|
- m6in.32xlarge
|
||||||
- r5n.large
|
- m6in.metal
|
||||||
- r5n.xlarge
|
- m7a.medium
|
||||||
- r5n.2xlarge
|
- m7a.large
|
||||||
- r5n.4xlarge
|
- m7a.xlarge
|
||||||
- r5n.8xlarge
|
- m7a.2xlarge
|
||||||
- r5n.12xlarge
|
- m7a.4xlarge
|
||||||
- r5n.16xlarge
|
- m7a.8xlarge
|
||||||
- r5n.24xlarge
|
- m7a.12xlarge
|
||||||
|
- m7a.16xlarge
|
||||||
|
- m7a.24xlarge
|
||||||
|
- m7a.32xlarge
|
||||||
|
- m7a.48xlarge
|
||||||
|
- m7a.metal-48xl
|
||||||
|
- m7g.medium
|
||||||
|
- m7g.large
|
||||||
|
- m7g.xlarge
|
||||||
|
- m7g.2xlarge
|
||||||
|
- m7g.4xlarge
|
||||||
|
- m7g.8xlarge
|
||||||
|
- m7g.12xlarge
|
||||||
|
- m7g.16xlarge
|
||||||
|
- m7g.metal
|
||||||
|
- m7gd.medium
|
||||||
|
- m7gd.large
|
||||||
|
- m7gd.xlarge
|
||||||
|
- m7gd.2xlarge
|
||||||
|
- m7gd.4xlarge
|
||||||
|
- m7gd.8xlarge
|
||||||
|
- m7gd.12xlarge
|
||||||
|
- m7gd.16xlarge
|
||||||
|
- m7gd.metal
|
||||||
|
- m7i-flex.large
|
||||||
|
- m7i-flex.xlarge
|
||||||
|
- m7i-flex.2xlarge
|
||||||
|
- m7i-flex.4xlarge
|
||||||
|
- m7i-flex.8xlarge
|
||||||
|
- m7i-flex.12xlarge
|
||||||
|
- m7i-flex.16xlarge
|
||||||
|
- m7i.large
|
||||||
|
- m7i.xlarge
|
||||||
|
- m7i.2xlarge
|
||||||
|
- m7i.4xlarge
|
||||||
|
- m7i.8xlarge
|
||||||
|
- m7i.12xlarge
|
||||||
|
- m7i.16xlarge
|
||||||
|
- m7i.24xlarge
|
||||||
|
- m7i.48xlarge
|
||||||
|
- m7i.metal-24xl
|
||||||
|
- m7i.metal-48xl
|
||||||
|
- m8g.medium
|
||||||
|
- m8g.large
|
||||||
|
- m8g.xlarge
|
||||||
|
- m8g.2xlarge
|
||||||
|
- m8g.4xlarge
|
||||||
|
- m8g.8xlarge
|
||||||
|
- m8g.12xlarge
|
||||||
|
- m8g.16xlarge
|
||||||
|
- m8g.24xlarge
|
||||||
|
- m8g.48xlarge
|
||||||
|
- m8g.metal-24xl
|
||||||
|
- m8g.metal-48xl
|
||||||
ConstraintDescription: "Must be a valid EC2 instance type"
|
ConstraintDescription: "Must be a valid EC2 instance type"
|
||||||
|
|
||||||
KeyName:
|
KeyName:
|
||||||
|
|
@ -208,10 +416,11 @@ Parameters:
|
||||||
AllowedPattern: ^.+$
|
AllowedPattern: ^.+$
|
||||||
ConstraintDescription: must be the name of an existing EC2 KeyPair.
|
ConstraintDescription: must be the name of an existing EC2 KeyPair.
|
||||||
|
|
||||||
AmiId:
|
OperatingSystem:
|
||||||
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
|
Description: VSCode Server EC2 operating system
|
||||||
Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id
|
Type: String
|
||||||
Description: AMI ID for the EC2 instances
|
Default: "Ubuntu-24"
|
||||||
|
AllowedValues: ['Ubuntu-22', 'Ubuntu-24']
|
||||||
|
|
||||||
S3AppDataBucketName:
|
S3AppDataBucketName:
|
||||||
Type: String
|
Type: String
|
||||||
|
|
@ -238,7 +447,7 @@ Metadata:
|
||||||
Parameters:
|
Parameters:
|
||||||
- InstanceType
|
- InstanceType
|
||||||
- KeyName
|
- KeyName
|
||||||
- AmiId
|
- OperatingSystem
|
||||||
- Label:
|
- Label:
|
||||||
default: S3 bucket for application data and recordings
|
default: S3 bucket for application data and recordings
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
@ -258,6 +467,31 @@ Conditions:
|
||||||
PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]
|
PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]
|
||||||
PublicElasticIPAbsent: !Equals [!Ref PublicElasticIP, ""]
|
PublicElasticIPAbsent: !Equals [!Ref PublicElasticIP, ""]
|
||||||
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
|
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
|
||||||
|
IsGraviton: !Or
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 't4g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c6g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c7g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c7gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'c8g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm6g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm6gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm7g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm7gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref InstanceType ]], 'm8g']
|
||||||
|
|
||||||
|
Mappings:
|
||||||
|
ArmImage:
|
||||||
|
# aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/" --recursive --query "Parameters[*].Name" > canonical-ami.txt
|
||||||
|
Ubuntu-22:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/arm64/hvm/ebs-gp2/ami-id}}'
|
||||||
|
Ubuntu-24:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/arm64/hvm/ebs-gp3/ami-id}}'
|
||||||
|
AmdImage:
|
||||||
|
Ubuntu-22:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id}}'
|
||||||
|
Ubuntu-24:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id}}'
|
||||||
|
|
||||||
|
|
||||||
Resources:
|
Resources:
|
||||||
|
|
||||||
|
|
@ -384,7 +618,7 @@ Resources:
|
||||||
'/usr/local/bin/install.sh':
|
'/usr/local/bin/install.sh':
|
||||||
content: !Sub |
|
content: !Sub |
|
||||||
#!/bin/bash -x
|
#!/bin/bash -x
|
||||||
OPENVIDU_VERSION=3.4.1
|
OPENVIDU_VERSION=main
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
YQ_VERSION=v4.44.5
|
YQ_VERSION=v4.44.5
|
||||||
|
|
||||||
|
|
@ -827,7 +1061,10 @@ Resources:
|
||||||
owner: "root"
|
owner: "root"
|
||||||
group: "root"
|
group: "root"
|
||||||
Properties:
|
Properties:
|
||||||
ImageId: !Ref AmiId
|
ImageId: !If
|
||||||
|
- IsGraviton
|
||||||
|
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
|
||||||
|
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
|
||||||
LaunchTemplate:
|
LaunchTemplate:
|
||||||
# Enable IMDSv2 by default
|
# Enable IMDSv2 by default
|
||||||
LaunchTemplateId: !Ref IMDSv2LaunchTemplate
|
LaunchTemplateId: !Ref IMDSv2LaunchTemplate
|
||||||
|
|
@ -845,6 +1082,8 @@ Resources:
|
||||||
#!/bin/bash -x
|
#!/bin/bash -x
|
||||||
set -eu -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
ec2-instance-connect
|
ec2-instance-connect
|
||||||
|
|
|
||||||
|
|
@ -45,117 +45,8 @@ param initialMeetApiKey string = ''
|
||||||
|
|
||||||
|
|
||||||
// Azure instance config
|
// Azure instance config
|
||||||
@description('Specifies the azure vm size for your OpenVidu instance')
|
@description('Specifies the azure vm size for your OpenVidu instance. You can use any valid Azure VM size (e.g., Standard_B4s, Standard_D4s_v5, Standard_E4ps_v5). See https://learn.microsoft.com/en-us/azure/virtual-machines/sizes for available sizes.')
|
||||||
@allowed([
|
param instanceType string = 'Standard_B4s'
|
||||||
'Standard_B1s'
|
|
||||||
'Standard_B1ms'
|
|
||||||
'Standard_B2s'
|
|
||||||
'Standard_B2ms'
|
|
||||||
'Standard_B4ms'
|
|
||||||
'Standard_B8ms'
|
|
||||||
'Standard_D2_v3'
|
|
||||||
'Standard_D4_v3'
|
|
||||||
'Standard_D8_v3'
|
|
||||||
'Standard_D16_v3'
|
|
||||||
'Standard_D32_v3'
|
|
||||||
'Standard_D48_v3'
|
|
||||||
'Standard_D64_v3'
|
|
||||||
'Standard_D2_v4'
|
|
||||||
'Standard_D4_v4'
|
|
||||||
'Standard_D8_v4'
|
|
||||||
'Standard_D16_v4'
|
|
||||||
'Standard_D32_v4'
|
|
||||||
'Standard_D48_v4'
|
|
||||||
'Standard_D64_v4'
|
|
||||||
'Standard_D96_v4'
|
|
||||||
'Standard_D2_v5'
|
|
||||||
'Standard_D4_v5'
|
|
||||||
'Standard_D8_v5'
|
|
||||||
'Standard_D16_v5'
|
|
||||||
'Standard_D32_v5'
|
|
||||||
'Standard_D48_v5'
|
|
||||||
'Standard_D64_v5'
|
|
||||||
'Standard_D96_v5'
|
|
||||||
'Standard_F2'
|
|
||||||
'Standard_F4'
|
|
||||||
'Standard_F8'
|
|
||||||
'Standard_F16'
|
|
||||||
'Standard_F32'
|
|
||||||
'Standard_F64'
|
|
||||||
'Standard_F72'
|
|
||||||
'Standard_F2s_v2'
|
|
||||||
'Standard_F4s_v2'
|
|
||||||
'Standard_F8s_v2'
|
|
||||||
'Standard_F16s_v2'
|
|
||||||
'Standard_F32s_v2'
|
|
||||||
'Standard_F64s_v2'
|
|
||||||
'Standard_F72s_v2'
|
|
||||||
'Standard_E2_v3'
|
|
||||||
'Standard_E4_v3'
|
|
||||||
'Standard_E8_v3'
|
|
||||||
'Standard_E16_v3'
|
|
||||||
'Standard_E32_v3'
|
|
||||||
'Standard_E48_v3'
|
|
||||||
'Standard_E64_v3'
|
|
||||||
'Standard_E96_v3'
|
|
||||||
'Standard_E2_v4'
|
|
||||||
'Standard_E4_v4'
|
|
||||||
'Standard_E8_v4'
|
|
||||||
'Standard_E16_v4'
|
|
||||||
'Standard_E32_v4'
|
|
||||||
'Standard_E48_v4'
|
|
||||||
'Standard_E64_v4'
|
|
||||||
'Standard_E2_v5'
|
|
||||||
'Standard_E4_v5'
|
|
||||||
'Standard_E8_v5'
|
|
||||||
'Standard_E16_v5'
|
|
||||||
'Standard_E32_v5'
|
|
||||||
'Standard_E48_v5'
|
|
||||||
'Standard_E64_v5'
|
|
||||||
'Standard_E96_v5'
|
|
||||||
'Standard_M64'
|
|
||||||
'Standard_M128'
|
|
||||||
'Standard_M208ms_v2'
|
|
||||||
'Standard_M416ms_v2'
|
|
||||||
'Standard_L4s_v2'
|
|
||||||
'Standard_L8s_v2'
|
|
||||||
'Standard_L16s_v2'
|
|
||||||
'Standard_L32s_v2'
|
|
||||||
'Standard_L64s_v2'
|
|
||||||
'Standard_L80s_v2'
|
|
||||||
'Standard_NC6'
|
|
||||||
'Standard_NC12'
|
|
||||||
'Standard_NC24'
|
|
||||||
'Standard_NC24r'
|
|
||||||
'Standard_ND6s'
|
|
||||||
'Standard_ND12s'
|
|
||||||
'Standard_ND24s'
|
|
||||||
'Standard_ND24rs'
|
|
||||||
'Standard_NV6'
|
|
||||||
'Standard_NV12'
|
|
||||||
'Standard_NV24'
|
|
||||||
'Standard_H8'
|
|
||||||
'Standard_H16'
|
|
||||||
'Standard_H16r'
|
|
||||||
'Standard_H16mr'
|
|
||||||
'Standard_HB120rs_v2'
|
|
||||||
'Standard_HC44rs'
|
|
||||||
'Standard_DC2s'
|
|
||||||
'Standard_DC4s'
|
|
||||||
'Standard_DC2s_v2'
|
|
||||||
'Standard_DC4s_v2'
|
|
||||||
'Standard_DC8s_v2'
|
|
||||||
'Standard_DC16s_v2'
|
|
||||||
'Standard_DC32s_v2'
|
|
||||||
'Standard_A1_v2'
|
|
||||||
'Standard_A2_v2'
|
|
||||||
'Standard_A4_v2'
|
|
||||||
'Standard_A8_v2'
|
|
||||||
'Standard_A2m_v2'
|
|
||||||
'Standard_A4m_v2'
|
|
||||||
'Standard_A8m_v2'
|
|
||||||
])
|
|
||||||
param instanceType string = 'Standard_B2s' // Azure instance types.
|
|
||||||
|
|
||||||
@description('Username for the Virtual Machine.')
|
@description('Username for the Virtual Machine.')
|
||||||
param adminUsername string
|
param adminUsername string
|
||||||
|
|
@ -174,6 +65,15 @@ var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
|
||||||
//Condition for the domain name
|
//Condition for the domain name
|
||||||
var isEmptyDomain = domainName == ''
|
var isEmptyDomain = domainName == ''
|
||||||
|
|
||||||
|
// ARM64 instances are detected by checking for 'p' in the instance type name pattern.
|
||||||
|
// Azure ARM-based VMs use 'p' to indicate ARM processors (Ampere Altra, Microsoft Cobalt, etc.)
|
||||||
|
// Examples: Standard_D2ps_v5, Standard_E4pds_v5, Standard_B2pls_v2, etc.
|
||||||
|
// The pattern checks for 'p' followed by optional letters (like 'l', 'd', 's') before '_v' version suffix
|
||||||
|
var instanceTypeLower = toLower(instanceType)
|
||||||
|
var isArm64Instance = contains(instanceTypeLower, 'ps_v') || contains(instanceTypeLower, 'pls_v') || contains(instanceTypeLower, 'pds_v') || contains(instanceTypeLower, 'plds_v') || contains(instanceTypeLower, 'psv') || contains(instanceTypeLower, 'plsv') || contains(instanceTypeLower, 'pdsv') || contains(instanceTypeLower, 'pldsv')
|
||||||
|
|
||||||
|
var ubuntuSku = isArm64Instance ? 'server-arm64' : 'server'
|
||||||
|
|
||||||
//Variables for deployment
|
//Variables for deployment
|
||||||
var networkSettings = {
|
var networkSettings = {
|
||||||
privateIPaddressNetInterface: '10.0.0.5'
|
privateIPaddressNetInterface: '10.0.0.5'
|
||||||
|
|
@ -189,8 +89,8 @@ var openviduVMSettings = {
|
||||||
osDiskType: 'StandardSSD_LRS'
|
osDiskType: 'StandardSSD_LRS'
|
||||||
ubuntuOSVersion: {
|
ubuntuOSVersion: {
|
||||||
publisher: 'Canonical'
|
publisher: 'Canonical'
|
||||||
offer: '0001-com-ubuntu-server-jammy'
|
offer: 'ubuntu-24_04-lts'
|
||||||
sku: '22_04-lts-gen2'
|
sku: ubuntuSku
|
||||||
version: 'latest'
|
version: 'latest'
|
||||||
}
|
}
|
||||||
linuxConfiguration: {
|
linuxConfiguration: {
|
||||||
|
|
@ -275,9 +175,11 @@ var stringInterpolationParams = {
|
||||||
|
|
||||||
var installScriptTemplate = '''
|
var installScriptTemplate = '''
|
||||||
#!/bin/bash -x
|
#!/bin/bash -x
|
||||||
OPENVIDU_VERSION=3.4.1
|
OPENVIDU_VERSION=main
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
unzip \
|
unzip \
|
||||||
|
|
@ -850,6 +752,8 @@ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||||||
|
|
||||||
az login --identity --allow-no-subscriptions
|
az login --identity --allow-no-subscriptions
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y
|
apt-get update && apt-get install -y
|
||||||
|
|
||||||
export HOME="/root"
|
export HOME="/root"
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -209,14 +209,14 @@
|
||||||
"label": "Type of Instance",
|
"label": "Type of Instance",
|
||||||
"toolTip": "Specifies the azure vm size for your OpenVidu instance",
|
"toolTip": "Specifies the azure vm size for your OpenVidu instance",
|
||||||
"recommendedSizes": [
|
"recommendedSizes": [
|
||||||
"Standard_B2s",
|
"Standard_B4s",
|
||||||
"Standard_B4ms"
|
"Standard_B4ms",
|
||||||
|
"Standard_D4ps_v5",
|
||||||
|
"Standard_D4pls_v5"
|
||||||
],
|
],
|
||||||
"constraints": {
|
"constraints": {
|
||||||
"allowedSizes": [],
|
"allowedSizes": [],
|
||||||
"excludedSizes": [],
|
"excludedSizes": []
|
||||||
"numAvailabilityZonesRequired": 3,
|
|
||||||
"zone": "3"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"hideDiskTypeFilter": false
|
"hideDiskTypeFilter": false
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
# ------------------------- outputs.tf -------------------------
|
# ------------------------- outputs.tf -------------------------
|
||||||
|
|
||||||
output "openvidu_instance_name" {
|
output "secrets_manager" {
|
||||||
value = google_compute_instance.openvidu_server.name
|
value = "https://console.cloud.google.com/security/secret-manager?project=${var.projectId}"
|
||||||
}
|
|
||||||
|
|
||||||
output "openvidu_public_ip" {
|
|
||||||
value = length(google_compute_address.public_ip_address) > 0 ? google_compute_address.public_ip_address[0].address : google_compute_instance.openvidu_server.network_interface[0].access_config[0].nat_ip
|
|
||||||
}
|
|
||||||
|
|
||||||
output "appdata_bucket" {
|
|
||||||
value = local.isEmpty ? google_storage_bucket.bucket[0].name : var.bucketName
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,28 @@ resource "google_project_service" "cloudresourcemanager_api" { service = "cloudr
|
||||||
|
|
||||||
resource "random_id" "bucket_suffix" { byte_length = 3 }
|
resource "random_id" "bucket_suffix" { byte_length = 3 }
|
||||||
|
|
||||||
|
|
||||||
|
# Secret Manager secrets for OpenVidu deployment information
|
||||||
|
resource "google_secret_manager_secret" "openvidu_shared_info" {
|
||||||
|
for_each = toset([
|
||||||
|
"OPENVIDU_URL", "MEET_INITIAL_ADMIN_USER", "MEET_INITIAL_ADMIN_PASSWORD",
|
||||||
|
"MEET_INITIAL_API_KEY", "LIVEKIT_URL", "LIVEKIT_API_KEY", "LIVEKIT_API_SECRET",
|
||||||
|
"DASHBOARD_URL", "GRAFANA_URL", "MINIO_URL", "DOMAIN_NAME", "LIVEKIT_TURN_DOMAIN_NAME",
|
||||||
|
"REDIS_PASSWORD", "MONGO_ADMIN_USERNAME", "MONGO_ADMIN_PASSWORD", "MONGO_REPLICA_SET_KEY",
|
||||||
|
"MINIO_ACCESS_KEY", "MINIO_SECRET_KEY", "DASHBOARD_ADMIN_USERNAME", "DASHBOARD_ADMIN_PASSWORD",
|
||||||
|
"GRAFANA_ADMIN_USERNAME", "GRAFANA_ADMIN_PASSWORD", "ENABLED_MODULES"
|
||||||
|
])
|
||||||
|
|
||||||
|
secret_id = each.key
|
||||||
|
replication {
|
||||||
|
auto {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# GCS bucket
|
# GCS bucket
|
||||||
resource "google_storage_bucket" "bucket" {
|
resource "google_storage_bucket" "bucket" {
|
||||||
count = 1
|
count = local.isEmpty ? 1 : 0
|
||||||
name = local.isEmpty ? "${var.projectId}-${random_id.bucket_suffix.hex}" : var.bucketName
|
name = "${var.projectId}-${var.stackName}-${random_id.bucket_suffix.hex}"
|
||||||
location = var.region
|
location = var.region
|
||||||
force_destroy = true
|
force_destroy = true
|
||||||
uniform_bucket_level_access = true
|
uniform_bucket_level_access = true
|
||||||
|
|
@ -66,6 +84,14 @@ resource "google_compute_address" "public_ip_address" {
|
||||||
region = var.region
|
region = var.region
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Check if ARM
|
||||||
|
locals {
|
||||||
|
is_arm_instance = startswith(var.instanceType, "c4a-") || startswith(var.instanceType, "t2a-") || startswith(var.instanceType, "n4a-") || startswith(var.instanceType, "a4x-")
|
||||||
|
yq_arch = local.is_arm_instance ? "arm64" : "amd64"
|
||||||
|
|
||||||
|
ubuntu_image = local.is_arm_instance ? "ubuntu-os-cloud/ubuntu-2404-noble-arm64-v20241219" : "ubuntu-os-cloud/ubuntu-2404-noble-amd64-v20241219"
|
||||||
|
}
|
||||||
|
|
||||||
# Compute instance for OpenVidu
|
# Compute instance for OpenVidu
|
||||||
resource "google_compute_instance" "openvidu_server" {
|
resource "google_compute_instance" "openvidu_server" {
|
||||||
name = lower("${var.stackName}-vm-ce")
|
name = lower("${var.stackName}-vm-ce")
|
||||||
|
|
@ -76,7 +102,7 @@ resource "google_compute_instance" "openvidu_server" {
|
||||||
|
|
||||||
boot_disk {
|
boot_disk {
|
||||||
initialize_params {
|
initialize_params {
|
||||||
image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts"
|
image = local.ubuntu_image
|
||||||
size = 100
|
size = 100
|
||||||
type = "pd-standard"
|
type = "pd-standard"
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +117,7 @@ resource "google_compute_instance" "openvidu_server" {
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
# metadata values are accessible from the instance
|
# metadata values are accessible from the instance
|
||||||
publicIpAddress = google_compute_address.public_ip_address[0].address
|
publicIpAddress = coalesce(var.publicIpAddress, google_compute_address.public_ip_address[0].address)
|
||||||
region = var.region
|
region = var.region
|
||||||
stackName = var.stackName
|
stackName = var.stackName
|
||||||
certificateType = var.certificateType
|
certificateType = var.certificateType
|
||||||
|
|
@ -125,9 +151,11 @@ locals {
|
||||||
#!/bin/bash -x
|
#!/bin/bash -x
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
OPENVIDU_VERSION=3.4.1
|
OPENVIDU_VERSION=main
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
YQ_VERSION=v4.44.5
|
YQ_VERSION=v4.44.5
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
unzip \
|
unzip \
|
||||||
|
|
@ -138,8 +166,8 @@ apt-get update && apt-get install -y \
|
||||||
lsb-release \
|
lsb-release \
|
||||||
openssl
|
openssl
|
||||||
|
|
||||||
wget https://github.com/mikefarah/yq/releases/download/$${YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\
|
wget https://github.com/mikefarah/yq/releases/download/$${YQ_VERSION}/yq_linux_${local.yq_arch}.tar.gz -O - |\
|
||||||
tar xz && mv yq_linux_amd64 /usr/bin/yq
|
tar xz && mv yq_linux_${local.yq_arch} /usr/bin/yq
|
||||||
|
|
||||||
# Configure gcloud with instance service account
|
# Configure gcloud with instance service account
|
||||||
gcloud auth activate-service-account --key-file=/dev/null 2>/dev/null || true
|
gcloud auth activate-service-account --key-file=/dev/null 2>/dev/null || true
|
||||||
|
|
@ -149,31 +177,6 @@ get_meta() { curl -s -H "Metadata-Flavor: Google" "$${METADATA_URL}/$1"; }
|
||||||
# Create counter file for tracking script executions
|
# Create counter file for tracking script executions
|
||||||
echo 1 > /usr/local/bin/openvidu_install_counter.txt
|
echo 1 > /usr/local/bin/openvidu_install_counter.txt
|
||||||
|
|
||||||
# Create all the secrets
|
|
||||||
gcloud secrets create OPENVIDU_URL --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MEET_INITIAL_ADMIN_USER --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MEET_INITIAL_ADMIN_PASSWORD --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MEET_INITIAL_API_KEY --replication-policy=automatic || true
|
|
||||||
gcloud secrets create LIVEKIT_URL --replication-policy=automatic || true
|
|
||||||
gcloud secrets create LIVEKIT_API_KEY --replication-policy=automatic || true
|
|
||||||
gcloud secrets create LIVEKIT_API_SECRET --replication-policy=automatic || true
|
|
||||||
gcloud secrets create DASHBOARD_URL --replication-policy=automatic || true
|
|
||||||
gcloud secrets create GRAFANA_URL --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MINIO_URL --replication-policy=automatic || true
|
|
||||||
gcloud secrets create DOMAIN_NAME --replication-policy=automatic || true
|
|
||||||
gcloud secrets create LIVEKIT_TURN_DOMAIN_NAME --replication-policy=automatic || true
|
|
||||||
gcloud secrets create REDIS_PASSWORD --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MONGO_ADMIN_USERNAME --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MONGO_ADMIN_PASSWORD --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MONGO_REPLICA_SET_KEY --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MINIO_ACCESS_KEY --replication-policy=automatic || true
|
|
||||||
gcloud secrets create MINIO_SECRET_KEY --replication-policy=automatic || true
|
|
||||||
gcloud secrets create DASHBOARD_ADMIN_USERNAME --replication-policy=automatic || true
|
|
||||||
gcloud secrets create DASHBOARD_ADMIN_PASSWORD --replication-policy=automatic || true
|
|
||||||
gcloud secrets create GRAFANA_ADMIN_USERNAME --replication-policy=automatic || true
|
|
||||||
gcloud secrets create GRAFANA_ADMIN_PASSWORD --replication-policy=automatic || true
|
|
||||||
gcloud secrets create ENABLED_MODULES --replication-policy=automatic || true
|
|
||||||
|
|
||||||
# Configure domain
|
# Configure domain
|
||||||
if [[ "${var.domainName}" == "" ]]; then
|
if [[ "${var.domainName}" == "" ]]; then
|
||||||
[ ! -d "/usr/share/openvidu" ] && mkdir -p /usr/share/openvidu
|
[ ! -d "/usr/share/openvidu" ] && mkdir -p /usr/share/openvidu
|
||||||
|
|
@ -339,7 +342,7 @@ EXTERNAL_S3_SECRET_KEY=$(echo "$HMAC_OUTPUT" | jq -r '.secret')
|
||||||
EXTERNAL_S3_ENDPOINT="https://storage.googleapis.com"
|
EXTERNAL_S3_ENDPOINT="https://storage.googleapis.com"
|
||||||
EXTERNAL_S3_REGION="${var.region}"
|
EXTERNAL_S3_REGION="${var.region}"
|
||||||
EXTERNAL_S3_PATH_STYLE_ACCESS="true"
|
EXTERNAL_S3_PATH_STYLE_ACCESS="true"
|
||||||
EXTERNAL_S3_BUCKET_APP_DATA=${google_storage_bucket.bucket[0].name}
|
EXTERNAL_S3_BUCKET_APP_DATA=$(get_meta "instance/attributes/bucketName")
|
||||||
|
|
||||||
# Update egress.yaml to use hardcoded credentials instead of env variable
|
# Update egress.yaml to use hardcoded credentials instead of env variable
|
||||||
if [ -f "$${CONFIG_DIR}/egress.yaml" ]; then
|
if [ -f "$${CONFIG_DIR}/egress.yaml" ]; then
|
||||||
|
|
@ -675,7 +678,7 @@ ${local.config_s3_script}
|
||||||
CONFIG_S3_EOF
|
CONFIG_S3_EOF
|
||||||
chmod +x /usr/local/bin/config_s3.sh
|
chmod +x /usr/local/bin/config_s3.sh
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
apt-get update && apt-get install -y
|
apt-get update && apt-get install -y
|
||||||
|
|
||||||
# Install google cli
|
# Install google cli
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ variable "projectId" {
|
||||||
variable "region" {
|
variable "region" {
|
||||||
description = "GCP region where resources will be created."
|
description = "GCP region where resources will be created."
|
||||||
type = string
|
type = string
|
||||||
default = "europe-west1"
|
default = "europe-west2"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "zone" {
|
variable "zone" {
|
||||||
description = "GCP zone that some resources will use."
|
description = "GCP zone that some resources will use."
|
||||||
type = string
|
type = string
|
||||||
default = "europe-west1-b"
|
default = "europe-west2-b"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "stackName" {
|
variable "stackName" {
|
||||||
|
|
@ -88,11 +88,7 @@ variable "initialMeetApiKey" {
|
||||||
variable "instanceType" {
|
variable "instanceType" {
|
||||||
description = "Specifies the GCE machine type for your OpenVidu instance"
|
description = "Specifies the GCE machine type for your OpenVidu instance"
|
||||||
type = string
|
type = string
|
||||||
default = "e2-standard-8"
|
default = "e2-standard-2"
|
||||||
validation {
|
|
||||||
condition = can(regex("^(e2-(micro|small|medium|standard-[2-9]|standard-1[0-6]|highmem-[2-9]|highmem-1[0-6]|highcpu-[2-9]|highcpu-1[0-6])|n1-(standard-[1-9]|standard-[1-9][0-9]|highmem-[2-9]|highmem-[1-9][0-9]|highcpu-[1-9]|highcpu-[1-9][0-9])|n2-(standard-[2-9]|standard-[1-9][0-9]|standard-1[0-2][0-8]|highmem-[2-9]|highmem-[1-9][0-9]|highmem-1[0-2][0-8]|highcpu-[1-9][0-9]|highcpu-1[0-2][0-8])|n2d-(standard-[2-9]|standard-[1-9][0-9]|standard-2[0-2][0-4]|highmem-[2-9]|highmem-[1-9][0-9]|highmem-9[0-6]|highcpu-[1-9][0-9]|highcpu-2[0-2][0-4])|c2-(standard-[4-9]|standard-[1-5][0-9]|standard-60)|c2d-(standard-[2-9]|standard-[1-9][0-9]|standard-1[0-1][0-2]|highmem-[2-9]|highmem-[1-9][0-9]|highmem-1[0-1][0-2]|highcpu-[1-9][0-9]|highcpu-1[0-1][0-2])|m1-(ultramem-[4-9][0-9]|ultramem-160)|m2-(ultramem-208|ultramem-416|megamem-416)|m3-(ultramem-32|ultramem-64|ultramem-128|megamem-64|megamem-128)|a2-(standard-[1-9]|standard-[1-9][0-9]|standard-96|highmem-1g|ultramem-1g|megamem-1g)|a3-(standard-[1-9]|standard-[1-9][0-9]|standard-80|highmem-1g|megamem-1g)|g2-(standard-[4-9]|standard-[1-9][0-9]|standard-96)|t2d-(standard-[1-9]|standard-[1-9][0-9]|standard-60)|t2a-(standard-[1-9]|standard-[1-9][0-9]|standard-48)|h3-(standard-88)|f1-(micro)|t4g-(micro|small|medium|standard-[1-9]|standard-[1-9][0-9]))$", var.instanceType))
|
|
||||||
error_message = "The instance type is not valid"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "bucketName" {
|
variable "bucketName" {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Docker & Docker Compose will need to be installed on the machine
|
# Docker & Docker Compose will need to be installed on the machine
|
||||||
set -eu
|
set -eu
|
||||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.3.3}"
|
export DOCKER_VERSION="${DOCKER_VERSION:-29.0.2}"
|
||||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.39.4}"
|
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
|
||||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.4.1}"
|
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||||
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
||||||
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
|
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.9.7-debian-12-r3}"
|
||||||
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-08-13T08-35-41Z}"
|
||||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.15}"
|
||||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:8.2.2-alpine}"
|
||||||
export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}"
|
export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}"
|
||||||
export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}"
|
export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}"
|
||||||
export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}"
|
export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}"
|
||||||
|
|
@ -22,11 +22,11 @@ export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.
|
||||||
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
||||||
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
||||||
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/openvidu/egress:${OPENVIDU_VERSION}}"
|
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/openvidu/egress:${OPENVIDU_VERSION}}"
|
||||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.7.1}"
|
||||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.7}"
|
||||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.7}"
|
||||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.17.1}"
|
||||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:12.2.0}"
|
||||||
|
|
||||||
# Function to compare two version strings
|
# Function to compare two version strings
|
||||||
compare_versions() {
|
compare_versions() {
|
||||||
|
|
@ -158,11 +158,13 @@ if [ "$DOCKER_COMPOSE_NEEDED" = true ]; then
|
||||||
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
|
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Restart Docker and wait for it to start
|
# Check if docker is running with docker info
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "Docker is not running. Starting Docker..."
|
||||||
systemctl enable docker
|
systemctl enable docker
|
||||||
systemctl stop docker
|
|
||||||
systemctl start docker
|
systemctl start docker
|
||||||
wait_for_docker
|
wait_for_docker
|
||||||
|
fi
|
||||||
|
|
||||||
# Create random temp directory
|
# Create random temp directory
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Docker & Docker Compose will need to be installed on the machine
|
# Docker & Docker Compose will need to be installed on the machine
|
||||||
set -eu
|
set -eu
|
||||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.3.3}"
|
export DOCKER_VERSION="${DOCKER_VERSION:-29.0.2}"
|
||||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.39.4}"
|
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
|
||||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.4.1}"
|
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||||
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
||||||
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
|
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.9.7-debian-12-r3}"
|
||||||
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-08-13T08-35-41Z}"
|
||||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.15}"
|
||||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:8.2.2-alpine}"
|
||||||
export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}"
|
export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}"
|
||||||
export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}"
|
export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}"
|
||||||
export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}"
|
export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}"
|
||||||
|
|
@ -22,11 +22,11 @@ export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.
|
||||||
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
||||||
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
||||||
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/openvidu/egress:${OPENVIDU_VERSION}}"
|
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/openvidu/egress:${OPENVIDU_VERSION}}"
|
||||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.7.1}"
|
||||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.7}"
|
||||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.7}"
|
||||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.17.1}"
|
||||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:12.2.0}"
|
||||||
|
|
||||||
# Function to compare two version strings
|
# Function to compare two version strings
|
||||||
compare_versions() {
|
compare_versions() {
|
||||||
|
|
@ -158,11 +158,13 @@ if [ "$DOCKER_COMPOSE_NEEDED" = true ]; then
|
||||||
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
|
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Restart Docker and wait for it to start
|
# Check if docker is running with docker info
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "Docker is not running. Starting Docker..."
|
||||||
systemctl enable docker
|
systemctl enable docker
|
||||||
systemctl stop docker
|
|
||||||
systemctl start docker
|
systemctl start docker
|
||||||
wait_for_docker
|
wait_for_docker
|
||||||
|
fi
|
||||||
|
|
||||||
# Create random temp directory
|
# Create random temp directory
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
|
|
|
||||||
|
|
@ -95,42 +95,27 @@ Parameters:
|
||||||
Type: String
|
Type: String
|
||||||
Default: c6a.xlarge
|
Default: c6a.xlarge
|
||||||
AllowedValues:
|
AllowedValues:
|
||||||
- t2.large
|
- t3.nano
|
||||||
- t2.xlarge
|
- t3.micro
|
||||||
- t2.2xlarge
|
- t3.small
|
||||||
- t3.medium
|
- t3.medium
|
||||||
- t3.large
|
- t3.large
|
||||||
- t3.xlarge
|
- t3.xlarge
|
||||||
- t3.2xlarge
|
- t3.2xlarge
|
||||||
- m4.large
|
- t3a.nano
|
||||||
- m4.xlarge
|
- t3a.micro
|
||||||
- m4.2xlarge
|
- t3a.small
|
||||||
- m4.4xlarge
|
- t3a.medium
|
||||||
- m4.10xlarge
|
- t3a.large
|
||||||
- m4.16xlarge
|
- t3a.xlarge
|
||||||
- m5.large
|
- t3a.2xlarge
|
||||||
- m5.xlarge
|
- t4g.nano
|
||||||
- m5.2xlarge
|
- t4g.micro
|
||||||
- m5.4xlarge
|
- t4g.small
|
||||||
- m5.8xlarge
|
- t4g.medium
|
||||||
- m5.12xlarge
|
- t4g.large
|
||||||
- m5.16xlarge
|
- t4g.xlarge
|
||||||
- m5.24xlarge
|
- t4g.2xlarge
|
||||||
- m6i.large
|
|
||||||
- m6i.xlarge
|
|
||||||
- m6i.2xlarge
|
|
||||||
- m6i.4xlarge
|
|
||||||
- m6i.8xlarge
|
|
||||||
- m6i.12xlarge
|
|
||||||
- m6i.16xlarge
|
|
||||||
- m6i.24xlarge
|
|
||||||
- m6i.32xlarge
|
|
||||||
- m6i.metal
|
|
||||||
- c4.large
|
|
||||||
- c4.xlarge
|
|
||||||
- c4.2xlarge
|
|
||||||
- c4.4xlarge
|
|
||||||
- c4.8xlarge
|
|
||||||
- c5.large
|
- c5.large
|
||||||
- c5.xlarge
|
- c5.xlarge
|
||||||
- c5.2xlarge
|
- c5.2xlarge
|
||||||
|
|
@ -139,6 +124,39 @@ Parameters:
|
||||||
- c5.12xlarge
|
- c5.12xlarge
|
||||||
- c5.18xlarge
|
- c5.18xlarge
|
||||||
- c5.24xlarge
|
- c5.24xlarge
|
||||||
|
- c5.metal
|
||||||
|
- c5a.large
|
||||||
|
- c5a.xlarge
|
||||||
|
- c5a.2xlarge
|
||||||
|
- c5a.4xlarge
|
||||||
|
- c5a.8xlarge
|
||||||
|
- c5a.12xlarge
|
||||||
|
- c5a.16xlarge
|
||||||
|
- c5a.24xlarge
|
||||||
|
- c5ad.large
|
||||||
|
- c5ad.xlarge
|
||||||
|
- c5ad.2xlarge
|
||||||
|
- c5ad.4xlarge
|
||||||
|
- c5ad.8xlarge
|
||||||
|
- c5ad.12xlarge
|
||||||
|
- c5ad.16xlarge
|
||||||
|
- c5ad.24xlarge
|
||||||
|
- c5d.large
|
||||||
|
- c5d.xlarge
|
||||||
|
- c5d.2xlarge
|
||||||
|
- c5d.4xlarge
|
||||||
|
- c5d.9xlarge
|
||||||
|
- c5d.12xlarge
|
||||||
|
- c5d.18xlarge
|
||||||
|
- c5d.24xlarge
|
||||||
|
- c5d.metal
|
||||||
|
- c5n.large
|
||||||
|
- c5n.xlarge
|
||||||
|
- c5n.2xlarge
|
||||||
|
- c5n.4xlarge
|
||||||
|
- c5n.9xlarge
|
||||||
|
- c5n.18xlarge
|
||||||
|
- c5n.metal
|
||||||
- c6a.large
|
- c6a.large
|
||||||
- c6a.xlarge
|
- c6a.xlarge
|
||||||
- c6a.2xlarge
|
- c6a.2xlarge
|
||||||
|
|
@ -150,6 +168,32 @@ Parameters:
|
||||||
- c6a.32xlarge
|
- c6a.32xlarge
|
||||||
- c6a.48xlarge
|
- c6a.48xlarge
|
||||||
- c6a.metal
|
- c6a.metal
|
||||||
|
- c6g.medium
|
||||||
|
- c6g.large
|
||||||
|
- c6g.xlarge
|
||||||
|
- c6g.2xlarge
|
||||||
|
- c6g.4xlarge
|
||||||
|
- c6g.8xlarge
|
||||||
|
- c6g.12xlarge
|
||||||
|
- c6g.16xlarge
|
||||||
|
- c6g.metal
|
||||||
|
- c6gd.medium
|
||||||
|
- c6gd.large
|
||||||
|
- c6gd.xlarge
|
||||||
|
- c6gd.2xlarge
|
||||||
|
- c6gd.4xlarge
|
||||||
|
- c6gd.8xlarge
|
||||||
|
- c6gd.12xlarge
|
||||||
|
- c6gd.16xlarge
|
||||||
|
- c6gd.metal
|
||||||
|
- c6gn.medium
|
||||||
|
- c6gn.large
|
||||||
|
- c6gn.xlarge
|
||||||
|
- c6gn.2xlarge
|
||||||
|
- c6gn.4xlarge
|
||||||
|
- c6gn.8xlarge
|
||||||
|
- c6gn.12xlarge
|
||||||
|
- c6gn.16xlarge
|
||||||
- c6i.large
|
- c6i.large
|
||||||
- c6i.xlarge
|
- c6i.xlarge
|
||||||
- c6i.2xlarge
|
- c6i.2xlarge
|
||||||
|
|
@ -160,6 +204,26 @@ Parameters:
|
||||||
- c6i.24xlarge
|
- c6i.24xlarge
|
||||||
- c6i.32xlarge
|
- c6i.32xlarge
|
||||||
- c6i.metal
|
- c6i.metal
|
||||||
|
- c6id.large
|
||||||
|
- c6id.xlarge
|
||||||
|
- c6id.2xlarge
|
||||||
|
- c6id.4xlarge
|
||||||
|
- c6id.8xlarge
|
||||||
|
- c6id.12xlarge
|
||||||
|
- c6id.16xlarge
|
||||||
|
- c6id.24xlarge
|
||||||
|
- c6id.32xlarge
|
||||||
|
- c6id.metal
|
||||||
|
- c6in.large
|
||||||
|
- c6in.xlarge
|
||||||
|
- c6in.2xlarge
|
||||||
|
- c6in.4xlarge
|
||||||
|
- c6in.8xlarge
|
||||||
|
- c6in.12xlarge
|
||||||
|
- c6in.16xlarge
|
||||||
|
- c6in.24xlarge
|
||||||
|
- c6in.32xlarge
|
||||||
|
- c6in.metal
|
||||||
- c7a.medium
|
- c7a.medium
|
||||||
- c7a.large
|
- c7a.large
|
||||||
- c7a.xlarge
|
- c7a.xlarge
|
||||||
|
|
@ -172,6 +236,40 @@ Parameters:
|
||||||
- c7a.32xlarge
|
- c7a.32xlarge
|
||||||
- c7a.48xlarge
|
- c7a.48xlarge
|
||||||
- c7a.metal-48xl
|
- c7a.metal-48xl
|
||||||
|
- c7g.medium
|
||||||
|
- c7g.large
|
||||||
|
- c7g.xlarge
|
||||||
|
- c7g.2xlarge
|
||||||
|
- c7g.4xlarge
|
||||||
|
- c7g.8xlarge
|
||||||
|
- c7g.12xlarge
|
||||||
|
- c7g.16xlarge
|
||||||
|
- c7g.metal
|
||||||
|
- c7gd.medium
|
||||||
|
- c7gd.large
|
||||||
|
- c7gd.xlarge
|
||||||
|
- c7gd.2xlarge
|
||||||
|
- c7gd.4xlarge
|
||||||
|
- c7gd.8xlarge
|
||||||
|
- c7gd.12xlarge
|
||||||
|
- c7gd.16xlarge
|
||||||
|
- c7gd.metal
|
||||||
|
- c7gn.medium
|
||||||
|
- c7gn.large
|
||||||
|
- c7gn.xlarge
|
||||||
|
- c7gn.2xlarge
|
||||||
|
- c7gn.4xlarge
|
||||||
|
- c7gn.8xlarge
|
||||||
|
- c7gn.12xlarge
|
||||||
|
- c7gn.16xlarge
|
||||||
|
- c7gn.metal
|
||||||
|
- c7i-flex.large
|
||||||
|
- c7i-flex.xlarge
|
||||||
|
- c7i-flex.2xlarge
|
||||||
|
- c7i-flex.4xlarge
|
||||||
|
- c7i-flex.8xlarge
|
||||||
|
- c7i-flex.12xlarge
|
||||||
|
- c7i-flex.16xlarge
|
||||||
- c7i.large
|
- c7i.large
|
||||||
- c7i.xlarge
|
- c7i.xlarge
|
||||||
- c7i.2xlarge
|
- c7i.2xlarge
|
||||||
|
|
@ -183,20 +281,77 @@ Parameters:
|
||||||
- c7i.48xlarge
|
- c7i.48xlarge
|
||||||
- c7i.metal-24xl
|
- c7i.metal-24xl
|
||||||
- c7i.metal-48xl
|
- c7i.metal-48xl
|
||||||
- c5n.large
|
- c8g.medium
|
||||||
- c5n.xlarge
|
- c8g.large
|
||||||
- c5n.2xlarge
|
- c8g.xlarge
|
||||||
- c5n.4xlarge
|
- c8g.2xlarge
|
||||||
- c5n.9xlarge
|
- c8g.4xlarge
|
||||||
- c5n.18xlarge
|
- c8g.8xlarge
|
||||||
- m5n.large
|
- c8g.12xlarge
|
||||||
- m5n.xlarge
|
- c8g.16xlarge
|
||||||
- m5n.2xlarge
|
- c8g.24xlarge
|
||||||
- m5n.4xlarge
|
- c8g.48xlarge
|
||||||
- m5n.8xlarge
|
- c8g.metal-24xl
|
||||||
- m5n.12xlarge
|
- c8g.metal-48xl
|
||||||
- m5n.16xlarge
|
- m6a.large
|
||||||
- m5n.24xlarge
|
- m6a.xlarge
|
||||||
|
- m6a.2xlarge
|
||||||
|
- m6a.4xlarge
|
||||||
|
- m6a.8xlarge
|
||||||
|
- m6a.12xlarge
|
||||||
|
- m6a.16xlarge
|
||||||
|
- m6a.24xlarge
|
||||||
|
- m6a.32xlarge
|
||||||
|
- m6a.48xlarge
|
||||||
|
- m6a.metal
|
||||||
|
- m6g.medium
|
||||||
|
- m6g.large
|
||||||
|
- m6g.xlarge
|
||||||
|
- m6g.2xlarge
|
||||||
|
- m6g.4xlarge
|
||||||
|
- m6g.8xlarge
|
||||||
|
- m6g.12xlarge
|
||||||
|
- m6g.16xlarge
|
||||||
|
- m6g.metal
|
||||||
|
- m6gd.medium
|
||||||
|
- m6gd.large
|
||||||
|
- m6gd.xlarge
|
||||||
|
- m6gd.2xlarge
|
||||||
|
- m6gd.4xlarge
|
||||||
|
- m6gd.8xlarge
|
||||||
|
- m6gd.12xlarge
|
||||||
|
- m6gd.16xlarge
|
||||||
|
- m6gd.metal
|
||||||
|
- m6i.large
|
||||||
|
- m6i.xlarge
|
||||||
|
- m6i.2xlarge
|
||||||
|
- m6i.4xlarge
|
||||||
|
- m6i.8xlarge
|
||||||
|
- m6i.12xlarge
|
||||||
|
- m6i.16xlarge
|
||||||
|
- m6i.24xlarge
|
||||||
|
- m6i.32xlarge
|
||||||
|
- m6i.metal
|
||||||
|
- m6id.large
|
||||||
|
- m6id.xlarge
|
||||||
|
- m6id.2xlarge
|
||||||
|
- m6id.4xlarge
|
||||||
|
- m6id.8xlarge
|
||||||
|
- m6id.12xlarge
|
||||||
|
- m6id.16xlarge
|
||||||
|
- m6id.24xlarge
|
||||||
|
- m6id.32xlarge
|
||||||
|
- m6id.metal
|
||||||
|
- m6idn.large
|
||||||
|
- m6idn.xlarge
|
||||||
|
- m6idn.2xlarge
|
||||||
|
- m6idn.4xlarge
|
||||||
|
- m6idn.8xlarge
|
||||||
|
- m6idn.12xlarge
|
||||||
|
- m6idn.16xlarge
|
||||||
|
- m6idn.24xlarge
|
||||||
|
- m6idn.32xlarge
|
||||||
|
- m6idn.metal
|
||||||
- m6in.large
|
- m6in.large
|
||||||
- m6in.xlarge
|
- m6in.xlarge
|
||||||
- m6in.2xlarge
|
- m6in.2xlarge
|
||||||
|
|
@ -206,14 +361,67 @@ Parameters:
|
||||||
- m6in.16xlarge
|
- m6in.16xlarge
|
||||||
- m6in.24xlarge
|
- m6in.24xlarge
|
||||||
- m6in.32xlarge
|
- m6in.32xlarge
|
||||||
- r5n.large
|
- m6in.metal
|
||||||
- r5n.xlarge
|
- m7a.medium
|
||||||
- r5n.2xlarge
|
- m7a.large
|
||||||
- r5n.4xlarge
|
- m7a.xlarge
|
||||||
- r5n.8xlarge
|
- m7a.2xlarge
|
||||||
- r5n.12xlarge
|
- m7a.4xlarge
|
||||||
- r5n.16xlarge
|
- m7a.8xlarge
|
||||||
- r5n.24xlarge
|
- m7a.12xlarge
|
||||||
|
- m7a.16xlarge
|
||||||
|
- m7a.24xlarge
|
||||||
|
- m7a.32xlarge
|
||||||
|
- m7a.48xlarge
|
||||||
|
- m7a.metal-48xl
|
||||||
|
- m7g.medium
|
||||||
|
- m7g.large
|
||||||
|
- m7g.xlarge
|
||||||
|
- m7g.2xlarge
|
||||||
|
- m7g.4xlarge
|
||||||
|
- m7g.8xlarge
|
||||||
|
- m7g.12xlarge
|
||||||
|
- m7g.16xlarge
|
||||||
|
- m7g.metal
|
||||||
|
- m7gd.medium
|
||||||
|
- m7gd.large
|
||||||
|
- m7gd.xlarge
|
||||||
|
- m7gd.2xlarge
|
||||||
|
- m7gd.4xlarge
|
||||||
|
- m7gd.8xlarge
|
||||||
|
- m7gd.12xlarge
|
||||||
|
- m7gd.16xlarge
|
||||||
|
- m7gd.metal
|
||||||
|
- m7i-flex.large
|
||||||
|
- m7i-flex.xlarge
|
||||||
|
- m7i-flex.2xlarge
|
||||||
|
- m7i-flex.4xlarge
|
||||||
|
- m7i-flex.8xlarge
|
||||||
|
- m7i-flex.12xlarge
|
||||||
|
- m7i-flex.16xlarge
|
||||||
|
- m7i.large
|
||||||
|
- m7i.xlarge
|
||||||
|
- m7i.2xlarge
|
||||||
|
- m7i.4xlarge
|
||||||
|
- m7i.8xlarge
|
||||||
|
- m7i.12xlarge
|
||||||
|
- m7i.16xlarge
|
||||||
|
- m7i.24xlarge
|
||||||
|
- m7i.48xlarge
|
||||||
|
- m7i.metal-24xl
|
||||||
|
- m7i.metal-48xl
|
||||||
|
- m8g.medium
|
||||||
|
- m8g.large
|
||||||
|
- m8g.xlarge
|
||||||
|
- m8g.2xlarge
|
||||||
|
- m8g.4xlarge
|
||||||
|
- m8g.8xlarge
|
||||||
|
- m8g.12xlarge
|
||||||
|
- m8g.16xlarge
|
||||||
|
- m8g.24xlarge
|
||||||
|
- m8g.48xlarge
|
||||||
|
- m8g.metal-24xl
|
||||||
|
- m8g.metal-48xl
|
||||||
ConstraintDescription: "Must be a valid EC2 instance type"
|
ConstraintDescription: "Must be a valid EC2 instance type"
|
||||||
|
|
||||||
MediaNodeInstanceType:
|
MediaNodeInstanceType:
|
||||||
|
|
@ -221,42 +429,27 @@ Parameters:
|
||||||
Type: String
|
Type: String
|
||||||
Default: c6a.xlarge
|
Default: c6a.xlarge
|
||||||
AllowedValues:
|
AllowedValues:
|
||||||
- t2.large
|
- t3.nano
|
||||||
- t2.xlarge
|
- t3.micro
|
||||||
- t2.2xlarge
|
- t3.small
|
||||||
- t3.medium
|
- t3.medium
|
||||||
- t3.large
|
- t3.large
|
||||||
- t3.xlarge
|
- t3.xlarge
|
||||||
- t3.2xlarge
|
- t3.2xlarge
|
||||||
- m4.large
|
- t3a.nano
|
||||||
- m4.xlarge
|
- t3a.micro
|
||||||
- m4.2xlarge
|
- t3a.small
|
||||||
- m4.4xlarge
|
- t3a.medium
|
||||||
- m4.10xlarge
|
- t3a.large
|
||||||
- m4.16xlarge
|
- t3a.xlarge
|
||||||
- m5.large
|
- t3a.2xlarge
|
||||||
- m5.xlarge
|
- t4g.nano
|
||||||
- m5.2xlarge
|
- t4g.micro
|
||||||
- m5.4xlarge
|
- t4g.small
|
||||||
- m5.8xlarge
|
- t4g.medium
|
||||||
- m5.12xlarge
|
- t4g.large
|
||||||
- m5.16xlarge
|
- t4g.xlarge
|
||||||
- m5.24xlarge
|
- t4g.2xlarge
|
||||||
- m6i.large
|
|
||||||
- m6i.xlarge
|
|
||||||
- m6i.2xlarge
|
|
||||||
- m6i.4xlarge
|
|
||||||
- m6i.8xlarge
|
|
||||||
- m6i.12xlarge
|
|
||||||
- m6i.16xlarge
|
|
||||||
- m6i.24xlarge
|
|
||||||
- m6i.32xlarge
|
|
||||||
- m6i.metal
|
|
||||||
- c4.large
|
|
||||||
- c4.xlarge
|
|
||||||
- c4.2xlarge
|
|
||||||
- c4.4xlarge
|
|
||||||
- c4.8xlarge
|
|
||||||
- c5.large
|
- c5.large
|
||||||
- c5.xlarge
|
- c5.xlarge
|
||||||
- c5.2xlarge
|
- c5.2xlarge
|
||||||
|
|
@ -265,6 +458,39 @@ Parameters:
|
||||||
- c5.12xlarge
|
- c5.12xlarge
|
||||||
- c5.18xlarge
|
- c5.18xlarge
|
||||||
- c5.24xlarge
|
- c5.24xlarge
|
||||||
|
- c5.metal
|
||||||
|
- c5a.large
|
||||||
|
- c5a.xlarge
|
||||||
|
- c5a.2xlarge
|
||||||
|
- c5a.4xlarge
|
||||||
|
- c5a.8xlarge
|
||||||
|
- c5a.12xlarge
|
||||||
|
- c5a.16xlarge
|
||||||
|
- c5a.24xlarge
|
||||||
|
- c5ad.large
|
||||||
|
- c5ad.xlarge
|
||||||
|
- c5ad.2xlarge
|
||||||
|
- c5ad.4xlarge
|
||||||
|
- c5ad.8xlarge
|
||||||
|
- c5ad.12xlarge
|
||||||
|
- c5ad.16xlarge
|
||||||
|
- c5ad.24xlarge
|
||||||
|
- c5d.large
|
||||||
|
- c5d.xlarge
|
||||||
|
- c5d.2xlarge
|
||||||
|
- c5d.4xlarge
|
||||||
|
- c5d.9xlarge
|
||||||
|
- c5d.12xlarge
|
||||||
|
- c5d.18xlarge
|
||||||
|
- c5d.24xlarge
|
||||||
|
- c5d.metal
|
||||||
|
- c5n.large
|
||||||
|
- c5n.xlarge
|
||||||
|
- c5n.2xlarge
|
||||||
|
- c5n.4xlarge
|
||||||
|
- c5n.9xlarge
|
||||||
|
- c5n.18xlarge
|
||||||
|
- c5n.metal
|
||||||
- c6a.large
|
- c6a.large
|
||||||
- c6a.xlarge
|
- c6a.xlarge
|
||||||
- c6a.2xlarge
|
- c6a.2xlarge
|
||||||
|
|
@ -276,6 +502,32 @@ Parameters:
|
||||||
- c6a.32xlarge
|
- c6a.32xlarge
|
||||||
- c6a.48xlarge
|
- c6a.48xlarge
|
||||||
- c6a.metal
|
- c6a.metal
|
||||||
|
- c6g.medium
|
||||||
|
- c6g.large
|
||||||
|
- c6g.xlarge
|
||||||
|
- c6g.2xlarge
|
||||||
|
- c6g.4xlarge
|
||||||
|
- c6g.8xlarge
|
||||||
|
- c6g.12xlarge
|
||||||
|
- c6g.16xlarge
|
||||||
|
- c6g.metal
|
||||||
|
- c6gd.medium
|
||||||
|
- c6gd.large
|
||||||
|
- c6gd.xlarge
|
||||||
|
- c6gd.2xlarge
|
||||||
|
- c6gd.4xlarge
|
||||||
|
- c6gd.8xlarge
|
||||||
|
- c6gd.12xlarge
|
||||||
|
- c6gd.16xlarge
|
||||||
|
- c6gd.metal
|
||||||
|
- c6gn.medium
|
||||||
|
- c6gn.large
|
||||||
|
- c6gn.xlarge
|
||||||
|
- c6gn.2xlarge
|
||||||
|
- c6gn.4xlarge
|
||||||
|
- c6gn.8xlarge
|
||||||
|
- c6gn.12xlarge
|
||||||
|
- c6gn.16xlarge
|
||||||
- c6i.large
|
- c6i.large
|
||||||
- c6i.xlarge
|
- c6i.xlarge
|
||||||
- c6i.2xlarge
|
- c6i.2xlarge
|
||||||
|
|
@ -286,6 +538,26 @@ Parameters:
|
||||||
- c6i.24xlarge
|
- c6i.24xlarge
|
||||||
- c6i.32xlarge
|
- c6i.32xlarge
|
||||||
- c6i.metal
|
- c6i.metal
|
||||||
|
- c6id.large
|
||||||
|
- c6id.xlarge
|
||||||
|
- c6id.2xlarge
|
||||||
|
- c6id.4xlarge
|
||||||
|
- c6id.8xlarge
|
||||||
|
- c6id.12xlarge
|
||||||
|
- c6id.16xlarge
|
||||||
|
- c6id.24xlarge
|
||||||
|
- c6id.32xlarge
|
||||||
|
- c6id.metal
|
||||||
|
- c6in.large
|
||||||
|
- c6in.xlarge
|
||||||
|
- c6in.2xlarge
|
||||||
|
- c6in.4xlarge
|
||||||
|
- c6in.8xlarge
|
||||||
|
- c6in.12xlarge
|
||||||
|
- c6in.16xlarge
|
||||||
|
- c6in.24xlarge
|
||||||
|
- c6in.32xlarge
|
||||||
|
- c6in.metal
|
||||||
- c7a.medium
|
- c7a.medium
|
||||||
- c7a.large
|
- c7a.large
|
||||||
- c7a.xlarge
|
- c7a.xlarge
|
||||||
|
|
@ -298,6 +570,40 @@ Parameters:
|
||||||
- c7a.32xlarge
|
- c7a.32xlarge
|
||||||
- c7a.48xlarge
|
- c7a.48xlarge
|
||||||
- c7a.metal-48xl
|
- c7a.metal-48xl
|
||||||
|
- c7g.medium
|
||||||
|
- c7g.large
|
||||||
|
- c7g.xlarge
|
||||||
|
- c7g.2xlarge
|
||||||
|
- c7g.4xlarge
|
||||||
|
- c7g.8xlarge
|
||||||
|
- c7g.12xlarge
|
||||||
|
- c7g.16xlarge
|
||||||
|
- c7g.metal
|
||||||
|
- c7gd.medium
|
||||||
|
- c7gd.large
|
||||||
|
- c7gd.xlarge
|
||||||
|
- c7gd.2xlarge
|
||||||
|
- c7gd.4xlarge
|
||||||
|
- c7gd.8xlarge
|
||||||
|
- c7gd.12xlarge
|
||||||
|
- c7gd.16xlarge
|
||||||
|
- c7gd.metal
|
||||||
|
- c7gn.medium
|
||||||
|
- c7gn.large
|
||||||
|
- c7gn.xlarge
|
||||||
|
- c7gn.2xlarge
|
||||||
|
- c7gn.4xlarge
|
||||||
|
- c7gn.8xlarge
|
||||||
|
- c7gn.12xlarge
|
||||||
|
- c7gn.16xlarge
|
||||||
|
- c7gn.metal
|
||||||
|
- c7i-flex.large
|
||||||
|
- c7i-flex.xlarge
|
||||||
|
- c7i-flex.2xlarge
|
||||||
|
- c7i-flex.4xlarge
|
||||||
|
- c7i-flex.8xlarge
|
||||||
|
- c7i-flex.12xlarge
|
||||||
|
- c7i-flex.16xlarge
|
||||||
- c7i.large
|
- c7i.large
|
||||||
- c7i.xlarge
|
- c7i.xlarge
|
||||||
- c7i.2xlarge
|
- c7i.2xlarge
|
||||||
|
|
@ -309,20 +615,77 @@ Parameters:
|
||||||
- c7i.48xlarge
|
- c7i.48xlarge
|
||||||
- c7i.metal-24xl
|
- c7i.metal-24xl
|
||||||
- c7i.metal-48xl
|
- c7i.metal-48xl
|
||||||
- c5n.large
|
- c8g.medium
|
||||||
- c5n.xlarge
|
- c8g.large
|
||||||
- c5n.2xlarge
|
- c8g.xlarge
|
||||||
- c5n.4xlarge
|
- c8g.2xlarge
|
||||||
- c5n.9xlarge
|
- c8g.4xlarge
|
||||||
- c5n.18xlarge
|
- c8g.8xlarge
|
||||||
- m5n.large
|
- c8g.12xlarge
|
||||||
- m5n.xlarge
|
- c8g.16xlarge
|
||||||
- m5n.2xlarge
|
- c8g.24xlarge
|
||||||
- m5n.4xlarge
|
- c8g.48xlarge
|
||||||
- m5n.8xlarge
|
- c8g.metal-24xl
|
||||||
- m5n.12xlarge
|
- c8g.metal-48xl
|
||||||
- m5n.16xlarge
|
- m6a.large
|
||||||
- m5n.24xlarge
|
- m6a.xlarge
|
||||||
|
- m6a.2xlarge
|
||||||
|
- m6a.4xlarge
|
||||||
|
- m6a.8xlarge
|
||||||
|
- m6a.12xlarge
|
||||||
|
- m6a.16xlarge
|
||||||
|
- m6a.24xlarge
|
||||||
|
- m6a.32xlarge
|
||||||
|
- m6a.48xlarge
|
||||||
|
- m6a.metal
|
||||||
|
- m6g.medium
|
||||||
|
- m6g.large
|
||||||
|
- m6g.xlarge
|
||||||
|
- m6g.2xlarge
|
||||||
|
- m6g.4xlarge
|
||||||
|
- m6g.8xlarge
|
||||||
|
- m6g.12xlarge
|
||||||
|
- m6g.16xlarge
|
||||||
|
- m6g.metal
|
||||||
|
- m6gd.medium
|
||||||
|
- m6gd.large
|
||||||
|
- m6gd.xlarge
|
||||||
|
- m6gd.2xlarge
|
||||||
|
- m6gd.4xlarge
|
||||||
|
- m6gd.8xlarge
|
||||||
|
- m6gd.12xlarge
|
||||||
|
- m6gd.16xlarge
|
||||||
|
- m6gd.metal
|
||||||
|
- m6i.large
|
||||||
|
- m6i.xlarge
|
||||||
|
- m6i.2xlarge
|
||||||
|
- m6i.4xlarge
|
||||||
|
- m6i.8xlarge
|
||||||
|
- m6i.12xlarge
|
||||||
|
- m6i.16xlarge
|
||||||
|
- m6i.24xlarge
|
||||||
|
- m6i.32xlarge
|
||||||
|
- m6i.metal
|
||||||
|
- m6id.large
|
||||||
|
- m6id.xlarge
|
||||||
|
- m6id.2xlarge
|
||||||
|
- m6id.4xlarge
|
||||||
|
- m6id.8xlarge
|
||||||
|
- m6id.12xlarge
|
||||||
|
- m6id.16xlarge
|
||||||
|
- m6id.24xlarge
|
||||||
|
- m6id.32xlarge
|
||||||
|
- m6id.metal
|
||||||
|
- m6idn.large
|
||||||
|
- m6idn.xlarge
|
||||||
|
- m6idn.2xlarge
|
||||||
|
- m6idn.4xlarge
|
||||||
|
- m6idn.8xlarge
|
||||||
|
- m6idn.12xlarge
|
||||||
|
- m6idn.16xlarge
|
||||||
|
- m6idn.24xlarge
|
||||||
|
- m6idn.32xlarge
|
||||||
|
- m6idn.metal
|
||||||
- m6in.large
|
- m6in.large
|
||||||
- m6in.xlarge
|
- m6in.xlarge
|
||||||
- m6in.2xlarge
|
- m6in.2xlarge
|
||||||
|
|
@ -332,14 +695,67 @@ Parameters:
|
||||||
- m6in.16xlarge
|
- m6in.16xlarge
|
||||||
- m6in.24xlarge
|
- m6in.24xlarge
|
||||||
- m6in.32xlarge
|
- m6in.32xlarge
|
||||||
- r5n.large
|
- m6in.metal
|
||||||
- r5n.xlarge
|
- m7a.medium
|
||||||
- r5n.2xlarge
|
- m7a.large
|
||||||
- r5n.4xlarge
|
- m7a.xlarge
|
||||||
- r5n.8xlarge
|
- m7a.2xlarge
|
||||||
- r5n.12xlarge
|
- m7a.4xlarge
|
||||||
- r5n.16xlarge
|
- m7a.8xlarge
|
||||||
- r5n.24xlarge
|
- m7a.12xlarge
|
||||||
|
- m7a.16xlarge
|
||||||
|
- m7a.24xlarge
|
||||||
|
- m7a.32xlarge
|
||||||
|
- m7a.48xlarge
|
||||||
|
- m7a.metal-48xl
|
||||||
|
- m7g.medium
|
||||||
|
- m7g.large
|
||||||
|
- m7g.xlarge
|
||||||
|
- m7g.2xlarge
|
||||||
|
- m7g.4xlarge
|
||||||
|
- m7g.8xlarge
|
||||||
|
- m7g.12xlarge
|
||||||
|
- m7g.16xlarge
|
||||||
|
- m7g.metal
|
||||||
|
- m7gd.medium
|
||||||
|
- m7gd.large
|
||||||
|
- m7gd.xlarge
|
||||||
|
- m7gd.2xlarge
|
||||||
|
- m7gd.4xlarge
|
||||||
|
- m7gd.8xlarge
|
||||||
|
- m7gd.12xlarge
|
||||||
|
- m7gd.16xlarge
|
||||||
|
- m7gd.metal
|
||||||
|
- m7i-flex.large
|
||||||
|
- m7i-flex.xlarge
|
||||||
|
- m7i-flex.2xlarge
|
||||||
|
- m7i-flex.4xlarge
|
||||||
|
- m7i-flex.8xlarge
|
||||||
|
- m7i-flex.12xlarge
|
||||||
|
- m7i-flex.16xlarge
|
||||||
|
- m7i.large
|
||||||
|
- m7i.xlarge
|
||||||
|
- m7i.2xlarge
|
||||||
|
- m7i.4xlarge
|
||||||
|
- m7i.8xlarge
|
||||||
|
- m7i.12xlarge
|
||||||
|
- m7i.16xlarge
|
||||||
|
- m7i.24xlarge
|
||||||
|
- m7i.48xlarge
|
||||||
|
- m7i.metal-24xl
|
||||||
|
- m7i.metal-48xl
|
||||||
|
- m8g.medium
|
||||||
|
- m8g.large
|
||||||
|
- m8g.xlarge
|
||||||
|
- m8g.2xlarge
|
||||||
|
- m8g.4xlarge
|
||||||
|
- m8g.8xlarge
|
||||||
|
- m8g.12xlarge
|
||||||
|
- m8g.16xlarge
|
||||||
|
- m8g.24xlarge
|
||||||
|
- m8g.48xlarge
|
||||||
|
- m8g.metal-24xl
|
||||||
|
- m8g.metal-48xl
|
||||||
ConstraintDescription: "Must be a valid EC2 instance type"
|
ConstraintDescription: "Must be a valid EC2 instance type"
|
||||||
|
|
||||||
KeyName:
|
KeyName:
|
||||||
|
|
@ -348,10 +764,11 @@ Parameters:
|
||||||
AllowedPattern: ^.+$
|
AllowedPattern: ^.+$
|
||||||
ConstraintDescription: must be the name of an existing EC2 KeyPair.
|
ConstraintDescription: must be the name of an existing EC2 KeyPair.
|
||||||
|
|
||||||
AmiId:
|
OperatingSystem:
|
||||||
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
|
Description: OpenVidu EC2 operating system
|
||||||
Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id
|
Type: String
|
||||||
Description: AMI ID for the EC2 instances
|
Default: "Ubuntu-24"
|
||||||
|
AllowedValues: ['Ubuntu-22', 'Ubuntu-24']
|
||||||
|
|
||||||
InitialNumberOfMediaNodes:
|
InitialNumberOfMediaNodes:
|
||||||
Type: Number
|
Type: Number
|
||||||
|
|
@ -422,7 +839,7 @@ Metadata:
|
||||||
- MasterNodeInstanceType
|
- MasterNodeInstanceType
|
||||||
- MediaNodeInstanceType
|
- MediaNodeInstanceType
|
||||||
- KeyName
|
- KeyName
|
||||||
- AmiId
|
- OperatingSystem
|
||||||
- Label:
|
- Label:
|
||||||
default: Media Nodes Autoscaling Group configuration
|
default: Media Nodes Autoscaling Group configuration
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
@ -455,6 +872,41 @@ Conditions:
|
||||||
PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]
|
PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]
|
||||||
PublicElasticIPAbsent: !Equals [!Ref PublicElasticIP, ""]
|
PublicElasticIPAbsent: !Equals [!Ref PublicElasticIP, ""]
|
||||||
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
|
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
|
||||||
|
IsMasterGraviton: !Or
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 't4g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c6g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c7gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'c8g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm6g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm6gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm7g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm7gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MasterNodeInstanceType ]], 'm8g']
|
||||||
|
IsMediaGraviton: !Or
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 't4g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c6g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c7gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'c8g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm6g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm6gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm7g']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm7gd']
|
||||||
|
- !Equals [ !Select [ 0, !Split ['.', !Ref MediaNodeInstanceType ]], 'm8g']
|
||||||
|
|
||||||
|
Mappings:
|
||||||
|
ArmImage:
|
||||||
|
# aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/" --recursive --query "Parameters[*].Name" > canonical-ami.txt
|
||||||
|
Ubuntu-22:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/arm64/hvm/ebs-gp2/ami-id}}'
|
||||||
|
Ubuntu-24:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/arm64/hvm/ebs-gp3/ami-id}}'
|
||||||
|
AmdImage:
|
||||||
|
Ubuntu-22:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id}}'
|
||||||
|
Ubuntu-24:
|
||||||
|
ImageId: '{{resolve:ssm:/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id}}'
|
||||||
|
|
||||||
Resources:
|
Resources:
|
||||||
|
|
||||||
|
|
@ -684,7 +1136,7 @@ Resources:
|
||||||
content: !Sub |
|
content: !Sub |
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
OPENVIDU_VERSION=3.4.1
|
OPENVIDU_VERSION=main
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
YQ_VERSION=v4.44.5
|
YQ_VERSION=v4.44.5
|
||||||
|
|
||||||
|
|
@ -1172,7 +1624,10 @@ Resources:
|
||||||
owner: "root"
|
owner: "root"
|
||||||
group: "root"
|
group: "root"
|
||||||
Properties:
|
Properties:
|
||||||
ImageId: !Ref AmiId
|
ImageId: !If
|
||||||
|
- IsMasterGraviton
|
||||||
|
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
|
||||||
|
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
|
||||||
LaunchTemplate:
|
LaunchTemplate:
|
||||||
# Enable IMDSv2 by default
|
# Enable IMDSv2 by default
|
||||||
LaunchTemplateId: !Ref IMDSv2LaunchTemplateMasterNode
|
LaunchTemplateId: !Ref IMDSv2LaunchTemplateMasterNode
|
||||||
|
|
@ -1191,6 +1646,8 @@ Resources:
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -eu -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
ec2-instance-connect
|
ec2-instance-connect
|
||||||
|
|
@ -1420,7 +1877,10 @@ Resources:
|
||||||
Arn: !GetAtt OpenViduMediaNodeInstanceProfile.Arn
|
Arn: !GetAtt OpenViduMediaNodeInstanceProfile.Arn
|
||||||
SecurityGroupIds:
|
SecurityGroupIds:
|
||||||
- !GetAtt OpenViduMediaNodeSG.GroupId
|
- !GetAtt OpenViduMediaNodeSG.GroupId
|
||||||
ImageId: !Ref AmiId
|
ImageId: !If
|
||||||
|
- IsMediaGraviton
|
||||||
|
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
|
||||||
|
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
|
||||||
KeyName: !Ref KeyName
|
KeyName: !Ref KeyName
|
||||||
InstanceType: !Ref MediaNodeInstanceType
|
InstanceType: !Ref MediaNodeInstanceType
|
||||||
UserData:
|
UserData:
|
||||||
|
|
@ -1428,6 +1888,8 @@ Resources:
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -eu -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
ec2-instance-connect
|
ec2-instance-connect
|
||||||
|
|
|
||||||
|
|
@ -53,229 +53,11 @@ param initialMeetAdminPassword string = ''
|
||||||
@secure()
|
@secure()
|
||||||
param initialMeetApiKey string = ''
|
param initialMeetApiKey string = ''
|
||||||
|
|
||||||
@description('Specifies the EC2 instance type for your OpenVidu Master Node')
|
@description('Specifies the VM size for your OpenVidu Master Node. You can use any valid Azure VM size (e.g., Standard_B4s, Standard_D4s_v5, Standard_E4ps_v5). See https://learn.microsoft.com/en-us/azure/virtual-machines/sizes for available sizes.')
|
||||||
@allowed([
|
param masterNodeInstanceType string = 'Standard_B4s'
|
||||||
'Standard_B1s'
|
|
||||||
'Standard_B1ms'
|
|
||||||
'Standard_B2s'
|
|
||||||
'Standard_B2ms'
|
|
||||||
'Standard_B4ms'
|
|
||||||
'Standard_B8ms'
|
|
||||||
'Standard_D2_v3'
|
|
||||||
'Standard_D4_v3'
|
|
||||||
'Standard_D8_v3'
|
|
||||||
'Standard_D16_v3'
|
|
||||||
'Standard_D32_v3'
|
|
||||||
'Standard_D48_v3'
|
|
||||||
'Standard_D64_v3'
|
|
||||||
'Standard_D2_v4'
|
|
||||||
'Standard_D4_v4'
|
|
||||||
'Standard_D8_v4'
|
|
||||||
'Standard_D16_v4'
|
|
||||||
'Standard_D32_v4'
|
|
||||||
'Standard_D48_v4'
|
|
||||||
'Standard_D64_v4'
|
|
||||||
'Standard_D96_v4'
|
|
||||||
'Standard_D2_v5'
|
|
||||||
'Standard_D4_v5'
|
|
||||||
'Standard_D8_v5'
|
|
||||||
'Standard_D16_v5'
|
|
||||||
'Standard_D32_v5'
|
|
||||||
'Standard_D48_v5'
|
|
||||||
'Standard_D64_v5'
|
|
||||||
'Standard_D96_v5'
|
|
||||||
'Standard_F2'
|
|
||||||
'Standard_F4'
|
|
||||||
'Standard_F8'
|
|
||||||
'Standard_F16'
|
|
||||||
'Standard_F32'
|
|
||||||
'Standard_F64'
|
|
||||||
'Standard_F72'
|
|
||||||
'Standard_F2s_v2'
|
|
||||||
'Standard_F4s_v2'
|
|
||||||
'Standard_F8s_v2'
|
|
||||||
'Standard_F16s_v2'
|
|
||||||
'Standard_F32s_v2'
|
|
||||||
'Standard_F64s_v2'
|
|
||||||
'Standard_F72s_v2'
|
|
||||||
'Standard_E2_v3'
|
|
||||||
'Standard_E4_v3'
|
|
||||||
'Standard_E8_v3'
|
|
||||||
'Standard_E16_v3'
|
|
||||||
'Standard_E32_v3'
|
|
||||||
'Standard_E48_v3'
|
|
||||||
'Standard_E64_v3'
|
|
||||||
'Standard_E96_v3'
|
|
||||||
'Standard_E2_v4'
|
|
||||||
'Standard_E4_v4'
|
|
||||||
'Standard_E8_v4'
|
|
||||||
'Standard_E16_v4'
|
|
||||||
'Standard_E32_v4'
|
|
||||||
'Standard_E48_v4'
|
|
||||||
'Standard_E64_v4'
|
|
||||||
'Standard_E2_v5'
|
|
||||||
'Standard_E4_v5'
|
|
||||||
'Standard_E8_v5'
|
|
||||||
'Standard_E16_v5'
|
|
||||||
'Standard_E32_v5'
|
|
||||||
'Standard_E48_v5'
|
|
||||||
'Standard_E64_v5'
|
|
||||||
'Standard_E96_v5'
|
|
||||||
'Standard_M64'
|
|
||||||
'Standard_M128'
|
|
||||||
'Standard_M208ms_v2'
|
|
||||||
'Standard_M416ms_v2'
|
|
||||||
'Standard_L4s_v2'
|
|
||||||
'Standard_L8s_v2'
|
|
||||||
'Standard_L16s_v2'
|
|
||||||
'Standard_L32s_v2'
|
|
||||||
'Standard_L64s_v2'
|
|
||||||
'Standard_L80s_v2'
|
|
||||||
'Standard_NC6'
|
|
||||||
'Standard_NC12'
|
|
||||||
'Standard_NC24'
|
|
||||||
'Standard_NC24r'
|
|
||||||
'Standard_ND6s'
|
|
||||||
'Standard_ND12s'
|
|
||||||
'Standard_ND24s'
|
|
||||||
'Standard_ND24rs'
|
|
||||||
'Standard_NV6'
|
|
||||||
'Standard_NV12'
|
|
||||||
'Standard_NV24'
|
|
||||||
'Standard_H8'
|
|
||||||
'Standard_H16'
|
|
||||||
'Standard_H16r'
|
|
||||||
'Standard_H16mr'
|
|
||||||
'Standard_HB120rs_v2'
|
|
||||||
'Standard_HC44rs'
|
|
||||||
'Standard_DC2s'
|
|
||||||
'Standard_DC4s'
|
|
||||||
'Standard_DC2s_v2'
|
|
||||||
'Standard_DC4s_v2'
|
|
||||||
'Standard_DC8s_v2'
|
|
||||||
'Standard_DC16s_v2'
|
|
||||||
'Standard_DC32s_v2'
|
|
||||||
'Standard_A1_v2'
|
|
||||||
'Standard_A2_v2'
|
|
||||||
'Standard_A4_v2'
|
|
||||||
'Standard_A8_v2'
|
|
||||||
'Standard_A2m_v2'
|
|
||||||
'Standard_A4m_v2'
|
|
||||||
'Standard_A8m_v2'
|
|
||||||
])
|
|
||||||
param masterNodeInstanceType string = 'Standard_B2s'
|
|
||||||
|
|
||||||
@description('Specifies the EC2 instance type for your OpenVidu Media Nodes')
|
@description('Specifies the VM size for your OpenVidu Media Nodes. You can use any valid Azure VM size (e.g., Standard_B4s, Standard_D4s_v5, Standard_E4ps_v5). See https://learn.microsoft.com/en-us/azure/virtual-machines/sizes for available sizes.')
|
||||||
@allowed([
|
param mediaNodeInstanceType string = 'Standard_B4s'
|
||||||
'Standard_B1s'
|
|
||||||
'Standard_B1ms'
|
|
||||||
'Standard_B2s'
|
|
||||||
'Standard_B2ms'
|
|
||||||
'Standard_B4ms'
|
|
||||||
'Standard_B8ms'
|
|
||||||
'Standard_D2_v3'
|
|
||||||
'Standard_D4_v3'
|
|
||||||
'Standard_D8_v3'
|
|
||||||
'Standard_D16_v3'
|
|
||||||
'Standard_D32_v3'
|
|
||||||
'Standard_D48_v3'
|
|
||||||
'Standard_D64_v3'
|
|
||||||
'Standard_D2_v4'
|
|
||||||
'Standard_D4_v4'
|
|
||||||
'Standard_D8_v4'
|
|
||||||
'Standard_D16_v4'
|
|
||||||
'Standard_D32_v4'
|
|
||||||
'Standard_D48_v4'
|
|
||||||
'Standard_D64_v4'
|
|
||||||
'Standard_D96_v4'
|
|
||||||
'Standard_D2_v5'
|
|
||||||
'Standard_D4_v5'
|
|
||||||
'Standard_D8_v5'
|
|
||||||
'Standard_D16_v5'
|
|
||||||
'Standard_D32_v5'
|
|
||||||
'Standard_D48_v5'
|
|
||||||
'Standard_D64_v5'
|
|
||||||
'Standard_D96_v5'
|
|
||||||
'Standard_F2'
|
|
||||||
'Standard_F4'
|
|
||||||
'Standard_F8'
|
|
||||||
'Standard_F16'
|
|
||||||
'Standard_F32'
|
|
||||||
'Standard_F64'
|
|
||||||
'Standard_F72'
|
|
||||||
'Standard_F2s_v2'
|
|
||||||
'Standard_F4s_v2'
|
|
||||||
'Standard_F8s_v2'
|
|
||||||
'Standard_F16s_v2'
|
|
||||||
'Standard_F32s_v2'
|
|
||||||
'Standard_F64s_v2'
|
|
||||||
'Standard_F72s_v2'
|
|
||||||
'Standard_E2_v3'
|
|
||||||
'Standard_E4_v3'
|
|
||||||
'Standard_E8_v3'
|
|
||||||
'Standard_E16_v3'
|
|
||||||
'Standard_E32_v3'
|
|
||||||
'Standard_E48_v3'
|
|
||||||
'Standard_E64_v3'
|
|
||||||
'Standard_E96_v3'
|
|
||||||
'Standard_E2_v4'
|
|
||||||
'Standard_E4_v4'
|
|
||||||
'Standard_E8_v4'
|
|
||||||
'Standard_E16_v4'
|
|
||||||
'Standard_E32_v4'
|
|
||||||
'Standard_E48_v4'
|
|
||||||
'Standard_E64_v4'
|
|
||||||
'Standard_E2_v5'
|
|
||||||
'Standard_E4_v5'
|
|
||||||
'Standard_E8_v5'
|
|
||||||
'Standard_E16_v5'
|
|
||||||
'Standard_E32_v5'
|
|
||||||
'Standard_E48_v5'
|
|
||||||
'Standard_E64_v5'
|
|
||||||
'Standard_E96_v5'
|
|
||||||
'Standard_M64'
|
|
||||||
'Standard_M128'
|
|
||||||
'Standard_M208ms_v2'
|
|
||||||
'Standard_M416ms_v2'
|
|
||||||
'Standard_L4s_v2'
|
|
||||||
'Standard_L8s_v2'
|
|
||||||
'Standard_L16s_v2'
|
|
||||||
'Standard_L32s_v2'
|
|
||||||
'Standard_L64s_v2'
|
|
||||||
'Standard_L80s_v2'
|
|
||||||
'Standard_NC6'
|
|
||||||
'Standard_NC12'
|
|
||||||
'Standard_NC24'
|
|
||||||
'Standard_NC24r'
|
|
||||||
'Standard_ND6s'
|
|
||||||
'Standard_ND12s'
|
|
||||||
'Standard_ND24s'
|
|
||||||
'Standard_ND24rs'
|
|
||||||
'Standard_NV6'
|
|
||||||
'Standard_NV12'
|
|
||||||
'Standard_NV24'
|
|
||||||
'Standard_H8'
|
|
||||||
'Standard_H16'
|
|
||||||
'Standard_H16r'
|
|
||||||
'Standard_H16mr'
|
|
||||||
'Standard_HB120rs_v2'
|
|
||||||
'Standard_HC44rs'
|
|
||||||
'Standard_DC2s'
|
|
||||||
'Standard_DC4s'
|
|
||||||
'Standard_DC2s_v2'
|
|
||||||
'Standard_DC4s_v2'
|
|
||||||
'Standard_DC8s_v2'
|
|
||||||
'Standard_DC16s_v2'
|
|
||||||
'Standard_DC32s_v2'
|
|
||||||
'Standard_A1_v2'
|
|
||||||
'Standard_A2_v2'
|
|
||||||
'Standard_A4_v2'
|
|
||||||
'Standard_A8_v2'
|
|
||||||
'Standard_A2m_v2'
|
|
||||||
'Standard_A4m_v2'
|
|
||||||
'Standard_A8m_v2'
|
|
||||||
])
|
|
||||||
param mediaNodeInstanceType string = 'Standard_B2s'
|
|
||||||
|
|
||||||
@description('Username for the Virtual Machine.')
|
@description('Username for the Virtual Machine.')
|
||||||
param adminUsername string
|
param adminUsername string
|
||||||
|
|
@ -304,13 +86,25 @@ var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
|
||||||
|
|
||||||
var isEmptyDomain = domainName == ''
|
var isEmptyDomain = domainName == ''
|
||||||
|
|
||||||
|
// ARM64 instances are detected by checking for 'p' in the instance type name pattern.
|
||||||
|
// Azure ARM-based VMs use 'p' to indicate ARM processors (Ampere Altra, Microsoft Cobalt, etc.)
|
||||||
|
// Examples: Standard_D2ps_v5, Standard_E4pds_v5, Standard_B2pls_v2, etc.
|
||||||
|
// The pattern checks for 'p' followed by optional letters (like 'l', 'd', 's') before '_v' version suffix
|
||||||
|
var masterNodeInstanceTypeLower = toLower(masterNodeInstanceType)
|
||||||
|
var mediaNodeInstanceTypeLower = toLower(mediaNodeInstanceType)
|
||||||
|
var isMasterArm64 = contains(masterNodeInstanceTypeLower, 'ps_v') || contains(masterNodeInstanceTypeLower, 'pls_v') || contains(masterNodeInstanceTypeLower, 'pds_v') || contains(masterNodeInstanceTypeLower, 'plds_v') || contains(masterNodeInstanceTypeLower, 'psv') || contains(masterNodeInstanceTypeLower, 'plsv') || contains(masterNodeInstanceTypeLower, 'pdsv') || contains(masterNodeInstanceTypeLower, 'pldsv')
|
||||||
|
var isMediaArm64 = contains(mediaNodeInstanceTypeLower, 'ps_v') || contains(mediaNodeInstanceTypeLower, 'pls_v') || contains(mediaNodeInstanceTypeLower, 'pds_v') || contains(mediaNodeInstanceTypeLower, 'plds_v') || contains(mediaNodeInstanceTypeLower, 'psv') || contains(mediaNodeInstanceTypeLower, 'plsv') || contains(mediaNodeInstanceTypeLower, 'pdsv') || contains(mediaNodeInstanceTypeLower, 'pldsv')
|
||||||
|
|
||||||
|
var masterUbuntuSku = isMasterArm64 ? 'server-arm64' : 'server'
|
||||||
|
var mediaUbuntuSku = isMediaArm64 ? 'server-arm64' : 'server'
|
||||||
|
|
||||||
var masterNodeVMSettings = {
|
var masterNodeVMSettings = {
|
||||||
vmName: '${stackName}-VM-MasterNode'
|
vmName: '${stackName}-VM-MasterNode'
|
||||||
osDiskType: 'StandardSSD_LRS'
|
osDiskType: 'StandardSSD_LRS'
|
||||||
ubuntuOSVersion: {
|
ubuntuOSVersion: {
|
||||||
publisher: 'Canonical'
|
publisher: 'Canonical'
|
||||||
offer: '0001-com-ubuntu-server-jammy'
|
offer: 'ubuntu-24_04-lts'
|
||||||
sku: '22_04-lts-gen2'
|
sku: masterUbuntuSku
|
||||||
version: 'latest'
|
version: 'latest'
|
||||||
}
|
}
|
||||||
linuxConfiguration: {
|
linuxConfiguration: {
|
||||||
|
|
@ -331,8 +125,8 @@ var mediaNodeVMSettings = {
|
||||||
osDiskType: 'StandardSSD_LRS'
|
osDiskType: 'StandardSSD_LRS'
|
||||||
ubuntuOSVersion: {
|
ubuntuOSVersion: {
|
||||||
publisher: 'Canonical'
|
publisher: 'Canonical'
|
||||||
offer: '0001-com-ubuntu-server-jammy'
|
offer: 'ubuntu-24_04-lts'
|
||||||
sku: '22_04-lts-gen2'
|
sku: mediaUbuntuSku
|
||||||
version: 'latest'
|
version: 'latest'
|
||||||
}
|
}
|
||||||
linuxConfiguration: {
|
linuxConfiguration: {
|
||||||
|
|
@ -429,11 +223,13 @@ var stringInterpolationParamsMaster = {
|
||||||
|
|
||||||
var installScriptTemplateMaster = '''
|
var installScriptTemplateMaster = '''
|
||||||
#!/bin/bash -x
|
#!/bin/bash -x
|
||||||
OPENVIDU_VERSION=3.4.1
|
OPENVIDU_VERSION=main
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
|
|
||||||
# Assume azure cli is installed
|
# Assume azure cli is installed
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
unzip \
|
unzip \
|
||||||
|
|
@ -1054,6 +850,8 @@ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||||||
|
|
||||||
az login --identity --allow-no-subscriptions
|
az login --identity --allow-no-subscriptions
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y
|
apt-get update && apt-get install -y
|
||||||
|
|
||||||
export HOME="/root"
|
export HOME="/root"
|
||||||
|
|
@ -1137,6 +935,8 @@ set -e
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
unzip \
|
unzip \
|
||||||
|
|
@ -1284,6 +1084,8 @@ chmod +x /usr/local/bin/stop_media_node.sh
|
||||||
echo ${base64delete} | base64 -d > /usr/local/bin/delete_media_node.sh
|
echo ${base64delete} | base64 -d > /usr/local/bin/delete_media_node.sh
|
||||||
chmod +x /usr/local/bin/delete_media_node.sh
|
chmod +x /usr/local/bin/delete_media_node.sh
|
||||||
|
|
||||||
|
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
|
||||||
|
|
||||||
apt-get update && apt-get install -y
|
apt-get update && apt-get install -y
|
||||||
apt-get install -y jq
|
apt-get install -y jq
|
||||||
|
|
||||||
|
|
@ -1530,7 +1332,7 @@ module webhookModule '../../shared/webhookdeployment.json' = {
|
||||||
}
|
}
|
||||||
|
|
||||||
resource actionGroupScaleIn 'Microsoft.Insights/actionGroups@2023-01-01' = {
|
resource actionGroupScaleIn 'Microsoft.Insights/actionGroups@2023-01-01' = {
|
||||||
name: 'actiongrouptest'
|
name: 'actiongroupScaleIn'
|
||||||
location: 'global'
|
location: 'global'
|
||||||
properties: {
|
properties: {
|
||||||
groupShortName: 'scaleinag'
|
groupShortName: 'scaleinag'
|
||||||
|
|
@ -1675,7 +1477,9 @@ resource netInterfaceMasterNode 'Microsoft.Network/networkInterfaces@2023-11-01'
|
||||||
id: openviduMasterNodeASG.id
|
id: openviduMasterNodeASG.id
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
publicIPAddress: isEmptyIp ? null : {
|
publicIPAddress: isEmptyIp
|
||||||
|
? null
|
||||||
|
: {
|
||||||
id: ipNew ? publicIP_OV_ifNew.id : publicIP_OV_ifExisting.id
|
id: ipNew ? publicIP_OV_ifNew.id : publicIP_OV_ifExisting.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -80,7 +80,7 @@
|
||||||
"publicIpAddress": "Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP"
|
"publicIpAddress": "Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP"
|
||||||
},
|
},
|
||||||
"defaultValue": {
|
"defaultValue": {
|
||||||
"publicIpAddressName": "defaultName"
|
"publicIpAddressName": "ov-publicIpAddress"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"hideNone": true,
|
"hideNone": true,
|
||||||
|
|
@ -260,13 +260,14 @@
|
||||||
"label": "Master Node Instance Type",
|
"label": "Master Node Instance Type",
|
||||||
"toolTip": "Specifies the Azure instance type for your OpenVidu Master Node",
|
"toolTip": "Specifies the Azure instance type for your OpenVidu Master Node",
|
||||||
"recommendedSizes": [
|
"recommendedSizes": [
|
||||||
"Standard_B2s"
|
"Standard_B4s",
|
||||||
|
"Standard_B4ms",
|
||||||
|
"Standard_D4ps_v5",
|
||||||
|
"Standard_D4pls_v5"
|
||||||
],
|
],
|
||||||
"constraints": {
|
"constraints": {
|
||||||
"allowedSizes": [],
|
"allowedSizes": [],
|
||||||
"excludedSizes": [],
|
"excludedSizes": []
|
||||||
"numAvailabilityZonesRequired": 3,
|
|
||||||
"zone": "3"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"hideDiskTypeFilter": false
|
"hideDiskTypeFilter": false
|
||||||
|
|
@ -280,14 +281,14 @@
|
||||||
"label": "Media Node Instance Type",
|
"label": "Media Node Instance Type",
|
||||||
"toolTip": "Specifies the Azure instance type for your OpenVidu Media Nodes",
|
"toolTip": "Specifies the Azure instance type for your OpenVidu Media Nodes",
|
||||||
"recommendedSizes": [
|
"recommendedSizes": [
|
||||||
"Standard_B2s",
|
"Standard_B4s",
|
||||||
"Standard_B4ms"
|
"Standard_B4ms",
|
||||||
|
"Standard_D4ps_v5",
|
||||||
|
"Standard_D4pls_v5"
|
||||||
],
|
],
|
||||||
"constraints": {
|
"constraints": {
|
||||||
"allowedSizes": [],
|
"allowedSizes": [],
|
||||||
"excludedSizes": [],
|
"excludedSizes": []
|
||||||
"numAvailabilityZonesRequired": 3,
|
|
||||||
"zone": "3"
|
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"hideDiskTypeFilter": false
|
"hideDiskTypeFilter": false
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# ------------------------- outputs.tf -------------------------
|
||||||
|
|
||||||
|
output "secrets_manager" {
|
||||||
|
value = "https://console.cloud.google.com/security/secret-manager?project=${var.projectId}"
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,172 @@
|
||||||
|
# ------------------------- variables -------------------------
|
||||||
|
|
||||||
|
# Variables used by the configuration
|
||||||
|
variable "projectId" {
|
||||||
|
description = "GCP project id where the resourw es will be created."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
description = "GCP region where resources will be created."
|
||||||
|
type = string
|
||||||
|
default = "europe-west2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "zone" {
|
||||||
|
description = "GCP zone that some resources will use."
|
||||||
|
type = string
|
||||||
|
default = "europe-west2-b"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "stackName" {
|
||||||
|
description = "Stack name for OpenVidu deployment."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "certificateType" {
|
||||||
|
description = "[selfsigned] Not recommended for production use. Just for testing purposes or development environments. You don't need a FQDN to use this option. [owncert] Valid for production environments. Use your own certificate. You need a FQDN to use this option. [letsencrypt] Valid for production environments. Can be used with or without a FQDN (if no FQDN is provided, a random sslip.io domain will be used)."
|
||||||
|
type = string
|
||||||
|
default = "letsencrypt"
|
||||||
|
validation {
|
||||||
|
condition = contains(["selfsigned", "owncert", "letsencrypt"], var.certificateType)
|
||||||
|
error_message = "certificateType must be one of: selfsigned, owncert, letsencrypt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "publicIpAddress" {
|
||||||
|
description = "Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^$|^([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.([01]?\\d{1,2}|2[0-4]\\d|25[0-5])$", var.publicIpAddress))
|
||||||
|
error_message = "The Public Elastic IP does not have a valid IPv4 format"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domainName" {
|
||||||
|
description = "Domain name for the OpenVidu Deployment."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$", var.domainName))
|
||||||
|
error_message = "The domain name does not have a valid domain name format"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ownPublicCertificate" {
|
||||||
|
description = "If certificate type is 'owncert', this parameter will be used to specify the public certificate"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ownPrivateCertificate" {
|
||||||
|
description = "If certificate type is 'owncert', this parameter will be used to specify the private certificate"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "initialMeetAdminPassword" {
|
||||||
|
description = "Initial password for the 'admin' user in OpenVidu Meet. If not provided, a random password will be generated."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^[A-Za-z0-9_-]*$", var.initialMeetAdminPassword))
|
||||||
|
error_message = "Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to generate a random password."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "initialMeetApiKey" {
|
||||||
|
description = "Initial API key for OpenVidu Meet. If not provided, no API key will be set and the user can set it later from Meet Console."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^[A-Za-z0-9_-]*$", var.initialMeetApiKey))
|
||||||
|
error_message = "Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to not set an initial API key."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "masterNodeInstanceType" {
|
||||||
|
description = "Specifies the GCE machine type for your OpenVidu Master Node"
|
||||||
|
type = string
|
||||||
|
default = "e2-standard-2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mediaNodeInstanceType" {
|
||||||
|
description = "Specifies the GCE machine type for your OpenVidu Media Nodes"
|
||||||
|
type = string
|
||||||
|
default = "e2-standard-2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "initialNumberOfMediaNodes" {
|
||||||
|
description = "Number of initial media nodes to deploy"
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minNumberOfMediaNodes" {
|
||||||
|
description = "Minimum number of media nodes to deploy"
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "maxNumberOfMediaNodes" {
|
||||||
|
description = "Maximum number of media nodes to deploy"
|
||||||
|
type = number
|
||||||
|
default = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scaleTargetCPU" {
|
||||||
|
description = "Target CPU percentage to scale up or down"
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "bucketName" {
|
||||||
|
description = "Name of the GCS bucket to store data and recordings. If empty, a bucket will be created"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "openviduLicense" {
|
||||||
|
description = "Visit https://openvidu.io/account"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rtcEngine" {
|
||||||
|
description = "RTCEngine media engine to use"
|
||||||
|
type = string
|
||||||
|
default = "pion"
|
||||||
|
validation {
|
||||||
|
condition = contains(["pion", "mediasoup"], var.rtcEngine)
|
||||||
|
error_message = "rtcEngine must be one of: pion, mediasoup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "additionalInstallFlags" {
|
||||||
|
description = "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g.,'--flag1=value, --flag2')."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^[A-Za-z0-9, =_.\\-]*$", var.additionalInstallFlags))
|
||||||
|
error_message = "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "turnDomainName" {
|
||||||
|
description = "(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "turnOwnPublicCertificate" {
|
||||||
|
description = "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "turnOwnPrivateCertificate" {
|
||||||
|
description = "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
|
required_providers {
|
||||||
|
google = {
|
||||||
|
source = "hashicorp/google"
|
||||||
|
version = ">= 4.0"
|
||||||
|
}
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = ">= 3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "google" {
|
||||||
|
project = var.projectId
|
||||||
|
region = var.region
|
||||||
|
zone = var.zone
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Docker & Docker Compose will need to be installed on the machine
|
# Docker & Docker Compose will need to be installed on the machine
|
||||||
set -eu
|
set -eu
|
||||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.3.3}"
|
export DOCKER_VERSION="${DOCKER_VERSION:-29.0.2}"
|
||||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.39.4}"
|
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
|
||||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.4.1}"
|
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||||
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
||||||
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
|
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.9.7-debian-12-r3}"
|
||||||
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-08-13T08-35-41Z}"
|
||||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.15}"
|
||||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:8.2.2-alpine}"
|
||||||
export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}"
|
export BUSYBOX_IMAGE="${BUSYBOX_IMAGE:-docker.io/busybox:1.37.0}"
|
||||||
export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}"
|
export CADDY_SERVER_IMAGE="${CADDY_SERVER_IMAGE:-docker.io/openvidu/openvidu-caddy:${OPENVIDU_VERSION}}"
|
||||||
export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}"
|
export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-caddy:${OPENVIDU_VERSION}}"
|
||||||
|
|
@ -22,11 +22,11 @@ export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.
|
||||||
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
||||||
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
||||||
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/openvidu/egress:${OPENVIDU_VERSION}}"
|
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/openvidu/egress:${OPENVIDU_VERSION}}"
|
||||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.7.1}"
|
||||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.7}"
|
||||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.7}"
|
||||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.17.1}"
|
||||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:12.2.0}"
|
||||||
|
|
||||||
# Function to compare two version strings
|
# Function to compare two version strings
|
||||||
compare_versions() {
|
compare_versions() {
|
||||||
|
|
@ -158,11 +158,13 @@ if [ "$DOCKER_COMPOSE_NEEDED" = true ]; then
|
||||||
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
|
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Restart Docker and wait for it to start
|
# Check if docker is running with docker info
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "Docker is not running. Starting Docker..."
|
||||||
systemctl enable docker
|
systemctl enable docker
|
||||||
systemctl stop docker
|
|
||||||
systemctl start docker
|
systemctl start docker
|
||||||
wait_for_docker
|
wait_for_docker
|
||||||
|
fi
|
||||||
|
|
||||||
# Create random temp directory
|
# Create random temp directory
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue