mirror of https://github.com/OpenVidu/openvidu.git
Compare commits
75 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
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:
|
||||
push:
|
||||
|
|
@ -17,6 +17,10 @@ on:
|
|||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
CHROME_IMAGE: selenium/standalone-chrome:138.0
|
||||
|
||||
jobs:
|
||||
test_setup:
|
||||
name: Test setup
|
||||
|
|
@ -29,7 +33,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Commit URL
|
||||
run: echo https://github.com/OpenVidu/openvidu/commit/${{ inputs.commit_sha || github.sha }}
|
||||
- name: Send Dispatch Event
|
||||
|
|
@ -45,10 +49,41 @@ jobs:
|
|||
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"'"}}'
|
||||
|
||||
nested_events:
|
||||
e2e_tests:
|
||||
needs: test_setup
|
||||
name: Nested events
|
||||
name: ${{ matrix.name }}
|
||||
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:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -57,116 +92,16 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- 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: Install dependencies
|
||||
run: |
|
||||
cd openvidu-components-angular
|
||||
npm install
|
||||
- name: Build and Serve openvidu-components-angular Testapp
|
||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
||||
- name: Run nested components E2E event tests
|
||||
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
|
||||
if [ "${{ matrix.mount_assets }}" = "true" ]; then
|
||||
docker run --network=host -d -p 4444:4444 -v $(pwd)/openvidu-components-angular/e2e/assets:/e2e-assets ${{ env.CHROME_IMAGE }}
|
||||
else
|
||||
docker run --network=host -d -p 4444:4444 ${{ env.CHROME_IMAGE }}
|
||||
fi
|
||||
- name: Run openvidu-local-deployment
|
||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
||||
- name: Start OpenVidu Call backend
|
||||
|
|
@ -176,303 +111,7 @@ jobs:
|
|||
- name: Run Tests
|
||||
env:
|
||||
LAUNCH_MODE: CI
|
||||
run: npm run e2e:lib-directives --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
|
||||
run: npm run ${{ matrix.script }} --prefix openvidu-components-angular
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
uses: OpenVidu/actions/cleanup@main
|
||||
|
|
|
|||
|
|
@ -205,112 +205,151 @@ describe('E2E: Screensharing features', () => {
|
|||
await browser.sleep(500);
|
||||
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
|
||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
// expect(await screenshareButton.isDisplayed()).toBeTrue();
|
||||
// await screenshareButton.click();
|
||||
it('should NOT have multiple screens pinned when both participants share screen', async () => {
|
||||
const roomName = 'pinBugCase1';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
|
||||
// await utils.waitForElement('.OV_big');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// Participant A joins and shares screen
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
||||
// await muteVideoButton.click();
|
||||
// Verify A's screen is pinned
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||
const pinnedCountA1 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab A] After A shares: ${pinnedCountA1} pinned stream(s)`);
|
||||
|
||||
// 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 () => {
|
||||
// let isAudioEnabled;
|
||||
// const audioEnableScript = 'return document.getElementsByTagName("video")[0].srcObject.getAudioTracks()[0].enabled;';
|
||||
// B should see A's screen pinned
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3); // 2 cameras + 1 screen
|
||||
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||
const pinnedCountB1 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After B joins: ${pinnedCountB1} pinned stream(s)`);
|
||||
|
||||
// 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
|
||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
|
||||
// await screenshareButton.click();
|
||||
// Switch to Tab A and check
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
|
||||
|
||||
// await utils.waitForElement('.OV_big');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
// BUG: In A's view, BOTH screens are pinned
|
||||
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab A] After B shares: ${pinnedCountA2} pinned stream(s)`);
|
||||
|
||||
// // Muting camera video
|
||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
||||
// await muteVideoButton.click();
|
||||
// EXPECTED: Only B's screen should be pinned (the most recent one)
|
||||
// ACTUAL: Both A's and B's screens are pinned
|
||||
expect(pinnedCountA2).toEqual(1, 'BUG DETECTED: Multiple screens are pinned. Expected only the most recent screen to be pinned.');
|
||||
});
|
||||
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
it('should NOT re-pin manually unpinned screen when new participant joins', async () => {
|
||||
const roomName = 'pinBugCase2';
|
||||
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
|
||||
|
||||
// await browser.sleep(500);
|
||||
// expect(await utils.isPresent('#status-mic')).toBeFalse();
|
||||
// Participant A joins and shares screen
|
||||
await browser.get(fixedUrl);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// // Checking if audio is muted after join the room
|
||||
// isAudioEnabled = await browser.executeScript(audioEnableScript);
|
||||
// expect(isAudioEnabled).toBeTrue();
|
||||
// Verify A's screen is auto-pinned
|
||||
await utils.waitForElement('.OV_big');
|
||||
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
|
||||
|
||||
// // Unmuting camera
|
||||
// await muteVideoButton.click();
|
||||
// await browser.sleep(1000);
|
||||
// Participant B joins and shares screen
|
||||
const tabs = await utils.openTab(fixedUrl);
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await utils.checkLayoutPresent();
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#screenshare-btn');
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
|
||||
// await utils.waitForElement('.camera-type');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
// });
|
||||
// B should see their own screen pinned
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
|
||||
await utils.waitForElement('.OV_big');
|
||||
let pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After B shares: ${pinnedCountB} pinned stream(s)`);
|
||||
|
||||
// it('should camera come back with audio muted when screensharing', async () => {
|
||||
// let element, isAudioEnabled;
|
||||
// B manually unpins their own screen
|
||||
const screenStreams = await utils.getScreenShareStreams();
|
||||
if (screenStreams.length > 0) {
|
||||
// Find B's own screen (it should be the pinned one)
|
||||
await utils.toggleStreamPin('.OV_big');
|
||||
await browser.sleep(1000);
|
||||
}
|
||||
|
||||
// const getAudioScript = (className: string) => {
|
||||
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
|
||||
// };
|
||||
// Verify B's screen is now unpinned
|
||||
pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After manually unpinning B's screen: ${pinnedCountB} pinned stream(s)`);
|
||||
expect(pinnedCountB).toEqual(0, 'B should have no pinned streams after manual unpin');
|
||||
|
||||
// await browser.get(`${url}&prejoin=false`);
|
||||
// B manually pins A's screen
|
||||
const screenElements = await utils.getScreenShareStreams();
|
||||
if (screenElements.length >= 2) {
|
||||
// Pin the first screen that is not already pinned (should be A's screen)
|
||||
await utils.toggleStreamPin('.OV_stream.remote .screen-type');
|
||||
await utils.toggleStreamPin('#pin-btn');
|
||||
await browser.sleep(500);
|
||||
}
|
||||
|
||||
// await utils.checkLayoutPresent();
|
||||
// Verify A's screen is now pinned in B's view
|
||||
pinnedCountB = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab B] After manually pinning A's screen: ${pinnedCountB} pinned stream(s)`);
|
||||
expect(pinnedCountB).toEqual(1, "Only A's screen should be pinned");
|
||||
|
||||
// // Clicking to screensharing button
|
||||
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
|
||||
// await screenshareButton.click();
|
||||
// Participant C joins the room
|
||||
const tab3 = await utils.openTab(fixedUrl);
|
||||
await browser.switchTo().window(tab3[2]);
|
||||
await utils.checkLayoutPresent();
|
||||
await browser.sleep(1500);
|
||||
|
||||
// await utils.waitForElement('.screen-type');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
// Switch back to B's tab
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await browser.sleep(1000);
|
||||
|
||||
// // Mute camera
|
||||
// const muteVideoButton = await utils.waitForElement('#camera-btn');
|
||||
// await muteVideoButton.click();
|
||||
// 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)`);
|
||||
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
// expect(await utils.isPresent('#status-mic')).toBeFalse();
|
||||
// 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.');
|
||||
|
||||
// // Checking if audio is muted after join the room
|
||||
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
|
||||
// expect(isAudioEnabled).toBeTrue();
|
||||
// Switch back to A's tab to verify
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
await browser.sleep(500);
|
||||
|
||||
// // Mute audio
|
||||
// const muteAudioButton = await utils.waitForElement('#mic-btn');
|
||||
// await muteAudioButton.click();
|
||||
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
|
||||
console.log(`[Tab A] After C joins: ${pinnedCountA2} pinned stream(s)`);
|
||||
|
||||
// await utils.waitForElement('#status-mic');
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
|
||||
|
||||
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
|
||||
// expect(isAudioEnabled).toBeFalse();
|
||||
|
||||
// // Unmute camera
|
||||
// await muteVideoButton.click();
|
||||
|
||||
// await utils.waitForElement('.camera-type');
|
||||
// expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(2);
|
||||
|
||||
// isAudioEnabled = await browser.executeScript(getAudioScript('camera-type'));
|
||||
// expect(isAudioEnabled).toBeFalse();
|
||||
// });
|
||||
// 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' : '',
|
||||
browserName: 'chrome',
|
||||
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
|
||||
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
|
||||
browserOptions: new chrome.Options()
|
||||
.addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments)) as chrome.Options
|
||||
};
|
||||
|
||||
export const NestedConfig: BrowserConfig = {
|
||||
|
|
@ -64,13 +65,14 @@ export const NestedConfig: BrowserConfig = {
|
|||
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
|
||||
browserName: 'Chrome',
|
||||
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
|
||||
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
|
||||
browserOptions: new chrome.Options()
|
||||
.addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments)) as chrome.Options
|
||||
};
|
||||
|
||||
export function getBrowserOptionsWithoutDevices() {
|
||||
if (LAUNCH_MODE === 'CI') {
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI);
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI) as chrome.Options;
|
||||
} else {
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices);
|
||||
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices) as chrome.Options;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,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 { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
|||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1); //screen sharse video
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1); //screen share audio
|
||||
|
|
@ -106,7 +106,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
|||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(2); //screen share audio and local audio
|
||||
|
|
@ -221,7 +221,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
|||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(3);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(1);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(1); // screen share audios
|
||||
|
|
@ -260,7 +260,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
|||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(1000);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(3);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(3);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(3); // screen share audios and local audio and remote audio
|
||||
|
|
@ -304,7 +304,7 @@ describe('Stream rendering and media toggling scenarios', () => {
|
|||
|
||||
await utils.clickOn('#screenshare-btn');
|
||||
await browser.sleep(500);
|
||||
await utils.waitForElement('#local-element-screen_share');
|
||||
await utils.waitForElement('.local_participant.OV_screen');
|
||||
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(4);
|
||||
expect(await utils.getNumberOfElements('video')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('audio')).toEqual(2); // screen share audios
|
||||
|
|
@ -681,12 +681,47 @@ describe('Stream UI controls and interaction features', () => {
|
|||
|
||||
await browser.switchTo().window(tabs[0]);
|
||||
|
||||
await utils.waitForElement('.OV_stream.remote.speaking');
|
||||
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
|
||||
// Wait with retries for audio detection to appear (handles timing issues)
|
||||
const maxRetries = 5;
|
||||
const retryInterval = 1000;
|
||||
let audioDetected = false;
|
||||
|
||||
// Check only one element is marked as speaker due to the local participant is muted
|
||||
await utils.waitForElement('.OV_stream.speaking');
|
||||
expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
|
||||
for (let i = 0; i < maxRetries && !audioDetected; i++) {
|
||||
await browser.sleep(retryInterval);
|
||||
const remoteSpeakingCount = await utils.getNumberOfElements('.OV_stream.remote.speaking');
|
||||
if (remoteSpeakingCount >= 1) {
|
||||
audioDetected = true;
|
||||
console.log(`[Audio Detection] Detected after ${i + 1} attempt(s)`);
|
||||
} else {
|
||||
console.log(`[Audio Detection] Attempt ${i + 1}/${maxRetries}: No audio detected yet`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure at least one remote speaker element is present (timing-sensitive)
|
||||
expect(audioDetected).toBeTrue();
|
||||
if (!audioDetected) {
|
||||
console.error('Audio detection indicator did not appear within timeout');
|
||||
}
|
||||
|
||||
// The local participant is muted; poll briefly to ensure the local stream is not
|
||||
// marked as speaking. This handles timing races where classes may be applied
|
||||
// or removed slightly later.
|
||||
const timeout = 2000;
|
||||
const interval = 200;
|
||||
const start = Date.now();
|
||||
let localNotSpeaking = false;
|
||||
while (Date.now() - start < timeout) {
|
||||
const localCount = await utils.getNumberOfElements('.OV_stream.local.speaking');
|
||||
if (localCount === 0) {
|
||||
localNotSpeaking = true;
|
||||
break;
|
||||
}
|
||||
await browser.sleep(interval);
|
||||
}
|
||||
expect(localNotSpeaking).toBeTrue();
|
||||
if (!localNotSpeaking) {
|
||||
console.error('Local stream should not be marked as speaking when muted');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
|
||||
import * as fs from 'fs';
|
||||
import { PNG } from 'pngjs';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
import { PNG } from 'pngjs';
|
||||
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
|
||||
type PNGWithMetadata = PNG & { data: Buffer };
|
||||
|
||||
export class OpenViduComponentsPO {
|
||||
|
|
@ -279,4 +279,50 @@ export class OpenViduComponentsPO {
|
|||
// fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
||||
// expect(numDiffPixels).to.be.greaterThan(500, 'The virtual background was not applied correctly');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins or unpins a stream by clicking on it
|
||||
* @param streamSelector CSS selector for the stream element (e.g., '.screen-type', '.camera-type')
|
||||
*/
|
||||
async toggleStreamPin(streamSelector: string): Promise<void> {
|
||||
const stream = await this.waitForElement(streamSelector);
|
||||
await stream.click();
|
||||
await this.clickOn('#pin-btn');
|
||||
await this.browser.sleep(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of pinned streams (elements with class .OV_big)
|
||||
*/
|
||||
async getNumberOfPinnedStreams(): Promise<number> {
|
||||
return await this.getNumberOfElements('.OV_big');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific stream is pinned
|
||||
* @param streamSelector CSS selector for the stream element
|
||||
*/
|
||||
async isStreamPinned(streamSelector: string): Promise<boolean> {
|
||||
try {
|
||||
const stream = await this.waitForElement(streamSelector);
|
||||
const classes = await stream.getAttribute('class');
|
||||
return classes.includes('OV_big');
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all screen share streams
|
||||
*/
|
||||
async getScreenShareStreams(): Promise<WebElement[]> {
|
||||
return await this.browser.findElements(By.css('.screen-type'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all camera streams
|
||||
*/
|
||||
async getCameraStreams(): Promise<WebElement[]> {
|
||||
return await this.browser.findElements(By.css('.camera-type'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,108 +1,110 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.4",
|
||||
"@angular/cdk": "20.2.8",
|
||||
"@angular/common": "20.3.4",
|
||||
"@angular/core": "20.3.4",
|
||||
"@angular/forms": "20.3.4",
|
||||
"@angular/material": "20.2.8",
|
||||
"@angular/platform-browser": "20.3.4",
|
||||
"@angular/platform-browser-dynamic": "20.3.4",
|
||||
"@angular/router": "20.3.4",
|
||||
"@livekit/track-processors": "^0.5.6",
|
||||
"@types/dom-mediacapture-transform": "^0.1.11",
|
||||
"autolinker": "4.0.0",
|
||||
"livekit-client": "2.11.4",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.7.0",
|
||||
"zone.js": "^0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "20.3.5",
|
||||
"@angular/cli": "20.3.5",
|
||||
"@angular/compiler": "20.3.4",
|
||||
"@angular/compiler-cli": "20.3.4",
|
||||
"@compodoc/compodoc": "^1.1.25",
|
||||
"@types/jasmine": "^5.1.4",
|
||||
"@types/node": "^20.12.14",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/selenium-webdriver": "4.1.16",
|
||||
"@types/ws": "^8.5.12",
|
||||
"chromedriver": "141.0.1",
|
||||
"concat": "^1.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "^9.1.6",
|
||||
"jasmine": "^5.3.1",
|
||||
"jasmine-core": "5.3.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"karma": "^6.4.4",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage": "^2.2.1",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-notify-reporter": "1.3.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"ng-packagr": "20.3.0",
|
||||
"npm-watch": "^0.13.0",
|
||||
"pixelmatch": "^7.1.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"selenium-webdriver": "4.36.0",
|
||||
"ts-node": "10.9.2",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "5.8.3",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"name": "openvidu-components-testapp",
|
||||
"private": true,
|
||||
"watch": {
|
||||
"doc:serve": {
|
||||
"patterns": [
|
||||
"projects",
|
||||
"src"
|
||||
],
|
||||
"extensions": "ts,html,scss,css,md",
|
||||
"quiet": false
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ng serve --configuration development --open",
|
||||
"start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200",
|
||||
"start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080",
|
||||
"build": "ng build openvidu-components-testapp --configuration production",
|
||||
"doc:build": "npx compodoc -c ./projects/openvidu-components-angular/doc/.compodocrc.json",
|
||||
"doc:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js",
|
||||
"doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js",
|
||||
"doc:clean-copy": "rm -rf ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular && cp -r ./docs/openvidu-components-angular/ ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular",
|
||||
"doc:serve": "npx compodoc -c ../openvidu-components-angular/projects/openvidu-components-angular/doc/.compodocrc.json --serve --port 7000",
|
||||
"doc:serve-watch": "npm-watch doc:serve",
|
||||
"lib:serve": "ng build openvidu-components-angular --watch",
|
||||
"lib:build": "ng build openvidu-components-angular --configuration production",
|
||||
"lib:pack": "cd ./dist/openvidu-components-angular && npm pack",
|
||||
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/frontend",
|
||||
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
|
||||
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
|
||||
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
|
||||
"e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js",
|
||||
"e2e:nested-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js",
|
||||
"e2e:lib-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/api-directives.test.js",
|
||||
"e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
|
||||
"e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
|
||||
"e2e:lib-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
|
||||
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
|
||||
"e2e:lib-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/panels.test.js",
|
||||
"e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
|
||||
"e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
|
||||
"e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
|
||||
"e2e:lib-virtual-backgrounds": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/virtual-backgrounds.test.js",
|
||||
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
|
||||
"husky": "cd .. && husky install"
|
||||
},
|
||||
"version": "3.4.0"
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.15",
|
||||
"@angular/cdk": "20.2.14",
|
||||
"@angular/common": "20.3.15",
|
||||
"@angular/core": "20.3.15",
|
||||
"@angular/forms": "20.3.15",
|
||||
"@angular/material": "20.2.14",
|
||||
"@angular/platform-browser": "20.3.15",
|
||||
"@angular/platform-browser-dynamic": "20.3.15",
|
||||
"@angular/router": "20.3.15",
|
||||
"@livekit/track-processors": "0.7.0",
|
||||
"@types/dom-mediacapture-transform": "0.1.11",
|
||||
"autolinker": "4.1.5",
|
||||
"livekit-client": "2.16.0",
|
||||
"rxjs": "7.8.2",
|
||||
"tslib": "2.8.1",
|
||||
"zone.js": "0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "20.3.13",
|
||||
"@angular/cli": "20.3.13",
|
||||
"@angular/compiler": "20.3.15",
|
||||
"@angular/compiler-cli": "20.3.15",
|
||||
"@compodoc/compodoc": "1.1.32",
|
||||
"@types/jasmine": "5.1.13",
|
||||
"@types/node": "20.19.26",
|
||||
"@types/pngjs": "6.0.5",
|
||||
"@types/selenium-webdriver": "4.1.29",
|
||||
"@types/ws": "8.5.14",
|
||||
"chromedriver": "143.0.0",
|
||||
"concat": "1.0.3",
|
||||
"cpx": "1.5.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "9.1.6",
|
||||
"jasmine": "5.3.1",
|
||||
"jasmine-core": "5.3.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"karma": "6.4.4",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage": "2.2.1",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.1.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-notify-reporter": "1.3.0",
|
||||
"lint-staged": "15.2.10",
|
||||
"ng-packagr": "20.3.0",
|
||||
"npm-watch": "0.13.0",
|
||||
"pixelmatch": "7.1.0",
|
||||
"pngjs": "7.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "6.0.1",
|
||||
"selenium-webdriver": "4.39.0",
|
||||
"ts-node": "10.9.2",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "5.8.3",
|
||||
"webpack-bundle-analyzer": "4.10.2"
|
||||
},
|
||||
"name": "openvidu-components-testapp",
|
||||
"private": true,
|
||||
"watch": {
|
||||
"doc:serve": {
|
||||
"patterns": [
|
||||
"projects",
|
||||
"src"
|
||||
],
|
||||
"extensions": "ts,html,scss,css,md",
|
||||
"quiet": false
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ng serve --configuration development --open",
|
||||
"start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200",
|
||||
"start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080",
|
||||
"build": "ng build openvidu-components-testapp --configuration production",
|
||||
"doc:build": "npx compodoc -c ./projects/openvidu-components-angular/doc/.compodocrc.json",
|
||||
"doc:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js",
|
||||
"doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js",
|
||||
"doc:clean-copy": "rm -rf ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular && cp -r ./docs/openvidu-components-angular/ ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular",
|
||||
"doc:serve": "npx compodoc -c ../openvidu-components-angular/projects/openvidu-components-angular/doc/.compodocrc.json --serve --port 7000",
|
||||
"doc:serve-watch": "npm-watch doc:serve",
|
||||
"lib:serve": "ng build openvidu-components-angular --watch",
|
||||
"lib:build": "ng build openvidu-components-angular --configuration production && rimraf dist/openvidu-components-angular && cpx \"projects/openvidu-components-angular/dist/**/*\" dist/openvidu-components-angular",
|
||||
"lib:pack": "cd ./dist/openvidu-components-angular && npm pack",
|
||||
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/frontend",
|
||||
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
|
||||
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
|
||||
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
|
||||
"e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js",
|
||||
"e2e:nested-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js",
|
||||
"e2e:lib-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/api-directives.test.js",
|
||||
"e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
|
||||
"e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
|
||||
"e2e:lib-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
|
||||
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
|
||||
"e2e:lib-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/panels.test.js",
|
||||
"e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
|
||||
"e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
|
||||
"e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
|
||||
"e2e:lib-virtual-backgrounds": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/virtual-backgrounds.test.js",
|
||||
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
|
||||
"husky": "cd .. && husky install"
|
||||
},
|
||||
"version": "3.4.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/openvidu-components-angular",
|
||||
"dest": "./dist",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,418 +0,0 @@
|
|||
{
|
||||
"name": "openvidu-components-angular",
|
||||
"version": "3.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",
|
||||
"main": "dist/fesm2022/openvidu-components-angular.mjs",
|
||||
"module": "dist/fesm2022/openvidu-components-angular.mjs",
|
||||
"typings": "dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
|
|
@ -12,8 +15,8 @@
|
|||
"@angular/material": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"autolinker": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"livekit-client": "^2.15.0",
|
||||
"@livekit/track-processors": "^0.6.0"
|
||||
"livekit-client": "^2.16.0",
|
||||
"@livekit/track-processors": "^0.7.0"
|
||||
},
|
||||
"version": "3.4.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import { Component } from '@angular/core';
|
|||
*/
|
||||
@Component({
|
||||
selector: 'ov-audio-wave',
|
||||
template: `<div class="audio-container">
|
||||
<div class="stick normal play"></div>
|
||||
<div class="stick loud play"></div>
|
||||
<div class="stick normal play"></div>
|
||||
</div>`,
|
||||
template: `
|
||||
<div class="audio-container audio-wave-indicator">
|
||||
<div class="stick normal play"></div>
|
||||
<div class="stick loud play"></div>
|
||||
<div class="stick normal play"></div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./audio-wave.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*ngFor="let track of localParticipant.tracks; trackBy: trackParticipantElement"
|
||||
[ngClass]="{
|
||||
local_participant: true,
|
||||
OV_root: !track.isAudioTrack && !track.isMinimized,
|
||||
OV_publisher: !track.isAudioTrack && !track.isMinimized,
|
||||
OV_minimized: track.isMinimized,
|
||||
OV_big: track.isPinned,
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
|
||||
OV_screen: track.isScreenTrack
|
||||
}"
|
||||
[id]="'local-element-' + track.source"
|
||||
[id]="'participant-' + track.participant.identity"
|
||||
cdkDrag
|
||||
cdkDragBoundary=".layout"
|
||||
[cdkDragDisabled]="!track.isMinimized"
|
||||
|
|
@ -27,11 +29,13 @@
|
|||
<div
|
||||
*ngFor="let track of remoteParticipants | tracks; trackBy: trackParticipantElement"
|
||||
class="remote-participant"
|
||||
[id]="'participant-' + track.participant.identity"
|
||||
[ngClass]="{
|
||||
OV_root: !track.isAudioTrack,
|
||||
OV_publisher: !track.isAudioTrack,
|
||||
OV_big: track.isPinned,
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
|
||||
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
|
||||
OV_screen: track.isScreenTrack
|
||||
}"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ import { Track } from 'livekit-client';
|
|||
@Component({
|
||||
selector: 'ov-media-element',
|
||||
template: `
|
||||
<ov-avatar-profile @posterAnimation *ngIf="showAvatar" [name]="avatarName" [color]="avatarColor"></ov-avatar-profile>
|
||||
<ov-video-poster
|
||||
@posterAnimation
|
||||
[showAvatar]="showAvatar"
|
||||
[nickname]="avatarName"
|
||||
[color]="avatarColor"
|
||||
[hasEncryptionError]="hasEncryptionError"
|
||||
></ov-video-poster>
|
||||
<video #videoElement *ngIf="_track?.kind === 'video'" class="OV_video-element" [attr.id]="_track?.sid"></video>
|
||||
<audio #audioElement *ngIf="_track?.kind === 'audio'" [attr.id]="_track?.sid"></audio>
|
||||
`,
|
||||
|
|
@ -29,10 +35,11 @@ export class MediaElementComponent implements AfterViewInit, OnDestroy {
|
|||
private _muted: boolean = false;
|
||||
private previousTrack: Track | null = null;
|
||||
|
||||
@Input() showAvatar: boolean;
|
||||
@Input() avatarColor: string;
|
||||
@Input() avatarName: string;
|
||||
@Input() isLocal: boolean;
|
||||
@Input() showAvatar: boolean = false;
|
||||
@Input() avatarColor: string = '#000000';
|
||||
@Input() avatarName: string = 'User';
|
||||
@Input() isLocal: boolean = false;
|
||||
@Input() hasEncryptionError: boolean = false;
|
||||
|
||||
@ViewChild('videoElement', { static: false })
|
||||
set videoElement(element: ElementRef) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="panel-container" id="activities-container">
|
||||
<div class="panel-header-container">
|
||||
<h3 class="panel-title">Activities</h3>
|
||||
<h3 class="panel-title">{{ 'PANEL.ACTIVITIES.TITLE' | translate }}</h3>
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
|
|||
}
|
||||
|
||||
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;
|
||||
if (this.panelService.isChatPanelOpened()) {
|
||||
this.scrollToBottom();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<mat-list>
|
||||
<mat-list-item>
|
||||
<!-- 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 -->
|
||||
<div
|
||||
class="participant-avatar"
|
||||
[style.background-color]="_participant?.colorProfile"
|
||||
[attr.aria-label]="'Avatar for ' + participantDisplayName"
|
||||
>
|
||||
<mat-icon>person</mat-icon>
|
||||
<mat-icon>{{ _participant.hasEncryptionError ? 'lock_person' : 'person' }}</mat-icon>
|
||||
</div>
|
||||
|
||||
<!-- Content section with name and status -->
|
||||
|
|
@ -28,42 +28,48 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participant-subtitle">
|
||||
<span class="status-indicator">
|
||||
{{ _participant | tracksPublishedTypes }}
|
||||
</span>
|
||||
</div>
|
||||
@if (!_participant.hasEncryptionError) {
|
||||
<div class="participant-subtitle">
|
||||
<span class="status-indicator">
|
||||
{{ _participant | tracksPublishedTypes }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Action buttons section -->
|
||||
<div class="participant-action-buttons">
|
||||
<!-- Mute/Unmute button for remote participants -->
|
||||
<button
|
||||
mat-icon-button
|
||||
id="mute-btn"
|
||||
*ngIf="!isLocalParticipant && showMuteButton"
|
||||
[class.warn-btn]="_participant?.isMutedForcibly"
|
||||
(click)="toggleMuteForcibly()"
|
||||
[disabled]="!_participant"
|
||||
[disableRipple]="true"
|
||||
[attr.aria-label]="
|
||||
_participant?.isMutedForcibly
|
||||
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
|
||||
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
||||
"
|
||||
[matTooltip]="
|
||||
_participant?.isMutedForcibly ? ('PANEL.PARTICIPANTS.UNMUTE' | translate) : ('PANEL.PARTICIPANTS.MUTE' | translate)
|
||||
"
|
||||
>
|
||||
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
|
||||
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
|
||||
</button>
|
||||
@if (!_participant.hasEncryptionError) {
|
||||
<div class="participant-action-buttons">
|
||||
<!-- Mute/Unmute button for remote participants -->
|
||||
<button
|
||||
mat-icon-button
|
||||
id="mute-btn"
|
||||
*ngIf="!isLocalParticipant && showMuteButton"
|
||||
[class.warn-btn]="_participant?.isMutedForcibly"
|
||||
(click)="toggleMuteForcibly()"
|
||||
[disabled]="!_participant"
|
||||
[disableRipple]="true"
|
||||
[attr.aria-label]="
|
||||
_participant?.isMutedForcibly
|
||||
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
|
||||
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
||||
"
|
||||
[matTooltip]="
|
||||
_participant?.isMutedForcibly
|
||||
? ('PANEL.PARTICIPANTS.UNMUTE' | translate)
|
||||
: ('PANEL.PARTICIPANTS.MUTE' | translate)
|
||||
"
|
||||
>
|
||||
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
|
||||
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- External item elements with improved structure -->
|
||||
<div class="external-elements" *ngIf="hasExternalElements">
|
||||
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
||||
<!-- External item elements with improved structure -->
|
||||
<div class="external-elements" *ngIf="hasExternalElements">
|
||||
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,12 @@
|
|||
</mat-list>
|
||||
</div>
|
||||
}
|
||||
<!-- Additional elements injected via directive -->
|
||||
@if (generalAdditionalElementsTemplate) {
|
||||
<div class="additional-elements-section">
|
||||
<ng-container *ngTemplateOutlet="generalAdditionalElementsTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
|
||||
<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 { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model';
|
||||
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 { CustomDevice } from '../../../models/device.model';
|
||||
import { LangOption } from '../../../models/lang.model';
|
||||
import { SettingsPanelGeneralAdditionalElementsDirective } from '../../../directives/template/internals.directive';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
@ -23,6 +24,14 @@ export class SettingsPanelComponent implements OnInit {
|
|||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onLangChanged = new EventEmitter<LangOption>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* ContentChild for custom elements in general section
|
||||
*/
|
||||
@ContentChild(SettingsPanelGeneralAdditionalElementsDirective)
|
||||
externalGeneralAdditionalElements!: SettingsPanelGeneralAdditionalElementsDirective;
|
||||
|
||||
settingsOptions: typeof PanelSettingsOptions = PanelSettingsOptions;
|
||||
selectedOption: PanelSettingsOptions = PanelSettingsOptions.GENERAL;
|
||||
showCameraButton: boolean = true;
|
||||
|
|
@ -32,6 +41,14 @@ export class SettingsPanelComponent implements OnInit {
|
|||
isMobile: boolean = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Gets the template for additional elements in general section
|
||||
*/
|
||||
get generalAdditionalElementsTemplate(): TemplateRef<any> | undefined {
|
||||
return this.externalGeneralAdditionalElements?.template;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private panelService: PanelService,
|
||||
private platformService: PlatformService,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { ActionService } from '../../services/action/action.service';
|
||||
import { ActionServiceMock } from '../../services/action/action.service.mock';
|
||||
import { ActionServiceMock } from '../../../test-helpers/action.service.mock';
|
||||
|
||||
import { ChatService } from '../../services/chat/chat.service';
|
||||
import { ChatServiceMock } from '../../services/chat/chat.service.mock';
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '.
|
|||
import { RecordingStatus } from '../../models/recording.model';
|
||||
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
|
||||
import { ViewportService } from '../../services/viewport/viewport.service';
|
||||
import { E2eeService } from '../../services/e2ee/e2ee.service';
|
||||
import { safeJsonParse } from '../../utils/utils';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
@ -138,7 +140,8 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
private backgroundService: VirtualBackgroundService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private templateManagerService: TemplateManagerService,
|
||||
protected viewportService: ViewportService
|
||||
protected viewportService: ViewportService,
|
||||
private e2eeService: E2eeService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('SessionComponent');
|
||||
this.setupTemplates();
|
||||
|
|
@ -230,7 +233,8 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
// this.subscribeToCaptionLanguage();
|
||||
this.subcribeToActiveSpeakersChanged();
|
||||
this.subscribeToEncryptionErrors();
|
||||
this.subscribeToActiveSpeakersChanged();
|
||||
this.subscribeToParticipantConnected();
|
||||
this.subscribeToTrackSubscribed();
|
||||
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);
|
||||
}
|
||||
}
|
||||
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.participantService.setSpeaking(speakers);
|
||||
});
|
||||
|
|
@ -450,81 +465,134 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
private subscribeToDataMessage() {
|
||||
this.room.on(
|
||||
RoomEvent.DataReceived,
|
||||
(payload: Uint8Array, participant?: RemoteParticipant, _?: DataPacket_Kind, topic?: string) => {
|
||||
const event = JSON.parse(new TextDecoder().decode(payload));
|
||||
this.log.d(`Data event received: ${topic}`);
|
||||
switch (topic) {
|
||||
case DataTopic.CHAT:
|
||||
const participantName = participant?.name || 'Unknown';
|
||||
this.chatService.addRemoteMessage(event.message, participantName);
|
||||
break;
|
||||
case DataTopic.RECORDING_STARTING:
|
||||
this.log.d('Recording is starting', event);
|
||||
this.recordingService.setRecordingStarting();
|
||||
break;
|
||||
case DataTopic.RECORDING_STARTED:
|
||||
this.log.d('Recording has been started', event);
|
||||
this.recordingService.setRecordingStarted(event);
|
||||
break;
|
||||
case DataTopic.RECORDING_STOPPING:
|
||||
this.log.d('Recording is stopping', event);
|
||||
this.recordingService.setRecordingStopping();
|
||||
break;
|
||||
case DataTopic.RECORDING_STOPPED:
|
||||
this.log.d('RECORDING_STOPPED', event);
|
||||
this.recordingService.setRecordingStopped(event);
|
||||
break;
|
||||
async (payload: Uint8Array, participant?: RemoteParticipant, _?: DataPacket_Kind, topic?: string) => {
|
||||
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;
|
||||
}
|
||||
|
||||
case DataTopic.RECORDING_DELETED:
|
||||
this.log.d('RECORDING_DELETED', event);
|
||||
this.recordingService.deleteRecording(event);
|
||||
break;
|
||||
const participantIdentity = storedParticipant?.identity || '';
|
||||
const participantName = storedParticipant?.name || '';
|
||||
|
||||
case DataTopic.RECORDING_FAILED:
|
||||
this.log.d('RECORDING_FAILED', event);
|
||||
this.recordingService.setRecordingFailed(event.error);
|
||||
break;
|
||||
if (this.e2eeService.isEnabled) {
|
||||
payload = await this.decryptIfNeeded(topic, payload, participantIdentity);
|
||||
}
|
||||
|
||||
case DataTopic.BROADCASTING_STARTING:
|
||||
this.broadcastingService.setBroadcastingStarting();
|
||||
break;
|
||||
case DataTopic.BROADCASTING_STARTED:
|
||||
this.log.d('Broadcasting has been started', event);
|
||||
this.broadcastingService.setBroadcastingStarted(event);
|
||||
break;
|
||||
const rawText = decoder.decode(payload);
|
||||
this.log.d('DataReceived (raw)', { topic });
|
||||
|
||||
case DataTopic.BROADCASTING_STOPPING:
|
||||
this.broadcastingService.setBroadcastingStopping();
|
||||
break;
|
||||
case DataTopic.BROADCASTING_STOPPED:
|
||||
this.broadcastingService.setBroadcastingStopped();
|
||||
break;
|
||||
const eventMessage = safeJsonParse(rawText);
|
||||
if (!eventMessage) {
|
||||
this.log.w('Discarding data: malformed JSON', rawText);
|
||||
return;
|
||||
}
|
||||
|
||||
case DataTopic.BROADCASTING_FAILED:
|
||||
this.broadcastingService.setBroadcastingFailed(event.error);
|
||||
break;
|
||||
this.log.d(`Data event received: ${topic}`);
|
||||
|
||||
case DataTopic.ROOM_STATUS:
|
||||
const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData;
|
||||
|
||||
if (this.libService.showRecordingActivityRecordingsList()) {
|
||||
this.recordingService.setRecordingList(recordingList);
|
||||
}
|
||||
if (isRecordingStarted) {
|
||||
const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED);
|
||||
this.recordingService.setRecordingStarted(recordingActive);
|
||||
}
|
||||
if (isBroadcastingStarted) {
|
||||
this.broadcastingService.setBroadcastingStarted(broadcastingId);
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
// 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) {
|
||||
case DataTopic.CHAT:
|
||||
this.chatService.addRemoteMessage(event.message, participantName);
|
||||
break;
|
||||
case DataTopic.RECORDING_STARTING:
|
||||
this.log.d('Recording is starting', event);
|
||||
this.recordingService.setRecordingStarting();
|
||||
break;
|
||||
case DataTopic.RECORDING_STARTED:
|
||||
this.log.d('Recording has been started', event);
|
||||
this.recordingService.setRecordingStarted(event);
|
||||
break;
|
||||
case DataTopic.RECORDING_STOPPING:
|
||||
this.log.d('Recording is stopping', event);
|
||||
this.recordingService.setRecordingStopping();
|
||||
break;
|
||||
case DataTopic.RECORDING_STOPPED:
|
||||
this.log.d('RECORDING_STOPPED', event);
|
||||
this.recordingService.setRecordingStopped(event);
|
||||
break;
|
||||
|
||||
case DataTopic.RECORDING_DELETED:
|
||||
this.log.d('RECORDING_DELETED', event);
|
||||
this.recordingService.deleteRecording(event);
|
||||
break;
|
||||
|
||||
case DataTopic.RECORDING_FAILED:
|
||||
this.log.d('RECORDING_FAILED', event);
|
||||
this.recordingService.setRecordingFailed(event.error);
|
||||
break;
|
||||
|
||||
case DataTopic.BROADCASTING_STARTING:
|
||||
this.broadcastingService.setBroadcastingStarting();
|
||||
break;
|
||||
case DataTopic.BROADCASTING_STARTED:
|
||||
this.log.d('Broadcasting has been started', event);
|
||||
this.broadcastingService.setBroadcastingStarted(event);
|
||||
break;
|
||||
|
||||
case DataTopic.BROADCASTING_STOPPING:
|
||||
this.broadcastingService.setBroadcastingStopping();
|
||||
break;
|
||||
case DataTopic.BROADCASTING_STOPPED:
|
||||
this.broadcastingService.setBroadcastingStopped();
|
||||
break;
|
||||
|
||||
case DataTopic.BROADCASTING_FAILED:
|
||||
this.broadcastingService.setBroadcastingFailed(event.error);
|
||||
break;
|
||||
|
||||
case DataTopic.ROOM_STATUS:
|
||||
const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData;
|
||||
|
||||
if (this.libService.showRecordingActivityRecordingsList()) {
|
||||
this.recordingService.setRecordingList(recordingList);
|
||||
}
|
||||
if (isRecordingStarted) {
|
||||
const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED);
|
||||
this.recordingService.setRecordingStarted(recordingActive);
|
||||
}
|
||||
if (isBroadcastingStarted) {
|
||||
this.broadcastingService.setBroadcastingStarted(broadcastingId);
|
||||
}
|
||||
|
||||
default:
|
||||
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() {
|
||||
this.room.on(RoomEvent.Reconnecting, () => {
|
||||
this.log.w('Connection lost: Reconnecting');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="input-wrapper">
|
||||
<mat-icon class="input-icon">person</mat-icon>
|
||||
<input
|
||||
id="name-input"
|
||||
id="participant-name-input"
|
||||
type="text"
|
||||
maxlength="20"
|
||||
[(ngModel)]="name"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
#streamContainer
|
||||
>
|
||||
<div
|
||||
*ngIf="!isMinimal && showParticipantName"
|
||||
*ngIf="!isMinimal && showParticipantName && !_track.isAudioTrack || (_track.isAudioTrack && _track.participant.onlyHasAudioTracks)"
|
||||
id="participant-name-container"
|
||||
class="participant-name"
|
||||
[class.fullscreen]="isFullscreen"
|
||||
|
|
@ -34,47 +34,50 @@
|
|||
[avatarName]="_track.participant.name"
|
||||
[muted]="_track.isMutedForcibly"
|
||||
[isLocal]="_track.participant.isLocal"
|
||||
[hasEncryptionError]="_track.participant.hasEncryptionError"
|
||||
></ov-media-element>
|
||||
|
||||
<div class="status-icons">
|
||||
<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-pinned" fontIcon="push_pin" *ngIf="_track.isPinned"></mat-icon>
|
||||
</div>
|
||||
|
||||
<div class="stream-video-controls" *ngIf="!isMinimal && showVideoControls && mouseHovering">
|
||||
<div class="flex-container">
|
||||
<button
|
||||
mat-icon-button
|
||||
id="pin-btn"
|
||||
(click)="toggleVideoPinned()"
|
||||
[matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)"
|
||||
>
|
||||
<mat-icon *ngIf="_track.isPinned" fontSet="material-symbols-outlined" fontIcon="keep">keep_off</mat-icon>
|
||||
<mat-icon *ngIf="!_track.isPinned" id="status-pinned" fontIcon="push_pin"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!_track.participant.isLocal"
|
||||
mat-icon-button
|
||||
id="silence-btn"
|
||||
(click)="toggleMuteForcibly()"
|
||||
[class.muted-btn]="_track.isMutedForcibly"
|
||||
[matTooltip]="_track.isMutedForcibly ? ('STREAM.UNMUTE_SOUND' | translate) : ('STREAM.MUTE_SOUND' | translate)"
|
||||
>
|
||||
<mat-icon *ngIf="_track.isMutedForcibly">volume_off</mat-icon>
|
||||
<mat-icon *ngIf="!_track.isMutedForcibly">volume_up</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="_track.participant.isLocal"
|
||||
mat-icon-button
|
||||
id="minimize-btn"
|
||||
[disabled]="_track.isPinned"
|
||||
(click)="toggleMinimize()"
|
||||
[matTooltip]="_track.isMinimized ? ('STREAM.MAXIMIZE' | translate) : ('STREAM.MINIMIZE' | translate)"
|
||||
>
|
||||
<mat-icon *ngIf="_track.isMinimized">open_in_full</mat-icon>
|
||||
<mat-icon *ngIf="!_track.isMinimized">close_fullscreen</mat-icon>
|
||||
</button>
|
||||
@if (!_track.participant.hasEncryptionError) {
|
||||
<div class="status-icons">
|
||||
<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-pinned" fontIcon="push_pin" *ngIf="_track.isPinned"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stream-video-controls" *ngIf="!isMinimal && showVideoControls && mouseHovering">
|
||||
<div class="flex-container">
|
||||
<button
|
||||
mat-icon-button
|
||||
id="pin-btn"
|
||||
(click)="toggleVideoPinned()"
|
||||
[matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)"
|
||||
>
|
||||
<mat-icon *ngIf="_track.isPinned" fontSet="material-symbols-outlined" fontIcon="keep">keep_off</mat-icon>
|
||||
<mat-icon *ngIf="!_track.isPinned" id="status-pinned" fontIcon="push_pin"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!_track.participant.isLocal"
|
||||
mat-icon-button
|
||||
id="silence-btn"
|
||||
(click)="toggleMuteForcibly()"
|
||||
[class.muted-btn]="_track.isMutedForcibly"
|
||||
[matTooltip]="_track.isMutedForcibly ? ('STREAM.UNMUTE_SOUND' | translate) : ('STREAM.MUTE_SOUND' | translate)"
|
||||
>
|
||||
<mat-icon *ngIf="_track.isMutedForcibly">volume_off</mat-icon>
|
||||
<mat-icon *ngIf="!_track.isMutedForcibly">volume_up</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="_track.participant.isLocal"
|
||||
mat-icon-button
|
||||
id="minimize-btn"
|
||||
[disabled]="_track.isPinned"
|
||||
(click)="toggleMinimize()"
|
||||
[matTooltip]="_track.isMinimized ? ('STREAM.MAXIMIZE' | translate) : ('STREAM.MINIMIZE' | translate)"
|
||||
>
|
||||
<mat-icon *ngIf="_track.isMinimized">open_in_full</mat-icon>
|
||||
<mat-icon *ngIf="!_track.isMinimized">close_fullscreen</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,25 +47,28 @@
|
|||
.stream-video-controls {
|
||||
background-color: var(--ov-primary-action-color);
|
||||
border-radius: var(--ov-video-radius);
|
||||
backdrop-filter: blur(8px);
|
||||
width: fit-content;
|
||||
height: 50px;
|
||||
height: 44px;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
display: inline-grid;
|
||||
z-index: 9999;
|
||||
margin: auto;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
// border: 2px solid var(--ov-text-primary-color);
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
padding: 4px 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
button {
|
||||
color: var(--ov-text-primary-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-video-controls:hover {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(-2px);
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { Track } from 'livekit-client';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ParticipantTrackPublication } from '../../models/participant.model';
|
||||
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
|
||||
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
||||
import { LayoutService } from '../../services/layout/layout.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}.
|
||||
|
|
@ -93,7 +93,7 @@ export class StreamComponent implements OnInit, OnDestroy {
|
|||
|
||||
private _streamContainer: ElementRef;
|
||||
private destroy$ = new Subject<void>();
|
||||
private readonly HOVER_TIMEOUT = 3000;
|
||||
private readonly HOVER_TIMEOUT = 2000;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
|
|
|
|||
|
|
@ -177,6 +177,11 @@
|
|||
</ng-container>
|
||||
}
|
||||
|
||||
<!-- Additional menu items injected via directive -->
|
||||
@if (moreOptionsAdditionalMenuItemsTemplate) {
|
||||
<ng-container *ngTemplateOutlet="moreOptionsAdditionalMenuItemsTemplate"></ng-container>
|
||||
}
|
||||
|
||||
<!-- Divider before settings -->
|
||||
@if (showSettingsButton) {
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
|
@ -198,7 +203,7 @@
|
|||
}
|
||||
|
||||
<!-- Leave session button -->
|
||||
@if (showLeaveButtonDirect()) {
|
||||
@if (showLeaveButton) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(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 { BroadcastingStatus } from '../../../models/broadcasting.model';
|
||||
import { ToolbarAdditionalButtonsPosition } from '../../../models/toolbar.model';
|
||||
import { ViewportService } from '../../../services/viewport/viewport.service';
|
||||
import { ToolbarMoreOptionsAdditionalMenuItemsDirective } from '../../../directives/template/internals.directive';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
@ -71,6 +72,21 @@ export class ToolbarMediaButtonsComponent {
|
|||
// Leave button template
|
||||
@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
|
||||
_recordingStatus = RecordingStatus;
|
||||
_broadcastingStatus = BroadcastingStatus;
|
||||
|
|
@ -96,9 +112,6 @@ export class ToolbarMediaButtonsComponent {
|
|||
// More options button - always visible when not minimal
|
||||
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
|
||||
readonly hasActiveFeatures = computed(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,12 @@
|
|||
(captionsToggled)="onCaptionsToggle()"
|
||||
(settingsToggled)="toggleSettings()"
|
||||
(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>
|
||||
|
||||
<!-- Panel buttons -->
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.servic
|
|||
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
|
||||
import { Room, RoomEvent } from 'livekit-client';
|
||||
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}.
|
||||
|
|
@ -80,6 +80,28 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
| TemplateRef<any>
|
||||
| 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
|
||||
*/
|
||||
|
|
@ -494,7 +516,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
|
||||
this._externalAdditionalButtons,
|
||||
this._externalAdditionalPanelButtons,
|
||||
this._externalLeaveButton
|
||||
this._externalLeaveButton,
|
||||
this._externalMoreOptionsAdditionalMenuItems
|
||||
);
|
||||
|
||||
// Apply templates to component properties for backward compatibility
|
||||
|
|
@ -515,6 +538,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
if (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() {
|
||||
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()) {
|
||||
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-container *ngTemplateOutlet="openviduAngularToolbarLeaveButtonTemplate"></ng-container>
|
||||
</ng-template>
|
||||
|
||||
<!-- Inject additional menu items in toolbar more options -->
|
||||
<ng-container *ovToolbarMoreOptionsAdditionalMenuItems>
|
||||
<ng-container *ngTemplateOutlet="ovToolbarMoreOptionsAdditionalMenuItemsTemplate"></ng-container>
|
||||
</ng-container>
|
||||
</ov-toolbar>
|
||||
</ng-template>
|
||||
|
||||
|
|
@ -127,7 +132,11 @@
|
|||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="onAudioEnabledChanged.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 #activitiesPanel>
|
||||
|
|
|
|||
|
|
@ -62,9 +62,12 @@ import {
|
|||
LayoutAdditionalElementsDirective,
|
||||
ParticipantPanelAfterLocalParticipantDirective,
|
||||
PreJoinDirective,
|
||||
LeaveButtonDirective
|
||||
LeaveButtonDirective,
|
||||
SettingsPanelGeneralAdditionalElementsDirective,
|
||||
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||
} from '../../directives/template/internals.directive';
|
||||
import { OpenViduThemeService } from '../../services/theme/theme.service';
|
||||
import { E2eeService } from '../../services/e2ee/e2ee.service';
|
||||
|
||||
/**
|
||||
* The **VideoconferenceComponent** is the parent of all OpenVidu components.
|
||||
|
|
@ -374,6 +377,38 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
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
|
||||
*/
|
||||
|
|
@ -476,6 +511,14 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
* @internal
|
||||
*/
|
||||
ovLayoutAdditionalElementsTemplate: TemplateRef<any>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ovSettingsPanelGeneralAdditionalElementsTemplate: TemplateRef<any>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ovToolbarMoreOptionsAdditionalMenuItemsTemplate: TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
@ -712,7 +755,8 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
private actionService: ActionService,
|
||||
private libService: OpenViduComponentsConfigService,
|
||||
private templateManagerService: TemplateManagerService,
|
||||
private themeService: OpenViduThemeService
|
||||
private themeService: OpenViduThemeService,
|
||||
private e2eeService: E2eeService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('VideoconferenceComponent');
|
||||
|
||||
|
|
@ -784,7 +828,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
layout: this.externalLayout,
|
||||
stream: this.externalStream,
|
||||
preJoin: this.externalPreJoin,
|
||||
layoutAdditionalElements: this.externalLayoutAdditionalElements
|
||||
layoutAdditionalElements: this.externalLayoutAdditionalElements,
|
||||
settingsPanelGeneralAdditionalElements: this.externalSettingsPanelGeneralAdditionalElements,
|
||||
toolbarMoreOptionsAdditionalMenuItems: this.externalToolbarMoreOptionsAdditionalMenuItems
|
||||
};
|
||||
|
||||
const defaultTemplates: DefaultTemplates = {
|
||||
|
|
@ -859,6 +905,12 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
if (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) {
|
||||
this.latestParticipantName = name;
|
||||
this.latestParticipantName = await this.e2eeService.decrypt(name);
|
||||
this.storageSrv.setParticipantName(name);
|
||||
|
||||
// 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 { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CdkOverlayContainer extends OverlayContainer {
|
||||
private readonly cdkContainerClass: string = '.cdk-overlay-container';
|
||||
private defaultSelector = 'body';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import {
|
|||
RecordingActivityViewRecordingsButtonDirective,
|
||||
RecordingActivityShowRecordingsListDirective,
|
||||
ToolbarRoomNameDirective,
|
||||
ShowThemeSelectorDirective
|
||||
ShowThemeSelectorDirective,
|
||||
E2EEKeyDirective
|
||||
} from './internals.directive';
|
||||
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
|
||||
import {
|
||||
|
|
@ -113,7 +114,8 @@ const directives = [
|
|||
RecordingActivityViewRecordingsButtonDirective,
|
||||
RecordingActivityShowRecordingsListDirective,
|
||||
ToolbarRoomNameDirective,
|
||||
ShowThemeSelectorDirective
|
||||
ShowThemeSelectorDirective,
|
||||
E2EEKeyDirective
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ export class FallbackLogoDirective implements OnInit {
|
|||
standalone: false
|
||||
})
|
||||
export class LayoutRemoteParticipantsDirective {
|
||||
private _ovRemoteParticipants: ParticipantModel[] | undefined;
|
||||
|
||||
@Input() set ovRemoteParticipants(value: ParticipantModel[] | undefined) {
|
||||
this._ovRemoteParticipants = value;
|
||||
this.update(value);
|
||||
}
|
||||
constructor(
|
||||
|
|
@ -71,7 +74,7 @@ export class LayoutRemoteParticipantsDirective {
|
|||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.update(this.ovRemoteParticipants);
|
||||
this.update(this._ovRemoteParticipants);
|
||||
}
|
||||
|
||||
update(value: ParticipantModel[] | undefined) {
|
||||
|
|
@ -570,3 +573,51 @@ export class ShowThemeSelectorDirective implements AfterViewInit, OnDestroy {
|
|||
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
|
||||
*/
|
||||
update(value: string) {
|
||||
if (value) this.libService.updateGeneralConfig({ participantName: value });
|
||||
update(participantName: string) {
|
||||
if (participantName) {
|
||||
this.libService.updateGeneralConfig({ participantName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@
|
|||
* ```
|
||||
* <!--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';
|
||||
|
|
@ -308,3 +308,73 @@ export class ParticipantPanelParticipantBadgeDirective {
|
|||
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,
|
||||
ParticipantPanelParticipantBadgeDirective,
|
||||
PreJoinDirective,
|
||||
LeaveButtonDirective
|
||||
LeaveButtonDirective,
|
||||
SettingsPanelGeneralAdditionalElementsDirective,
|
||||
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||
} from './internals.directive';
|
||||
|
||||
@NgModule({
|
||||
|
|
@ -40,7 +42,9 @@ import {
|
|||
PreJoinDirective,
|
||||
ParticipantPanelAfterLocalParticipantDirective,
|
||||
LayoutAdditionalElementsDirective,
|
||||
ParticipantPanelParticipantBadgeDirective
|
||||
ParticipantPanelParticipantBadgeDirective,
|
||||
SettingsPanelGeneralAdditionalElementsDirective,
|
||||
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||
// BackgroundEffectsPanelDirective
|
||||
],
|
||||
exports: [
|
||||
|
|
@ -60,7 +64,9 @@ import {
|
|||
PreJoinDirective,
|
||||
ParticipantPanelAfterLocalParticipantDirective,
|
||||
LayoutAdditionalElementsDirective,
|
||||
ParticipantPanelParticipantBadgeDirective
|
||||
ParticipantPanelParticipantBadgeDirective,
|
||||
SettingsPanelGeneralAdditionalElementsDirective,
|
||||
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||
// BackgroundEffectsPanelDirective
|
||||
]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@
|
|||
"MESSAGE_SENT_NOTIFICATION": "消息已发送",
|
||||
"OPEN_CHAT": "打开"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "活动"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "参与者",
|
||||
"CAMERA": "摄像头",
|
||||
|
|
@ -175,6 +178,8 @@
|
|||
"MEDIA_ERR_GENERIC": "加载视频时发生错误。",
|
||||
"MEDIA_ERR_NETWORK": "网络错误导致视频下载中途失败。",
|
||||
"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",
|
||||
"OPEN_CHAT": "ÖFFNEN"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "Aktivitäten"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "Teilnehmer",
|
||||
"CAMERA": "KAMERA",
|
||||
|
|
@ -170,6 +173,8 @@
|
|||
"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_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",
|
||||
"UNMUTE": "Unmute"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "Activities"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Settings",
|
||||
"GENERAL": "General",
|
||||
|
|
@ -179,6 +182,8 @@
|
|||
"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_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",
|
||||
"OPEN_CHAT": "ABRIR"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "Actividades"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "Participantes",
|
||||
"CAMERA": "CÁMARA",
|
||||
|
|
@ -174,6 +177,8 @@
|
|||
"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_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é",
|
||||
"OPEN_CHAT": "OUVRIR"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "Activités"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "Participants",
|
||||
"CAMERA": "CAMÉRA",
|
||||
|
|
@ -175,6 +178,8 @@
|
|||
"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_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": "संदेश भेजा गया",
|
||||
"OPEN_CHAT": "खोलें"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "गतिविधियाँ"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "सदस्य",
|
||||
"CAMERA": "कैमरा",
|
||||
|
|
@ -175,6 +178,8 @@
|
|||
"MEDIA_ERR_GENERIC": "वीडियो लोड करते समय एक त्रुटि हुई।",
|
||||
"MEDIA_ERR_NETWORK": "नेटवर्क त्रुटि के कारण वीडियो डाउनलोड बीच में ही विफल हो गया।",
|
||||
"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",
|
||||
"OPEN_CHAT": "APRI"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "Attività"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "Partecipanti",
|
||||
"CAMERA": "CAMERA",
|
||||
|
|
@ -175,6 +178,8 @@
|
|||
"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_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": "メッセージを送信しました",
|
||||
"OPEN_CHAT": "開く"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "アクティビティ"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "参加者",
|
||||
"CAMERA": "カメラ",
|
||||
|
|
@ -175,6 +178,8 @@
|
|||
"MEDIA_ERR_GENERIC": "ビデオの読み込み中にエラーが発生しました。",
|
||||
"MEDIA_ERR_NETWORK": "ネットワークエラーにより、ビデオのダウンロードが途中で失敗しました。",
|
||||
"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",
|
||||
"OPEN_CHAT": "OPENEN"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "Activiteiten"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "Deelnemers",
|
||||
"CAMERA": "CAMERA",
|
||||
|
|
@ -175,6 +178,8 @@
|
|||
"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_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",
|
||||
"OPEN_CHAT": "ABRIR"
|
||||
},
|
||||
"ACTIVITIES": {
|
||||
"TITLE": "Atividades"
|
||||
},
|
||||
"PARTICIPANTS": {
|
||||
"TITLE": "Participantes",
|
||||
"CAMERA": "CÂMERA",
|
||||
|
|
@ -176,6 +179,8 @@
|
|||
"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_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 speaking: boolean = false;
|
||||
private customVideoTrack: Partial<ParticipantTrackPublication>;
|
||||
private _hasEncryptionError: boolean = false;
|
||||
private _decryptedName: string | undefined;
|
||||
|
||||
constructor(props: ParticipantProperties) {
|
||||
this.participant = props.participant;
|
||||
|
|
@ -170,8 +172,16 @@ export class ParticipantModel {
|
|||
* @returns string
|
||||
*/
|
||||
get name(): string | undefined {
|
||||
return this.participant.name;
|
||||
// return this.identity;
|
||||
return this._decryptedName ?? this.participant.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 { 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 { ProFeatureDialogTemplateComponent } from './components/dialogs/pro-feature-dialog.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 { ThemeSelectorComponent } from './components/settings/theme-selector/theme-selector.component';
|
||||
import { LandscapeWarningComponent } from './components/landscape-warning/landscape-warning.component';
|
||||
import { VideoPosterComponent } from './components/video-poster/video-poster.component';
|
||||
|
||||
const publicComponents = [
|
||||
AdminDashboardComponent,
|
||||
|
|
@ -74,7 +74,7 @@ const privateComponents = [
|
|||
ProFeatureDialogTemplateComponent,
|
||||
RecordingDialogComponent,
|
||||
DeleteDialogComponent,
|
||||
AvatarProfileComponent,
|
||||
VideoPosterComponent,
|
||||
MediaElementComponent,
|
||||
VideoDevicesComponent,
|
||||
AudioDevicesComponent,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { GlobalConfigService } from './services/config/global-config.service';
|
|||
import { OpenViduComponentsConfigService } from './services/config/directive-config.service';
|
||||
import { OpenViduComponentsUiModule } from './openvidu-components-angular-ui.module';
|
||||
import { ViewportService } from './services/viewport/viewport.service';
|
||||
import { E2eeService } from './services/e2ee/e2ee.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [OpenViduComponentsUiModule],
|
||||
|
|
@ -38,7 +39,7 @@ export class OpenViduComponentsModule {
|
|||
BroadcastingService,
|
||||
// CaptionService,
|
||||
CdkOverlayContainer,
|
||||
{ provide: OverlayContainer, useClass: CdkOverlayContainer },
|
||||
{ provide: OverlayContainer, useExisting: CdkOverlayContainer },
|
||||
ChatService,
|
||||
DeviceService,
|
||||
DocumentService,
|
||||
|
|
@ -52,6 +53,7 @@ export class OpenViduComponentsModule {
|
|||
StorageService,
|
||||
VirtualBackgroundService,
|
||||
ViewportService,
|
||||
E2eeService,
|
||||
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 { 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 { ActionService } from './action.service';
|
||||
import { TranslateService } from '../translate/translate.service';
|
||||
import { TranslateServiceMock } from '../translate/translate.service.mock';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatDialogMock } from '../../../test-helpers/action.service.mock';
|
||||
import { TranslateServiceMock } from '../../../test-helpers/translate.service.mock';
|
||||
|
||||
export class MatDialogMock {
|
||||
open() {
|
||||
return { close: () => {} } as MatDialogRef<any>;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ActionService', () => {
|
||||
describe('ActionService (characterization)', () => {
|
||||
let service: ActionService;
|
||||
let dialog: MatDialog;
|
||||
let dialog: MatDialogMock;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatSnackBarModule],
|
||||
providers: [
|
||||
{ provide: MatDialog, useClass: MatDialogMock },
|
||||
{ provide: TranslateService, useClass: TranslateServiceMock },
|
||||
{ provide: 'TranslateService', useClass: TranslateServiceMock },
|
||||
{ provide: 'OPENVIDU_COMPONENTS_CONFIG', useValue: { production: false } }
|
||||
]
|
||||
});
|
||||
|
||||
service = TestBed.inject(ActionService);
|
||||
dialog = TestBed.inject(MatDialog);
|
||||
dialog = TestBed.inject(MatDialog) as unknown as MatDialogMock;
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
it('opens a connection dialog when requested', () => {
|
||||
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(() => {
|
||||
const dialogSpy = spyOn(dialog, 'open').and.callThrough();
|
||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
||||
expect(dialogSpy).toHaveBeenCalled();
|
||||
expect(service['isConnectionDialogOpen']).toBeTrue();
|
||||
}));
|
||||
it('does not open a new dialog if one is already open (repeated calls)', () => {
|
||||
const spy = spyOn(dialog, 'open').and.callThrough();
|
||||
|
||||
it('should not open connection dialog if one is already open', () => {
|
||||
service['isConnectionDialogOpen'] = true;
|
||||
const dialogSpy = spyOn(dialog, 'open').and.callThrough();
|
||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
||||
service.openConnectionDialog('Title', 'Description', false);
|
||||
// repeated calls simulate concurrent/repeated user attempts
|
||||
service.openConnectionDialog('Title', '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(() => {
|
||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
||||
it('closes the opened dialog when requested and allows opening a new one afterwards', fakeAsync(() => {
|
||||
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();
|
||||
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(() => {
|
||||
// Spy on the dialog open method
|
||||
const dialogSpy = spyOn(dialog, 'open').and.callThrough();
|
||||
it('handles rapid consecutive calls by creating a single dialog (reentrancy protection)', fakeAsync(() => {
|
||||
const spy = spyOn(dialog, 'open').and.callThrough();
|
||||
|
||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
||||
// Verify that the dialog has been called only once
|
||||
expect(dialogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(service['isConnectionDialogOpen']).toBeTrue();
|
||||
// several almost-simultaneous calls
|
||||
service.openConnectionDialog('T', 'D', false);
|
||||
service.openConnectionDialog('T', 'D', false);
|
||||
tick(0);
|
||||
service.openConnectionDialog('T', 'D', false);
|
||||
tick(0);
|
||||
|
||||
// Try to open the dialog again
|
||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
||||
service.openConnectionDialog('Test Title', 'Test Description', false);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
// Verify that the dialog has been called only once
|
||||
expect(dialogSpy).toHaveBeenCalledTimes(1);
|
||||
it('launchNotification uses snackbar and triggers callback on action', fakeAsync(() => {
|
||||
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:
|
||||
| MatDialogRef<DialogTemplateComponent | RecordingDialogComponent | DeleteDialogComponent | ProFeatureDialogTemplateComponent>
|
||||
| undefined;
|
||||
private dialogSubscription: Subscription;
|
||||
private connectionDialogRef: MatDialogRef<DialogTemplateComponent> | undefined;
|
||||
private isConnectionDialogOpen: boolean = false;
|
||||
private isConnectionDialogOpen = false;
|
||||
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
|
|
@ -29,7 +28,7 @@ export class ActionService {
|
|||
private translateService: TranslateService
|
||||
) {}
|
||||
|
||||
launchNotification(options: INotificationOptions, callback): void {
|
||||
launchNotification(options: INotificationOptions, callback?: () => void): void {
|
||||
if (!options.config) {
|
||||
options.config = {
|
||||
duration: 3000,
|
||||
|
|
@ -41,28 +40,23 @@ export class ActionService {
|
|||
|
||||
const notification = this.snackBar.open(options.message, options.buttonActionText, options.config);
|
||||
if (callback) {
|
||||
notification.onAction().subscribe(() => {
|
||||
// subscribe and complete immediately after calling callback
|
||||
const sub = notification.onAction().subscribe(() => {
|
||||
sub.unsubscribe();
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openDialog(titleMessage: string, descriptionMessage: string, allowClose = true) {
|
||||
try {
|
||||
this.closeDialog();
|
||||
} catch (error) {
|
||||
} finally {
|
||||
const config: MatDialogConfig = {
|
||||
minWidth: '250px',
|
||||
data: { title: titleMessage, description: descriptionMessage, showActionButtons: allowClose },
|
||||
disableClose: !allowClose
|
||||
};
|
||||
this.dialogRef = this.dialog.open(DialogTemplateComponent, config);
|
||||
this.dialogSubscription = this.dialogRef.afterClosed().subscribe((result) => {
|
||||
this.dialogRef = undefined;
|
||||
if (this.dialogSubscription) this.dialogSubscription.unsubscribe();
|
||||
});
|
||||
}
|
||||
this.closeDialog();
|
||||
const config: MatDialogConfig = {
|
||||
minWidth: '250px',
|
||||
data: { title: titleMessage, description: descriptionMessage, showActionButtons: allowClose },
|
||||
disableClose: !allowClose
|
||||
};
|
||||
this.dialogRef = this.dialog.open(DialogTemplateComponent, config);
|
||||
this.dialogRef.afterClosed().subscribe(() => (this.dialogRef = undefined));
|
||||
}
|
||||
|
||||
openConnectionDialog(titleMessage: string, descriptionMessage: string, allowClose = false) {
|
||||
|
|
@ -75,47 +69,44 @@ export class ActionService {
|
|||
|
||||
this.connectionDialogRef = this.dialog.open(DialogTemplateComponent, config);
|
||||
this.isConnectionDialogOpen = true;
|
||||
this.connectionDialogRef.afterClosed().subscribe(() => {
|
||||
this.isConnectionDialogOpen = false;
|
||||
this.connectionDialogRef = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
openDeleteRecordingDialog(succsessCallback) {
|
||||
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) {
|
||||
try {
|
||||
this.closeDialog();
|
||||
} catch (error) {
|
||||
} finally {
|
||||
const config: MatDialogConfig = {
|
||||
minWidth: '250px',
|
||||
data: { src, showActionButtons: allowClose },
|
||||
disableClose: !allowClose
|
||||
};
|
||||
this.dialogRef = this.dialog.open(RecordingDialogComponent, config);
|
||||
|
||||
this.dialogSubscription = this.dialogRef.afterClosed().subscribe((data: { manageError: boolean; error: MediaError | null }) => {
|
||||
if (data.manageError) {
|
||||
this.handleRecordingPlayerError(data.error);
|
||||
}
|
||||
if (this.dialogSubscription) this.dialogSubscription.unsubscribe();
|
||||
});
|
||||
}
|
||||
this.closeDialog();
|
||||
const config: MatDialogConfig = {
|
||||
minWidth: '250px',
|
||||
data: { src, showActionButtons: allowClose },
|
||||
disableClose: !allowClose
|
||||
};
|
||||
this.dialogRef = this.dialog.open(RecordingDialogComponent, config);
|
||||
this.dialogRef.afterClosed().subscribe((data: { manageError: boolean; error: MediaError | null }) => {
|
||||
if (data && data.manageError) {
|
||||
this.handleRecordingPlayerError(data.error);
|
||||
}
|
||||
this.dialogRef = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.dialogRef?.close();
|
||||
if (this.dialogRef) {
|
||||
this.dialogRef.close();
|
||||
this.dialogRef = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
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 { PanelType } from '../../models/panel.model';
|
||||
import { TranslateService } from '../translate/translate.service';
|
||||
import { E2eeService } from '../e2ee/e2ee.service';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
@ -21,7 +22,7 @@ import { TranslateService } from '../translate/translate.service';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class ChatService {
|
||||
messagesObs: Observable<ChatMessage[]>;
|
||||
chatMessages$: Observable<ChatMessage[]>;
|
||||
private messageSound: HTMLAudioElement;
|
||||
private _messageList = <BehaviorSubject<ChatMessage[]>>new BehaviorSubject<ChatMessage[]>([]);
|
||||
private messageList: ChatMessage[] = [];
|
||||
|
|
@ -31,10 +32,11 @@ export class ChatService {
|
|||
private participantService: ParticipantService,
|
||||
private panelService: PanelService,
|
||||
private actionService: ActionService,
|
||||
private translateService: TranslateService
|
||||
private translateService: TranslateService,
|
||||
private e2eeService: E2eeService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('ChatService');
|
||||
this.messagesObs = this._messageList.asObservable();
|
||||
this.chatMessages$ = this._messageList.asObservable();
|
||||
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'
|
||||
);
|
||||
|
|
@ -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) {
|
||||
message = message.replace(/ +(?= )/g, '');
|
||||
if (message !== '' && message !== ' ') {
|
||||
const strData = JSON.stringify({ message });
|
||||
const data: Uint8Array = new TextEncoder().encode(strData);
|
||||
await this.participantService.publishData(data, { topic: DataTopic.CHAT, reliable: true });
|
||||
this.addMessage(message, true, this.participantService.getMyName()!);
|
||||
const plainTextMessage = message.replace(/ +(?= )/g, '');
|
||||
if (plainTextMessage !== '' && plainTextMessage !== ' ') {
|
||||
try {
|
||||
// Create message payload
|
||||
const payload = JSON.stringify({ message: plainTextMessage });
|
||||
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;
|
||||
showThemeSelector: boolean;
|
||||
recordingStreamBaseUrl: string;
|
||||
e2eeKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -302,7 +303,8 @@ export class OpenViduComponentsConfigService {
|
|||
prejoinDisplayParticipantName: true,
|
||||
showDisconnectionDialog: true,
|
||||
showThemeSelector: false,
|
||||
recordingStreamBaseUrl: 'call/api/recordings'
|
||||
recordingStreamBaseUrl: 'call/api/recordings',
|
||||
e2eeKey: undefined
|
||||
});
|
||||
|
||||
private toolbarConfig = this.createToolbarConfigItem({
|
||||
|
|
@ -413,6 +415,11 @@ export class OpenViduComponentsConfigService {
|
|||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
e2eeKey$: Observable<string | undefined> = this.generalConfig.observable$.pipe(
|
||||
map((config) => config.e2eeKey),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
// Stream observables
|
||||
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled));
|
||||
|
|
@ -565,6 +572,10 @@ export class OpenViduComponentsConfigService {
|
|||
return baseUrl;
|
||||
}
|
||||
|
||||
getE2EEKey(): string | undefined {
|
||||
return this.generalConfig.subject.getValue().e2eeKey;
|
||||
}
|
||||
|
||||
// Stream configuration methods
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
|
|
@ -16,7 +17,6 @@ export class GlobalConfigService {
|
|||
@Inject(DOCUMENT) private document: Document
|
||||
) {
|
||||
this.configuration = config;
|
||||
console.log(this.configuration);
|
||||
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.
|
||||
*/
|
||||
private async getLocalDevices(): Promise<MediaDeviceInfo[]> {
|
||||
// Forcing media permissions request.
|
||||
const strategies = [
|
||||
{ audio: true, video: true },
|
||||
{ audio: true, video: false },
|
||||
{ audio: false, video: true }
|
||||
];
|
||||
const strategies = this.getPermissionStrategies();
|
||||
|
||||
for (const strategy of strategies) {
|
||||
try {
|
||||
this.log.d(`Trying strategy: audio=${strategy.audio}, video=${strategy.video}`);
|
||||
const localTracks = await createLocalTracks(strategy);
|
||||
localTracks.forEach((track) => track.stop());
|
||||
|
||||
// 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');
|
||||
const devices = await this.tryPermissionStrategy(strategy);
|
||||
if (devices) {
|
||||
return this.filterValidDevices(devices);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.log.w(`Strategy failed: audio=${strategy.audio}, video=${strategy.video}`, error);
|
||||
|
||||
|
|
@ -209,6 +201,38 @@ export class DeviceService {
|
|||
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[]> {
|
||||
// Firefox requires to get user media to get the devices
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
|
|
@ -219,20 +243,28 @@ export class DeviceService {
|
|||
this.log.w('All permission strategies failed, trying device enumeration without permissions');
|
||||
|
||||
try {
|
||||
if (error?.name === 'NotReadableError' || error?.name === 'AbortError') {
|
||||
this.log.w('Device busy, using enumerateDevices() instead');
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((d) => d.deviceId && d.deviceId !== 'default');
|
||||
}
|
||||
if (error?.name === 'NotAllowedError' || error?.name === 'SecurityError') {
|
||||
this.log.w('Permission denied to access devices');
|
||||
this.deviceAccessDeniedError = true;
|
||||
}
|
||||
return [];
|
||||
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') {
|
||||
this.log.w('Device busy, using enumerateDevices() instead');
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((d) => d.deviceId && d.deviceId !== 'default');
|
||||
}
|
||||
if (error?.name === 'NotAllowedError' || error?.name === 'SecurityError') {
|
||||
this.log.w('Permission denied to access devices');
|
||||
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,37 +11,83 @@ export class DocumentService {
|
|||
constructor() {}
|
||||
|
||||
toggleFullscreen(elementId: string) {
|
||||
const document: any = window.document;
|
||||
const fs = document.getElementById(elementId);
|
||||
if (
|
||||
!document.fullscreenElement &&
|
||||
!document.mozFullScreenElement &&
|
||||
!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();
|
||||
}
|
||||
const document: any = this.getDocument();
|
||||
const fs = this.getElementById(elementId);
|
||||
|
||||
if (this.isInFullscreen()) {
|
||||
this.exitFullscreen(document);
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
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) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { ILogger } from '../../models/logger.model';
|
||||
import { DeviceService } from '../device/device.service';
|
||||
import { LoggerService } from '../logger/logger.service';
|
||||
import { BackgroundProcessor, /*BackgroundProcessorWrapper,*/ SwitchBackgroundProcessorOptions } from '@livekit/track-processors';
|
||||
import {
|
||||
AudioCaptureOptions,
|
||||
ConnectionState,
|
||||
CreateLocalTracksOptions,
|
||||
E2EEOptions,
|
||||
ExternalE2EEKeyProvider,
|
||||
LocalAudioTrack,
|
||||
LocalTrack,
|
||||
LocalVideoTrack,
|
||||
|
|
@ -16,8 +16,14 @@ import {
|
|||
VideoPresets,
|
||||
createLocalTracks
|
||||
} 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';
|
||||
|
||||
// TODO: Remove this once livekit-client exports it
|
||||
type BackgroundProcessorWrapper = ReturnType<typeof BackgroundProcessor>;
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
|
|
@ -31,6 +37,7 @@ export class OpenViduService {
|
|||
// private _isSttReady: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||
|
||||
private room: Room;
|
||||
private keyProvider: ExternalE2EEKeyProvider | undefined;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
@ -47,16 +54,24 @@ export class OpenViduService {
|
|||
private livekitUrl = '';
|
||||
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
|
||||
*/
|
||||
constructor(
|
||||
private loggerSrv: LoggerService,
|
||||
private deviceService: DeviceService,
|
||||
private storageService: StorageService
|
||||
private storageService: StorageService,
|
||||
private configService: OpenViduComponentsConfigService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('OpenViduService');
|
||||
// this.isSttReadyObs = this._isSttReady.asObservable();
|
||||
this.backgroundProcessor = BackgroundProcessor({ mode: 'disabled' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,14 +79,25 @@ export class OpenViduService {
|
|||
* @internal
|
||||
*/
|
||||
initRoom(): void {
|
||||
// If room already exists, don't recreate it
|
||||
if (this.room) {
|
||||
// Check if E2EE configuration needs to be applied
|
||||
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');
|
||||
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 audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
|
||||
|
||||
const roomOptions: RoomOptions = {
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
|
|
@ -93,17 +119,47 @@ export class OpenViduService {
|
|||
stopLocalTrackOnUnpublish: 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.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
|
||||
*/
|
||||
async connectRoom(): Promise<void> {
|
||||
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);
|
||||
this.log.d(`Successfully connected to room ${this.room.name}`);
|
||||
|
||||
const participantName = this.storageService.getParticipantName();
|
||||
if (participantName) {
|
||||
this.room.localParticipant.setName(participantName);
|
||||
|
|
@ -219,6 +275,18 @@ export class OpenViduService {
|
|||
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
|
||||
**/
|
||||
|
|
@ -291,6 +359,14 @@ export class OpenViduService {
|
|||
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
|
||||
if (!this.deviceService.isCameraEnabled()) {
|
||||
newLocalTracks.find((t) => t.kind === Track.Kind.Video)?.mute();
|
||||
|
|
@ -434,11 +510,10 @@ export class OpenViduService {
|
|||
}
|
||||
} catch (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)
|
||||
* @param deviceId new audio device to use
|
||||
* @internal
|
||||
|
|
@ -485,7 +560,8 @@ export class OpenViduService {
|
|||
}
|
||||
} catch (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>;
|
||||
private log: ILogger;
|
||||
private isExternalOpened: boolean = false;
|
||||
private externalType: string;
|
||||
private externalType: string = '';
|
||||
private _panelOpened = <BehaviorSubject<PanelStatusInfo>>new BehaviorSubject({ isOpened: false });
|
||||
private panelTypes: string[] = Object.values(PanelType);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
VideoPresets
|
||||
} from 'livekit-client';
|
||||
import { StorageService } from '../storage/storage.service';
|
||||
import { E2eeService } from '../e2ee/e2ee.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -50,7 +51,8 @@ export class ParticipantService {
|
|||
private directiveService: OpenViduComponentsConfigService,
|
||||
private openviduService: OpenViduService,
|
||||
private storageSrv: StorageService,
|
||||
private loggerSrv: LoggerService
|
||||
private loggerSrv: LoggerService,
|
||||
private e2eeService: E2eeService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('ParticipantService');
|
||||
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.
|
||||
*/
|
||||
|
|
@ -533,11 +555,45 @@ export class ParticipantService {
|
|||
}
|
||||
}
|
||||
|
||||
private newParticipant(props: ParticipantProperties) {
|
||||
private newParticipant(props: ParticipantProperties): ParticipantModel {
|
||||
let participant: ParticipantModel;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import {
|
|||
PreJoinDirective,
|
||||
ParticipantPanelAfterLocalParticipantDirective,
|
||||
LayoutAdditionalElementsDirective,
|
||||
LeaveButtonDirective
|
||||
LeaveButtonDirective,
|
||||
SettingsPanelGeneralAdditionalElementsDirective,
|
||||
ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||
} from '../../directives/template/internals.directive';
|
||||
|
||||
/**
|
||||
|
|
@ -49,6 +51,12 @@ export interface TemplateConfiguration {
|
|||
streamTemplate: TemplateRef<any>;
|
||||
layoutAdditionalElementsTemplate?: TemplateRef<any>;
|
||||
|
||||
// Settings panel templates
|
||||
settingsPanelGeneralAdditionalElementsTemplate?: TemplateRef<any>;
|
||||
|
||||
// Toolbar templates
|
||||
toolbarMoreOptionsAdditionalMenuItemsTemplate?: TemplateRef<any>;
|
||||
|
||||
// PreJoin template
|
||||
preJoinTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
|
@ -72,6 +80,7 @@ export interface ToolbarTemplateConfiguration {
|
|||
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
|
||||
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
|
||||
toolbarLeaveButtonTemplate?: TemplateRef<any>;
|
||||
toolbarMoreOptionsAdditionalMenuItemsTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -126,6 +135,8 @@ export interface ExternalDirectives {
|
|||
stream?: StreamDirective;
|
||||
preJoin?: PreJoinDirective;
|
||||
layoutAdditionalElements?: LayoutAdditionalElementsDirective;
|
||||
settingsPanelGeneralAdditionalElements?: SettingsPanelGeneralAdditionalElementsDirective;
|
||||
toolbarMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,6 +219,16 @@ export class TemplateManagerService {
|
|||
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);
|
||||
return config;
|
||||
}
|
||||
|
|
@ -368,14 +389,16 @@ export class TemplateManagerService {
|
|||
setupToolbarTemplates(
|
||||
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
|
||||
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective,
|
||||
externalLeaveButton?: LeaveButtonDirective
|
||||
externalLeaveButton?: LeaveButtonDirective,
|
||||
externalMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective
|
||||
): ToolbarTemplateConfiguration {
|
||||
this.log.v('Setting up toolbar templates...');
|
||||
|
||||
return {
|
||||
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template,
|
||||
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template,
|
||||
toolbarLeaveButtonTemplate: externalLeaveButton?.template
|
||||
toolbarLeaveButtonTemplate: externalLeaveButton?.template,
|
||||
toolbarMoreOptionsAdditionalMenuItemsTemplate: externalMoreOptionsAdditionalMenuItems?.template
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { SwitchBackgroundProcessorOptions } from '@livekit/track-processors';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
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 { 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
|
||||
|
|
@ -47,8 +45,8 @@ export class VirtualBackgroundService {
|
|||
private HARD_BLUR_INTENSITY = 60;
|
||||
|
||||
private log: ILogger;
|
||||
|
||||
constructor(
|
||||
private participantService: ParticipantService,
|
||||
private openviduService: OpenViduService,
|
||||
private storageService: StorageService,
|
||||
private loggerSrv: LoggerService
|
||||
|
|
@ -71,162 +69,59 @@ export class VirtualBackgroundService {
|
|||
if (!!bgId) {
|
||||
const background = this.backgrounds.find((bg) => bg.id === bgId);
|
||||
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) {
|
||||
// If the background is already applied, do nothing
|
||||
if (this.backgroundIsAlreadyApplied(bg.id)) return;
|
||||
|
||||
const cameraTrack = this.getCameraTrack();
|
||||
if (!cameraTrack) {
|
||||
this.log.e('No camera track found. Cannot apply background.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// If no effect is selected, remove the background
|
||||
if (bg.type === EffectType.NONE) {
|
||||
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);
|
||||
}
|
||||
const options = this.getBackgroundOptions(bg);
|
||||
await this.openviduService.switchBackgroundMode(options);
|
||||
|
||||
this.storageService.setBackground(bg.id);
|
||||
this.backgroundIdSelected.next(bg.id);
|
||||
this.log.d('Background applied:', options);
|
||||
} catch (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() {
|
||||
if (this.isBackgroundApplied()) {
|
||||
this.backgroundIdSelected.next('no_effect');
|
||||
const cameraTrack = this.getCameraTrack();
|
||||
if (cameraTrack) {
|
||||
try {
|
||||
await cameraTrack.stopProcessor();
|
||||
} catch (e) {
|
||||
this.log.w('Error stopping processor:', e);
|
||||
}
|
||||
try {
|
||||
await this.openviduService.switchBackgroundMode({ mode: 'disabled' });
|
||||
} catch (e) {
|
||||
this.log.w('Error disabling processor:', e);
|
||||
}
|
||||
this.storageService.removeBackground();
|
||||
}
|
||||
}
|
||||
|
||||
private getBackgroundProcessor(bg: BackgroundEffect): ProcessorWrapper<BackgroundOptions> | undefined {
|
||||
switch (bg.type) {
|
||||
case EffectType.IMAGE:
|
||||
if (bg.src) {
|
||||
return VirtualBackground(bg.src);
|
||||
}
|
||||
break;
|
||||
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;
|
||||
private getBackgroundOptions(bg: BackgroundEffect): SwitchBackgroundProcessorOptions {
|
||||
if (bg.type === EffectType.NONE) {
|
||||
return { mode: 'disabled' };
|
||||
} else if (bg.type === EffectType.IMAGE && 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
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
return { mode: 'disabled' };
|
||||
}
|
||||
|
||||
private backgroundIsAlreadyApplied(backgroundId: string): boolean {
|
||||
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/theme/theme.service';
|
||||
export * from './lib/services/viewport/viewport.service';
|
||||
export * from './lib/services/e2ee/e2ee.service';
|
||||
//Modules
|
||||
export * from './lib/openvidu-components-angular.module';
|
||||
export * from './lib/config/custom-cdk-overlay';
|
||||
export * from './lib/openvidu-components-angular-ui.module';
|
||||
|
||||
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,
|
||||
"removeComments": true,
|
||||
"pretty": false,
|
||||
// "skipLibCheck": true // Livekit track processors fails with typescript types checking
|
||||
"skipLibCheck": true // TODO Livekit track processors fails with typescript types checking
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
|
|
|
|||
|
|
@ -245,7 +245,6 @@ services:
|
|||
|
||||
setup:
|
||||
image: docker.io/busybox:1.37.0
|
||||
platform: linux/amd64
|
||||
restart: "no"
|
||||
volumes:
|
||||
- minio-data:/minio
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
services:
|
||||
|
||||
caddy-proxy:
|
||||
image: docker.io/openvidu/openvidu-caddy-local:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/openvidu-caddy-local:main
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
|
|
@ -19,6 +18,7 @@ services:
|
|||
- MEET_INITIAL_API_KEY=${MEET_INITIAL_API_KEY:-meet-api-key}
|
||||
volumes:
|
||||
- scripts:/scripts
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
entrypoint: /bin/sh /scripts/entrypoint_caddy.sh
|
||||
ports:
|
||||
- 5443:5443
|
||||
|
|
@ -31,13 +31,13 @@ services:
|
|||
condition: service_completed_successfully
|
||||
|
||||
redis:
|
||||
image: docker.io/redis:7.4.4-alpine
|
||||
platform: linux/amd64
|
||||
image: docker.io/redis:8.2.2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- redis:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
command: >
|
||||
redis-server
|
||||
--bind 0.0.0.0
|
||||
|
|
@ -47,8 +47,7 @@ services:
|
|||
condition: service_completed_successfully
|
||||
|
||||
minio:
|
||||
image: docker.io/openvidu/minio:2025.5.24-debian-12-r1
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/minio:2025.9.7-debian-12-r3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9000:9000
|
||||
|
|
@ -58,21 +57,23 @@ services:
|
|||
- MINIO_DEFAULT_BUCKETS=openvidu-appdata
|
||||
- MINIO_CONSOLE_SUBPATH=/minio-console
|
||||
- MINIO_BROWSER_REDIRECT_URL=http://localhost:7880/minio-console
|
||||
- MINIO_BROWSER=on
|
||||
volumes:
|
||||
- minio-data:/bitnami/minio/data
|
||||
- minio-certs:/certs
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
mongo:
|
||||
image: docker.io/openvidu/mongodb:8.0.9
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/mongodb:8.0.15-r0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 27017:27017
|
||||
volumes:
|
||||
- mongo-data:/bitnami/mongodb
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
- MONGODB_ROOT_USER=${MONGO_ADMIN_USERNAME:-mongoadmin}
|
||||
- MONGODB_ROOT_PASSWORD=${MONGO_ADMIN_PASSWORD:-mongoadmin}
|
||||
|
|
@ -80,27 +81,26 @@ services:
|
|||
- MONGODB_REPLICA_SET_MODE=primary
|
||||
- MONGODB_REPLICA_SET_NAME=rs0
|
||||
- MONGODB_REPLICA_SET_KEY=devreplicasetkey
|
||||
- EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
|
||||
depends_on:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
dashboard:
|
||||
image: docker.io/openvidu/openvidu-dashboard:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/openvidu-dashboard:main
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SERVER_PORT=5000
|
||||
- ADMIN_USERNAME=${DASHBOARD_ADMIN_USERNAME:-admin}
|
||||
- ADMIN_PASSWORD=${DASHBOARD_ADMIN_PASSWORD:-admin}
|
||||
- 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:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
openvidu:
|
||||
image: docker.io/openvidu/openvidu-server:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/openvidu-server:main
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
|
|
@ -115,13 +115,13 @@ services:
|
|||
volumes:
|
||||
- scripts:/scripts
|
||||
- config:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
ingress:
|
||||
image: docker.io/openvidu/ingress:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/ingress:main
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
|
|
@ -133,13 +133,13 @@ services:
|
|||
- INGRESS_CONFIG_FILE=/config/ingress.yaml
|
||||
volumes:
|
||||
- config:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
egress:
|
||||
image: docker.io/openvidu/egress:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/egress:main
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
|
|
@ -148,20 +148,20 @@ services:
|
|||
volumes:
|
||||
- config:/config
|
||||
- egress-data:/home/egress/tmp
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
depends_on:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
operator:
|
||||
image: docker.io/openvidu/openvidu-operator:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/openvidu-operator:main
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- agents-config:/agents-config
|
||||
- operator-deployment:/deployment
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
- PLATFORM=linux/amd64
|
||||
- MODE=agent-manager-local
|
||||
- DEPLOYMENT_FILES_DIR=/deployment
|
||||
- AGENTS_CONFIG_DIR=/agents-config
|
||||
|
|
@ -177,8 +177,7 @@ services:
|
|||
condition: service_completed_successfully
|
||||
|
||||
openvidu-meet:
|
||||
image: docker.io/openvidu/openvidu-meet:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/openvidu-meet:main
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 9080:6080
|
||||
|
|
@ -209,16 +208,17 @@ services:
|
|||
- MEET_REDIS_PORT=6379
|
||||
- MEET_REDIS_PASSWORD=${REDIS_PASSWORD:-redispassword}
|
||||
- MEET_REDIS_DB=0
|
||||
- MEET_MONGO_URI=mongodb://${MONGO_ADMIN_USERNAME:-mongoadmin}:${MONGO_ADMIN_PASSWORD:-mongoadmin}@mongo:27017/?replicaSet=rs0&readPreference=primaryPreferred
|
||||
volumes:
|
||||
- scripts:/scripts
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
entrypoint: /bin/sh /scripts/entrypoint_openvidu_meet.sh
|
||||
depends_on:
|
||||
setup:
|
||||
condition: service_completed_successfully
|
||||
|
||||
openvidu-meet-init:
|
||||
image: docker.io/openvidu/openvidu-operator:3.4.0
|
||||
platform: linux/amd64
|
||||
image: docker.io/openvidu/openvidu-operator:main
|
||||
restart: on-failure
|
||||
environment:
|
||||
- MODE=local-ready-check
|
||||
|
|
@ -235,6 +235,7 @@ services:
|
|||
- MEET_INITIAL_API_KEY=${MEET_INITIAL_API_KEY:-meet-api-key}
|
||||
volumes:
|
||||
- scripts:/scripts
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
entrypoint: /bin/sh /scripts/entrypoint_ready_check.sh
|
||||
depends_on:
|
||||
- caddy-proxy
|
||||
|
|
|
|||
|
|
@ -81,42 +81,27 @@ Parameters:
|
|||
Type: String
|
||||
Default: c6a.xlarge
|
||||
AllowedValues:
|
||||
- t2.large
|
||||
- t2.xlarge
|
||||
- t2.2xlarge
|
||||
- t3.nano
|
||||
- t3.micro
|
||||
- t3.small
|
||||
- t3.medium
|
||||
- t3.large
|
||||
- t3.xlarge
|
||||
- t3.2xlarge
|
||||
- m4.large
|
||||
- m4.xlarge
|
||||
- m4.2xlarge
|
||||
- m4.4xlarge
|
||||
- m4.10xlarge
|
||||
- m4.16xlarge
|
||||
- m5.large
|
||||
- m5.xlarge
|
||||
- m5.2xlarge
|
||||
- m5.4xlarge
|
||||
- m5.8xlarge
|
||||
- m5.12xlarge
|
||||
- m5.16xlarge
|
||||
- m5.24xlarge
|
||||
- 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
|
||||
- t3a.nano
|
||||
- t3a.micro
|
||||
- t3a.small
|
||||
- t3a.medium
|
||||
- t3a.large
|
||||
- t3a.xlarge
|
||||
- t3a.2xlarge
|
||||
- t4g.nano
|
||||
- t4g.micro
|
||||
- t4g.small
|
||||
- t4g.medium
|
||||
- t4g.large
|
||||
- t4g.xlarge
|
||||
- t4g.2xlarge
|
||||
- c5.large
|
||||
- c5.xlarge
|
||||
- c5.2xlarge
|
||||
|
|
@ -125,6 +110,39 @@ Parameters:
|
|||
- c5.12xlarge
|
||||
- c5.18xlarge
|
||||
- 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.xlarge
|
||||
- c6a.2xlarge
|
||||
|
|
@ -136,6 +154,32 @@ Parameters:
|
|||
- c6a.32xlarge
|
||||
- c6a.48xlarge
|
||||
- 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.xlarge
|
||||
- c6i.2xlarge
|
||||
|
|
@ -146,6 +190,26 @@ Parameters:
|
|||
- c6i.24xlarge
|
||||
- c6i.32xlarge
|
||||
- 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.large
|
||||
- c7a.xlarge
|
||||
|
|
@ -158,6 +222,40 @@ Parameters:
|
|||
- c7a.32xlarge
|
||||
- c7a.48xlarge
|
||||
- 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.xlarge
|
||||
- c7i.2xlarge
|
||||
|
|
@ -169,20 +267,77 @@ Parameters:
|
|||
- c7i.48xlarge
|
||||
- c7i.metal-24xl
|
||||
- c7i.metal-48xl
|
||||
- c5n.large
|
||||
- c5n.xlarge
|
||||
- c5n.2xlarge
|
||||
- c5n.4xlarge
|
||||
- c5n.9xlarge
|
||||
- c5n.18xlarge
|
||||
- m5n.large
|
||||
- m5n.xlarge
|
||||
- m5n.2xlarge
|
||||
- m5n.4xlarge
|
||||
- m5n.8xlarge
|
||||
- m5n.12xlarge
|
||||
- m5n.16xlarge
|
||||
- m5n.24xlarge
|
||||
- c8g.medium
|
||||
- c8g.large
|
||||
- c8g.xlarge
|
||||
- c8g.2xlarge
|
||||
- c8g.4xlarge
|
||||
- c8g.8xlarge
|
||||
- c8g.12xlarge
|
||||
- c8g.16xlarge
|
||||
- c8g.24xlarge
|
||||
- c8g.48xlarge
|
||||
- c8g.metal-24xl
|
||||
- c8g.metal-48xl
|
||||
- m6a.large
|
||||
- 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.xlarge
|
||||
- m6in.2xlarge
|
||||
|
|
@ -192,14 +347,67 @@ Parameters:
|
|||
- m6in.16xlarge
|
||||
- m6in.24xlarge
|
||||
- m6in.32xlarge
|
||||
- r5n.large
|
||||
- r5n.xlarge
|
||||
- r5n.2xlarge
|
||||
- r5n.4xlarge
|
||||
- r5n.8xlarge
|
||||
- r5n.12xlarge
|
||||
- r5n.16xlarge
|
||||
- r5n.24xlarge
|
||||
- m6in.metal
|
||||
- m7a.medium
|
||||
- m7a.large
|
||||
- m7a.xlarge
|
||||
- m7a.2xlarge
|
||||
- m7a.4xlarge
|
||||
- m7a.8xlarge
|
||||
- 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"
|
||||
|
||||
KeyName:
|
||||
|
|
@ -208,10 +416,11 @@ Parameters:
|
|||
AllowedPattern: ^.+$
|
||||
ConstraintDescription: must be the name of an existing EC2 KeyPair.
|
||||
|
||||
AmiId:
|
||||
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
|
||||
Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id
|
||||
Description: AMI ID for the EC2 instances
|
||||
OperatingSystem:
|
||||
Description: VSCode Server EC2 operating system
|
||||
Type: String
|
||||
Default: "Ubuntu-24"
|
||||
AllowedValues: ['Ubuntu-22', 'Ubuntu-24']
|
||||
|
||||
S3AppDataBucketName:
|
||||
Type: String
|
||||
|
|
@ -238,7 +447,7 @@ Metadata:
|
|||
Parameters:
|
||||
- InstanceType
|
||||
- KeyName
|
||||
- AmiId
|
||||
- OperatingSystem
|
||||
- Label:
|
||||
default: S3 bucket for application data and recordings
|
||||
Parameters:
|
||||
|
|
@ -258,6 +467,31 @@ Conditions:
|
|||
PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]
|
||||
PublicElasticIPAbsent: !Equals [!Ref PublicElasticIP, ""]
|
||||
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:
|
||||
|
||||
|
|
@ -384,7 +618,7 @@ Resources:
|
|||
'/usr/local/bin/install.sh':
|
||||
content: !Sub |
|
||||
#!/bin/bash -x
|
||||
OPENVIDU_VERSION=3.4.1
|
||||
OPENVIDU_VERSION=main
|
||||
DOMAIN=
|
||||
YQ_VERSION=v4.44.5
|
||||
|
||||
|
|
@ -827,7 +1061,10 @@ Resources:
|
|||
owner: "root"
|
||||
group: "root"
|
||||
Properties:
|
||||
ImageId: !Ref AmiId
|
||||
ImageId: !If
|
||||
- IsGraviton
|
||||
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
|
||||
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
|
||||
LaunchTemplate:
|
||||
# Enable IMDSv2 by default
|
||||
LaunchTemplateId: !Ref IMDSv2LaunchTemplate
|
||||
|
|
|
|||
|
|
@ -45,117 +45,8 @@ param initialMeetApiKey string = ''
|
|||
|
||||
|
||||
// Azure instance config
|
||||
@description('Specifies the azure vm size for your OpenVidu instance')
|
||||
@allowed([
|
||||
'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('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.')
|
||||
param instanceType string = 'Standard_B4s'
|
||||
|
||||
@description('Username for the Virtual Machine.')
|
||||
param adminUsername string
|
||||
|
|
@ -174,6 +65,15 @@ var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
|
|||
//Condition for the domain name
|
||||
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
|
||||
var networkSettings = {
|
||||
privateIPaddressNetInterface: '10.0.0.5'
|
||||
|
|
@ -189,8 +89,8 @@ var openviduVMSettings = {
|
|||
osDiskType: 'StandardSSD_LRS'
|
||||
ubuntuOSVersion: {
|
||||
publisher: 'Canonical'
|
||||
offer: '0001-com-ubuntu-server-jammy'
|
||||
sku: '22_04-lts-gen2'
|
||||
offer: 'ubuntu-24_04-lts'
|
||||
sku: ubuntuSku
|
||||
version: 'latest'
|
||||
}
|
||||
linuxConfiguration: {
|
||||
|
|
@ -275,7 +175,7 @@ var stringInterpolationParams = {
|
|||
|
||||
var installScriptTemplate = '''
|
||||
#!/bin/bash -x
|
||||
OPENVIDU_VERSION=3.4.1
|
||||
OPENVIDU_VERSION=main
|
||||
DOMAIN=
|
||||
|
||||
apt-get update && apt-get install -y \
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -209,14 +209,14 @@
|
|||
"label": "Type of Instance",
|
||||
"toolTip": "Specifies the azure vm size for your OpenVidu instance",
|
||||
"recommendedSizes": [
|
||||
"Standard_B2s",
|
||||
"Standard_B4ms"
|
||||
"Standard_B4s",
|
||||
"Standard_B4ms",
|
||||
"Standard_D4ps_v5",
|
||||
"Standard_D4pls_v5"
|
||||
],
|
||||
"constraints": {
|
||||
"allowedSizes": [],
|
||||
"excludedSizes": [],
|
||||
"numAvailabilityZonesRequired": 3,
|
||||
"zone": "3"
|
||||
"excludedSizes": []
|
||||
},
|
||||
"options": {
|
||||
"hideDiskTypeFilter": false
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
# ------------------------- outputs.tf -------------------------
|
||||
|
||||
output "openvidu_instance_name" {
|
||||
value = google_compute_instance.openvidu_server.name
|
||||
}
|
||||
|
||||
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
|
||||
output "secrets_manager" {
|
||||
value = "https://console.cloud.google.com/security/secret-manager?project=${var.projectId}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,28 @@ resource "google_project_service" "cloudresourcemanager_api" { service = "cloudr
|
|||
|
||||
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
|
||||
resource "google_storage_bucket" "bucket" {
|
||||
count = 1
|
||||
name = local.isEmpty ? "${var.projectId}-${random_id.bucket_suffix.hex}" : var.bucketName
|
||||
count = local.isEmpty ? 1 : 0
|
||||
name = "${var.projectId}-${var.stackName}-${random_id.bucket_suffix.hex}"
|
||||
location = var.region
|
||||
force_destroy = true
|
||||
uniform_bucket_level_access = true
|
||||
|
|
@ -66,6 +84,14 @@ resource "google_compute_address" "public_ip_address" {
|
|||
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
|
||||
resource "google_compute_instance" "openvidu_server" {
|
||||
name = lower("${var.stackName}-vm-ce")
|
||||
|
|
@ -76,7 +102,7 @@ resource "google_compute_instance" "openvidu_server" {
|
|||
|
||||
boot_disk {
|
||||
initialize_params {
|
||||
image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts"
|
||||
image = local.ubuntu_image
|
||||
size = 100
|
||||
type = "pd-standard"
|
||||
}
|
||||
|
|
@ -91,7 +117,7 @@ resource "google_compute_instance" "openvidu_server" {
|
|||
|
||||
metadata = {
|
||||
# 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
|
||||
stackName = var.stackName
|
||||
certificateType = var.certificateType
|
||||
|
|
@ -125,7 +151,7 @@ locals {
|
|||
#!/bin/bash -x
|
||||
set -e
|
||||
|
||||
OPENVIDU_VERSION=3.4.1
|
||||
OPENVIDU_VERSION=main
|
||||
DOMAIN=
|
||||
YQ_VERSION=v4.44.5
|
||||
apt-get update && apt-get install -y \
|
||||
|
|
@ -138,8 +164,8 @@ apt-get update && apt-get install -y \
|
|||
lsb-release \
|
||||
openssl
|
||||
|
||||
wget https://github.com/mikefarah/yq/releases/download/$${YQ_VERSION}/yq_linux_amd64.tar.gz -O - |\
|
||||
tar xz && mv yq_linux_amd64 /usr/bin/yq
|
||||
wget https://github.com/mikefarah/yq/releases/download/$${YQ_VERSION}/yq_linux_${local.yq_arch}.tar.gz -O - |\
|
||||
tar xz && mv yq_linux_${local.yq_arch} /usr/bin/yq
|
||||
|
||||
# Configure gcloud with instance service account
|
||||
gcloud auth activate-service-account --key-file=/dev/null 2>/dev/null || true
|
||||
|
|
@ -149,31 +175,6 @@ get_meta() { curl -s -H "Metadata-Flavor: Google" "$${METADATA_URL}/$1"; }
|
|||
# Create counter file for tracking script executions
|
||||
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
|
||||
if [[ "${var.domainName}" == "" ]]; then
|
||||
[ ! -d "/usr/share/openvidu" ] && mkdir -p /usr/share/openvidu
|
||||
|
|
@ -339,7 +340,7 @@ EXTERNAL_S3_SECRET_KEY=$(echo "$HMAC_OUTPUT" | jq -r '.secret')
|
|||
EXTERNAL_S3_ENDPOINT="https://storage.googleapis.com"
|
||||
EXTERNAL_S3_REGION="${var.region}"
|
||||
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
|
||||
if [ -f "$${CONFIG_DIR}/egress.yaml" ]; then
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ variable "initialMeetApiKey" {
|
|||
variable "instanceType" {
|
||||
description = "Specifies the GCE machine type for your OpenVidu instance"
|
||||
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"
|
||||
|
|
@ -127,4 +127,4 @@ variable "turnOwnPrivateCertificate" {
|
|||
description = "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#!/bin/sh
|
||||
# Docker & Docker Compose will need to be installed on the machine
|
||||
set -eu
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.3.3}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.39.4}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.4.1}"
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-29.0.2}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||
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_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
||||
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-08-13T08-35-41Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.15}"
|
||||
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 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}}"
|
||||
|
|
@ -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 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.7.1}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.7}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.7}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.17.1}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:12.2.0}"
|
||||
|
||||
# Function to compare two version strings
|
||||
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
|
||||
fi
|
||||
|
||||
# Restart Docker and wait for it to start
|
||||
systemctl enable docker
|
||||
systemctl stop docker
|
||||
systemctl start docker
|
||||
wait_for_docker
|
||||
# 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 start docker
|
||||
wait_for_docker
|
||||
fi
|
||||
|
||||
# Create random temp directory
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#!/bin/sh
|
||||
# Docker & Docker Compose will need to be installed on the machine
|
||||
set -eu
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.3.3}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.39.4}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.4.1}"
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-29.0.2}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||
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_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
||||
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-08-13T08-35-41Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.15}"
|
||||
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 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}}"
|
||||
|
|
@ -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 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.7.1}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.7}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.7}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.17.1}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:12.2.0}"
|
||||
|
||||
# Function to compare two version strings
|
||||
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
|
||||
fi
|
||||
|
||||
# Restart Docker and wait for it to start
|
||||
systemctl enable docker
|
||||
systemctl stop docker
|
||||
systemctl start docker
|
||||
wait_for_docker
|
||||
# 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 start docker
|
||||
wait_for_docker
|
||||
fi
|
||||
|
||||
# Create random temp directory
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
|
|
|||
|
|
@ -95,42 +95,27 @@ Parameters:
|
|||
Type: String
|
||||
Default: c6a.xlarge
|
||||
AllowedValues:
|
||||
- t2.large
|
||||
- t2.xlarge
|
||||
- t2.2xlarge
|
||||
- t3.nano
|
||||
- t3.micro
|
||||
- t3.small
|
||||
- t3.medium
|
||||
- t3.large
|
||||
- t3.xlarge
|
||||
- t3.2xlarge
|
||||
- m4.large
|
||||
- m4.xlarge
|
||||
- m4.2xlarge
|
||||
- m4.4xlarge
|
||||
- m4.10xlarge
|
||||
- m4.16xlarge
|
||||
- m5.large
|
||||
- m5.xlarge
|
||||
- m5.2xlarge
|
||||
- m5.4xlarge
|
||||
- m5.8xlarge
|
||||
- m5.12xlarge
|
||||
- m5.16xlarge
|
||||
- m5.24xlarge
|
||||
- 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
|
||||
- t3a.nano
|
||||
- t3a.micro
|
||||
- t3a.small
|
||||
- t3a.medium
|
||||
- t3a.large
|
||||
- t3a.xlarge
|
||||
- t3a.2xlarge
|
||||
- t4g.nano
|
||||
- t4g.micro
|
||||
- t4g.small
|
||||
- t4g.medium
|
||||
- t4g.large
|
||||
- t4g.xlarge
|
||||
- t4g.2xlarge
|
||||
- c5.large
|
||||
- c5.xlarge
|
||||
- c5.2xlarge
|
||||
|
|
@ -139,6 +124,39 @@ Parameters:
|
|||
- c5.12xlarge
|
||||
- c5.18xlarge
|
||||
- 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.xlarge
|
||||
- c6a.2xlarge
|
||||
|
|
@ -150,6 +168,32 @@ Parameters:
|
|||
- c6a.32xlarge
|
||||
- c6a.48xlarge
|
||||
- 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.xlarge
|
||||
- c6i.2xlarge
|
||||
|
|
@ -160,6 +204,26 @@ Parameters:
|
|||
- c6i.24xlarge
|
||||
- c6i.32xlarge
|
||||
- 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.large
|
||||
- c7a.xlarge
|
||||
|
|
@ -172,6 +236,40 @@ Parameters:
|
|||
- c7a.32xlarge
|
||||
- c7a.48xlarge
|
||||
- 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.xlarge
|
||||
- c7i.2xlarge
|
||||
|
|
@ -183,20 +281,77 @@ Parameters:
|
|||
- c7i.48xlarge
|
||||
- c7i.metal-24xl
|
||||
- c7i.metal-48xl
|
||||
- c5n.large
|
||||
- c5n.xlarge
|
||||
- c5n.2xlarge
|
||||
- c5n.4xlarge
|
||||
- c5n.9xlarge
|
||||
- c5n.18xlarge
|
||||
- m5n.large
|
||||
- m5n.xlarge
|
||||
- m5n.2xlarge
|
||||
- m5n.4xlarge
|
||||
- m5n.8xlarge
|
||||
- m5n.12xlarge
|
||||
- m5n.16xlarge
|
||||
- m5n.24xlarge
|
||||
- c8g.medium
|
||||
- c8g.large
|
||||
- c8g.xlarge
|
||||
- c8g.2xlarge
|
||||
- c8g.4xlarge
|
||||
- c8g.8xlarge
|
||||
- c8g.12xlarge
|
||||
- c8g.16xlarge
|
||||
- c8g.24xlarge
|
||||
- c8g.48xlarge
|
||||
- c8g.metal-24xl
|
||||
- c8g.metal-48xl
|
||||
- m6a.large
|
||||
- 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.xlarge
|
||||
- m6in.2xlarge
|
||||
|
|
@ -206,14 +361,67 @@ Parameters:
|
|||
- m6in.16xlarge
|
||||
- m6in.24xlarge
|
||||
- m6in.32xlarge
|
||||
- r5n.large
|
||||
- r5n.xlarge
|
||||
- r5n.2xlarge
|
||||
- r5n.4xlarge
|
||||
- r5n.8xlarge
|
||||
- r5n.12xlarge
|
||||
- r5n.16xlarge
|
||||
- r5n.24xlarge
|
||||
- m6in.metal
|
||||
- m7a.medium
|
||||
- m7a.large
|
||||
- m7a.xlarge
|
||||
- m7a.2xlarge
|
||||
- m7a.4xlarge
|
||||
- m7a.8xlarge
|
||||
- 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"
|
||||
|
||||
MediaNodeInstanceType:
|
||||
|
|
@ -221,42 +429,27 @@ Parameters:
|
|||
Type: String
|
||||
Default: c6a.xlarge
|
||||
AllowedValues:
|
||||
- t2.large
|
||||
- t2.xlarge
|
||||
- t2.2xlarge
|
||||
- t3.nano
|
||||
- t3.micro
|
||||
- t3.small
|
||||
- t3.medium
|
||||
- t3.large
|
||||
- t3.xlarge
|
||||
- t3.2xlarge
|
||||
- m4.large
|
||||
- m4.xlarge
|
||||
- m4.2xlarge
|
||||
- m4.4xlarge
|
||||
- m4.10xlarge
|
||||
- m4.16xlarge
|
||||
- m5.large
|
||||
- m5.xlarge
|
||||
- m5.2xlarge
|
||||
- m5.4xlarge
|
||||
- m5.8xlarge
|
||||
- m5.12xlarge
|
||||
- m5.16xlarge
|
||||
- m5.24xlarge
|
||||
- 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
|
||||
- t3a.nano
|
||||
- t3a.micro
|
||||
- t3a.small
|
||||
- t3a.medium
|
||||
- t3a.large
|
||||
- t3a.xlarge
|
||||
- t3a.2xlarge
|
||||
- t4g.nano
|
||||
- t4g.micro
|
||||
- t4g.small
|
||||
- t4g.medium
|
||||
- t4g.large
|
||||
- t4g.xlarge
|
||||
- t4g.2xlarge
|
||||
- c5.large
|
||||
- c5.xlarge
|
||||
- c5.2xlarge
|
||||
|
|
@ -265,6 +458,39 @@ Parameters:
|
|||
- c5.12xlarge
|
||||
- c5.18xlarge
|
||||
- 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.xlarge
|
||||
- c6a.2xlarge
|
||||
|
|
@ -276,6 +502,32 @@ Parameters:
|
|||
- c6a.32xlarge
|
||||
- c6a.48xlarge
|
||||
- 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.xlarge
|
||||
- c6i.2xlarge
|
||||
|
|
@ -286,6 +538,26 @@ Parameters:
|
|||
- c6i.24xlarge
|
||||
- c6i.32xlarge
|
||||
- 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.large
|
||||
- c7a.xlarge
|
||||
|
|
@ -298,6 +570,40 @@ Parameters:
|
|||
- c7a.32xlarge
|
||||
- c7a.48xlarge
|
||||
- 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.xlarge
|
||||
- c7i.2xlarge
|
||||
|
|
@ -309,20 +615,77 @@ Parameters:
|
|||
- c7i.48xlarge
|
||||
- c7i.metal-24xl
|
||||
- c7i.metal-48xl
|
||||
- c5n.large
|
||||
- c5n.xlarge
|
||||
- c5n.2xlarge
|
||||
- c5n.4xlarge
|
||||
- c5n.9xlarge
|
||||
- c5n.18xlarge
|
||||
- m5n.large
|
||||
- m5n.xlarge
|
||||
- m5n.2xlarge
|
||||
- m5n.4xlarge
|
||||
- m5n.8xlarge
|
||||
- m5n.12xlarge
|
||||
- m5n.16xlarge
|
||||
- m5n.24xlarge
|
||||
- c8g.medium
|
||||
- c8g.large
|
||||
- c8g.xlarge
|
||||
- c8g.2xlarge
|
||||
- c8g.4xlarge
|
||||
- c8g.8xlarge
|
||||
- c8g.12xlarge
|
||||
- c8g.16xlarge
|
||||
- c8g.24xlarge
|
||||
- c8g.48xlarge
|
||||
- c8g.metal-24xl
|
||||
- c8g.metal-48xl
|
||||
- m6a.large
|
||||
- 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.xlarge
|
||||
- m6in.2xlarge
|
||||
|
|
@ -332,14 +695,67 @@ Parameters:
|
|||
- m6in.16xlarge
|
||||
- m6in.24xlarge
|
||||
- m6in.32xlarge
|
||||
- r5n.large
|
||||
- r5n.xlarge
|
||||
- r5n.2xlarge
|
||||
- r5n.4xlarge
|
||||
- r5n.8xlarge
|
||||
- r5n.12xlarge
|
||||
- r5n.16xlarge
|
||||
- r5n.24xlarge
|
||||
- m6in.metal
|
||||
- m7a.medium
|
||||
- m7a.large
|
||||
- m7a.xlarge
|
||||
- m7a.2xlarge
|
||||
- m7a.4xlarge
|
||||
- m7a.8xlarge
|
||||
- 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"
|
||||
|
||||
KeyName:
|
||||
|
|
@ -348,10 +764,11 @@ Parameters:
|
|||
AllowedPattern: ^.+$
|
||||
ConstraintDescription: must be the name of an existing EC2 KeyPair.
|
||||
|
||||
AmiId:
|
||||
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
|
||||
Default: /aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id
|
||||
Description: AMI ID for the EC2 instances
|
||||
OperatingSystem:
|
||||
Description: OpenVidu EC2 operating system
|
||||
Type: String
|
||||
Default: "Ubuntu-24"
|
||||
AllowedValues: ['Ubuntu-22', 'Ubuntu-24']
|
||||
|
||||
InitialNumberOfMediaNodes:
|
||||
Type: Number
|
||||
|
|
@ -422,7 +839,7 @@ Metadata:
|
|||
- MasterNodeInstanceType
|
||||
- MediaNodeInstanceType
|
||||
- KeyName
|
||||
- AmiId
|
||||
- OperatingSystem
|
||||
- Label:
|
||||
default: Media Nodes Autoscaling Group configuration
|
||||
Parameters:
|
||||
|
|
@ -455,6 +872,41 @@ Conditions:
|
|||
PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]
|
||||
PublicElasticIPAbsent: !Equals [!Ref PublicElasticIP, ""]
|
||||
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:
|
||||
|
||||
|
|
@ -684,7 +1136,7 @@ Resources:
|
|||
content: !Sub |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
OPENVIDU_VERSION=3.4.1
|
||||
OPENVIDU_VERSION=main
|
||||
DOMAIN=
|
||||
YQ_VERSION=v4.44.5
|
||||
|
||||
|
|
@ -1172,7 +1624,10 @@ Resources:
|
|||
owner: "root"
|
||||
group: "root"
|
||||
Properties:
|
||||
ImageId: !Ref AmiId
|
||||
ImageId: !If
|
||||
- IsMasterGraviton
|
||||
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
|
||||
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
|
||||
LaunchTemplate:
|
||||
# Enable IMDSv2 by default
|
||||
LaunchTemplateId: !Ref IMDSv2LaunchTemplateMasterNode
|
||||
|
|
@ -1420,7 +1875,10 @@ Resources:
|
|||
Arn: !GetAtt OpenViduMediaNodeInstanceProfile.Arn
|
||||
SecurityGroupIds:
|
||||
- !GetAtt OpenViduMediaNodeSG.GroupId
|
||||
ImageId: !Ref AmiId
|
||||
ImageId: !If
|
||||
- IsMediaGraviton
|
||||
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
|
||||
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
|
||||
KeyName: !Ref KeyName
|
||||
InstanceType: !Ref MediaNodeInstanceType
|
||||
UserData:
|
||||
|
|
|
|||
|
|
@ -53,229 +53,11 @@ param initialMeetAdminPassword string = ''
|
|||
@secure()
|
||||
param initialMeetApiKey string = ''
|
||||
|
||||
@description('Specifies the EC2 instance type for your OpenVidu Master Node')
|
||||
@allowed([
|
||||
'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 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.')
|
||||
param masterNodeInstanceType string = 'Standard_B4s'
|
||||
|
||||
@description('Specifies the EC2 instance type for your OpenVidu Media Nodes')
|
||||
@allowed([
|
||||
'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('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.')
|
||||
param mediaNodeInstanceType string = 'Standard_B4s'
|
||||
|
||||
@description('Username for the Virtual Machine.')
|
||||
param adminUsername string
|
||||
|
|
@ -304,13 +86,25 @@ var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
|
|||
|
||||
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 = {
|
||||
vmName: '${stackName}-VM-MasterNode'
|
||||
osDiskType: 'StandardSSD_LRS'
|
||||
ubuntuOSVersion: {
|
||||
publisher: 'Canonical'
|
||||
offer: '0001-com-ubuntu-server-jammy'
|
||||
sku: '22_04-lts-gen2'
|
||||
offer: 'ubuntu-24_04-lts'
|
||||
sku: masterUbuntuSku
|
||||
version: 'latest'
|
||||
}
|
||||
linuxConfiguration: {
|
||||
|
|
@ -331,8 +125,8 @@ var mediaNodeVMSettings = {
|
|||
osDiskType: 'StandardSSD_LRS'
|
||||
ubuntuOSVersion: {
|
||||
publisher: 'Canonical'
|
||||
offer: '0001-com-ubuntu-server-jammy'
|
||||
sku: '22_04-lts-gen2'
|
||||
offer: 'ubuntu-24_04-lts'
|
||||
sku: mediaUbuntuSku
|
||||
version: 'latest'
|
||||
}
|
||||
linuxConfiguration: {
|
||||
|
|
@ -429,7 +223,7 @@ var stringInterpolationParamsMaster = {
|
|||
|
||||
var installScriptTemplateMaster = '''
|
||||
#!/bin/bash -x
|
||||
OPENVIDU_VERSION=3.4.1
|
||||
OPENVIDU_VERSION=main
|
||||
DOMAIN=
|
||||
|
||||
# Assume azure cli is installed
|
||||
|
|
@ -945,7 +739,7 @@ var after_installScriptMaster = reduce(
|
|||
|
||||
var get_public_ip_script = reduce(
|
||||
items(stringInterpolationParamsMaster),
|
||||
{ value: get_public_ip},
|
||||
{ value: get_public_ip },
|
||||
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
|
||||
).value
|
||||
|
||||
|
|
@ -1530,7 +1324,7 @@ module webhookModule '../../shared/webhookdeployment.json' = {
|
|||
}
|
||||
|
||||
resource actionGroupScaleIn 'Microsoft.Insights/actionGroups@2023-01-01' = {
|
||||
name: 'actiongrouptest'
|
||||
name: 'actiongroupScaleIn'
|
||||
location: 'global'
|
||||
properties: {
|
||||
groupShortName: 'scaleinag'
|
||||
|
|
@ -1675,9 +1469,11 @@ resource netInterfaceMasterNode 'Microsoft.Network/networkInterfaces@2023-11-01'
|
|||
id: openviduMasterNodeASG.id
|
||||
}
|
||||
]
|
||||
publicIPAddress: isEmptyIp ? null : {
|
||||
id: ipNew ? publicIP_OV_ifNew.id : publicIP_OV_ifExisting.id
|
||||
}
|
||||
publicIPAddress: isEmptyIp
|
||||
? null
|
||||
: {
|
||||
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"
|
||||
},
|
||||
"defaultValue": {
|
||||
"publicIpAddressName": "defaultName"
|
||||
"publicIpAddressName": "ov-publicIpAddress"
|
||||
},
|
||||
"options": {
|
||||
"hideNone": true,
|
||||
|
|
@ -260,13 +260,14 @@
|
|||
"label": "Master Node Instance Type",
|
||||
"toolTip": "Specifies the Azure instance type for your OpenVidu Master Node",
|
||||
"recommendedSizes": [
|
||||
"Standard_B2s"
|
||||
"Standard_B4s",
|
||||
"Standard_B4ms",
|
||||
"Standard_D4ps_v5",
|
||||
"Standard_D4pls_v5"
|
||||
],
|
||||
"constraints": {
|
||||
"allowedSizes": [],
|
||||
"excludedSizes": [],
|
||||
"numAvailabilityZonesRequired": 3,
|
||||
"zone": "3"
|
||||
"excludedSizes": []
|
||||
},
|
||||
"options": {
|
||||
"hideDiskTypeFilter": false
|
||||
|
|
@ -280,14 +281,14 @@
|
|||
"label": "Media Node Instance Type",
|
||||
"toolTip": "Specifies the Azure instance type for your OpenVidu Media Nodes",
|
||||
"recommendedSizes": [
|
||||
"Standard_B2s",
|
||||
"Standard_B4ms"
|
||||
"Standard_B4s",
|
||||
"Standard_B4ms",
|
||||
"Standard_D4ps_v5",
|
||||
"Standard_D4pls_v5"
|
||||
],
|
||||
"constraints": {
|
||||
"allowedSizes": [],
|
||||
"excludedSizes": [],
|
||||
"numAvailabilityZonesRequired": 3,
|
||||
"zone": "3"
|
||||
"excludedSizes": []
|
||||
},
|
||||
"options": {
|
||||
"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,180 @@
|
|||
# ------------------------- 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"
|
||||
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.masterNodeInstanceType))
|
||||
error_message = "The instance type is not valid"
|
||||
}
|
||||
}
|
||||
|
||||
variable "mediaNodeInstanceType" {
|
||||
description = "Specifies the GCE machine type for your OpenVidu Media Nodes"
|
||||
type = string
|
||||
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.mediaNodeInstanceType))
|
||||
error_message = "The instance type is not valid"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
# Docker & Docker Compose will need to be installed on the machine
|
||||
set -eu
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.3.3}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.39.4}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.4.1}"
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-29.0.2}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||
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_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
||||
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-08-13T08-35-41Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.15}"
|
||||
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 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}}"
|
||||
|
|
@ -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 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.7.1}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.7}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.7}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.17.1}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:12.2.0}"
|
||||
|
||||
# Function to compare two version strings
|
||||
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
|
||||
fi
|
||||
|
||||
# Restart Docker and wait for it to start
|
||||
systemctl enable docker
|
||||
systemctl stop docker
|
||||
systemctl start docker
|
||||
wait_for_docker
|
||||
# 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 start docker
|
||||
wait_for_docker
|
||||
fi
|
||||
|
||||
# Create random temp directory
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#!/bin/sh
|
||||
# Docker & Docker Compose will need to be installed on the machine
|
||||
set -eu
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.3.3}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.39.4}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.4.1}"
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-29.0.2}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||
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_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
||||
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-08-13T08-35-41Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.15}"
|
||||
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 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}}"
|
||||
|
|
@ -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 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.7.1}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.7}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.7}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.17.1}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:12.2.0}"
|
||||
|
||||
# Function to compare two version strings
|
||||
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
|
||||
fi
|
||||
|
||||
# Restart Docker and wait for it to start
|
||||
systemctl enable docker
|
||||
systemctl stop docker
|
||||
systemctl start docker
|
||||
wait_for_docker
|
||||
# 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 start docker
|
||||
wait_for_docker
|
||||
fi
|
||||
|
||||
# Create random temp directory
|
||||
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