Compare commits

...

75 Commits

Author SHA1 Message Date
pabloFuente b469cf5455 openvidu-testapp: full track-processors-js capabilities 2025-12-12 15:40:27 +01:00
Carlos Santos d9ebae88fa ov-components: update hover timeout duration in StreamComponent 2025-12-11 13:49:55 +01:00
Piwccle 118d6d370e openvidu-deployment: GCP - Fixes in ARM64 changes and some cleaning. 2025-12-11 12:44:12 +01:00
Carlos Santos 00a9a21de3 ov-components: update element selectors for screen share functionality in tests and add local participant class in layout 2025-12-11 11:27:06 +01:00
Carlos Santos f2363eebd8 ov-components: increase sleep duration in screensharing test for stability 2025-12-11 11:17:07 +01:00
Carlos Santos d9565c07bd ov-components: update chromedriver and selenium-webdriver to latest versions 2025-12-11 11:15:29 +01:00
Carlos Santos ad80e2b3d3 ov-components: reorder imports and add toggleStreamPin method for stream pinning functionality 2025-12-11 11:14:46 +01:00
Carlos Santos 92511e0535 ov-componentes: improve formatting of browser options in selenium configuration 2025-12-11 10:30:56 +01:00
Carlos Santos 3a5f0d28da ov-components: make changes compile library with latest livekit dependencies 2025-12-11 10:06:18 +01:00
Carlos Santos 7c0333bf19 ov-components: update Angular and related dependencies 2025-12-10 18:49:18 +01:00
Piwccle c7f73e36eb openvidu-deployment: GCP - Added zone dependency becasue its required 2025-12-10 18:42:57 +01:00
Piwccle 2806cbcf8b openvidu-deployment: GCP - Removed more unnecesary var.zone 2025-12-10 18:29:26 +01:00
Piwccle 1359ec77fe openvidu-deployment: trying to remove dependency from var.zone 2025-12-10 18:15:58 +01:00
Piwccle 05697f7ab3 openvidu-deployment: GCP - Added ARM64 support 2025-12-10 18:12:08 +01:00
Carlos Santos 41dc440ef8 ov-components: remove unnecessary peer flags and update package versions 2025-12-10 17:50:04 +01:00
Piwccle a32efa876f openvidu-deployment: GCP - Fix to zone variable dependency in Single Node deployments and Elastic 2025-12-10 17:38:08 +01:00
Carlos Santos 8f42f50a01 ov-components: rename workflow to reflect E2E testing 2025-12-10 17:32:39 +01:00
Carlos Santos 87ec92ecc8 ov-components: optimize background processor application for video tracks 2025-12-05 16:41:49 +01:00
Carlos Santos 435db94254 ov-components: streamline virtual background processing 2025-12-05 15:40:40 +01:00
Carlos Santos 007297e4ff ov-components: update dependencies and refactor OpenVidu service
- Updated @livekit/track-processors from 0.6.1 to 0.7.0 in openvidu-components-angular/package.json
- Updated livekit-client from 2.15.15 to 2.16.0 in openvidu-components-angular/package.json
- Updated livekit-client from ^2.15.0 to ^2.16.0 in openvidu-components-angular/projects/openvidu-components-angular/package.json
- Updated @livekit/track-processors from ^0.6.0 to ^0.7.0 in openvidu-components-angular/projects/openvidu-components-angular/package.json
- Refactored OpenViduService to change roomOptions.e2ee to roomOptions.encryption for E2EE configuration
- Reorganized import statements in OpenViduService for better readability
2025-12-05 15:39:55 +01:00
Carlos Santos 895cf0e72c ov-components: Add screen track class to local participant tracks
participant-panel: Bind participant name to participant container
2025-12-04 19:51:48 +01:00
Carlos Santos 3531932c88 ov-components: Fixed directive value update
Ensures consistent identification of participant elements by using the participant identity, facilitating easier manipulation and tracking of elements.

Improves component updates by storing the remote participants value locally.
2025-12-02 21:00:07 +01:00
cruizba a3810f4f51 openvidu-deployment: Azure - Missed to remove contraints for Media Nodes size. 2025-12-02 20:47:00 +01:00
cruizba aa08432ea8 openvidu-deployment: Azure - Fix wrong sku and offer for ubuntu 24 2025-12-02 20:04:38 +01:00
cruizba 7bc782b7c0 openvidu-deployment: Azure - Remove unnecessary constraints from Azure UI definition files for OpenVidu deployments. 2025-12-02 19:39:49 +01:00
cruizba 75fc732c69 openvidu-deployment: Refactor Azure VM size parameters for OpenVidu deployment ARM support. 2025-12-02 19:21:16 +01:00
cruizba 0cfc342153 openvidu-deployment: Azure - Add ARM64 instance types and update Ubuntu to ubuntu 24 2025-12-02 18:29:44 +01:00
cruizba 712377a1b5 local-meet: remove unnecessary env var in mongo service 2025-12-02 15:53:33 +01:00
cruizba 4cffa3d8b9 local-meet: Add ARM64 support 2025-12-02 15:50:06 +01:00
cruizba d49d7ef943 openvidu-deployment: AWS - ha: Increase CloudFormation WaitCondition. 2025-12-01 21:54:57 +01:00
cruizba 72cad07118 openvidu-deployment: AWS - ha: Remove redundant crond.service enable and start commands from deployment script 2025-12-01 21:01:11 +01:00
cruizba 352fa03b0a openvidu-deployment: AWS - Support for ARM64 instances 2025-12-01 20:36:57 +01:00
cruizba d0b2bab7b1 local-meet: Install tzdata on images and use timezone of host. 2025-11-29 00:18:17 +01:00
Carlos Santos 7c43d73066 ov-components: Refactor GitHub Actions workflow for Angular tests and enhance E2E testing structure 2025-11-27 20:05:16 +01:00
Carlos Santos 9918b07f51 ov-components: Add directive for injecting custom menu items into toolbar more options 2025-11-27 20:05:16 +01:00
Carlos Santos 171a5104ae ov-components: Add directive for injecting custom elements into settings panel 2025-11-27 20:05:16 +01:00
Carlos Santos e59ed89a0b ov-components: Mark pre-join component tutorial link as internal 2025-11-27 20:05:16 +01:00
cruizba 8cdc71e22f local-meet: Add MEET_MONGO_URI environment variable for OpenVidu Meet service 2025-11-25 12:49:00 +01:00
Carlos Santos a0de27a78e ov-components: Refine stream video controls styling and improve hover effects 2025-11-21 13:44:39 +01:00
Carlos Santos 9c89adbdee Revert "ov-components: Optimize layout handling with caching and resize improvements"
This reverts commit 0cf5101931.
2025-11-20 18:11:40 +01:00
Carlos Santos 0cf5101931 ov-components: Optimize layout handling with caching and resize improvements 2025-11-20 17:39:14 +01:00
Carlos Santos 7c17e19cbb ov-components: Remove debug log for configuration in GlobalConfigService constructor 2025-11-20 17:39:14 +01:00
cruizba f4c4ca8cec openvidu-deployment: Update Docker version to 29.0.2 in installation scripts 2025-11-20 12:10:23 +01:00
Piwccle c22e957d4d openvidu-deployment: Refactor GCP Terraform configuration for OpenVidu deployment:
- Conditional creation of regional static IP for Network Load Balancer
- Improved validation messages for variables
- Adjusted default values for max number of media nodes and master nodes disk size
2025-11-19 18:42:44 +01:00
Piwccle 3d3089f479 openvidu-deployment: HA deployment GCP done and tested 2025-11-19 10:41:42 +01:00
cruizba 41fe3d718a openvidu-deployment: Typo docker compose version. 2025-11-18 19:14:28 +01:00
Carlos Santos 3be9dd6741 ov-components: Add getter for room name in ParticipantModel 2025-11-18 18:01:27 +01:00
cruizba cabb761024 openvidu-deployment: Only restart docker daemon on install if needed. 2025-11-17 14:03:17 +01:00
cruizba 70a9f1b2b0 openvidu-deployment: Update Docker and Docker Compose versions across installation scripts 2025-11-14 19:04:40 +01:00
Carlos Santos 8688211277 ov-components: Improve audio detection in stream UI tests to handle timing issues with retries 2025-11-14 13:44:02 +01:00
Carlos Santos 5a99839ed7 ov-components: Enhance screensharing tests to address pinning bugs and add utility methods for stream management 2025-11-14 13:30:47 +01:00
Carlos Santos 19a5c21162 ov-components: improve stream detection logic in stream UI tests to handle timing races and ensure accurate local participant status 2025-11-14 12:15:52 +01:00
Carlos Santos bea3b8e70a ov-components: Refactor ActionService and related tests to improve dialog handling and mock implementations 2025-11-14 12:02:42 +01:00
Carlos Santos 0f075008a4 ov-components: Refactor DeviceService to improve permission handling and add utility methods
- Extracted permission strategies into a separate method for better readability.
- Created a method to handle permission strategy attempts and return valid devices.
- Added a utility method to filter out invalid devices.
- Improved error handling in getMediaDevicesFirefox method.

test: Add unit tests for DocumentService

- Implemented comprehensive tests for DocumentService methods including toggleFullscreen, isSmallElement, and fullscreen handling.
- Mocked document and element interactions to ensure proper functionality.

feat: Implement E2EE service with encryption and decryption capabilities

- Developed E2eeService to handle end-to-end encryption with methods for encrypting and decrypting data.
- Added caching for decrypted strings to optimize performance.
- Included tests for various scenarios including encryption failures and binary data handling.

test: Add unit tests for PanelService

- Created tests for PanelService to validate panel opening, closing, and state management.
- Ensured proper emissions from panelStatusObs during panel state changes.

fix: Initialize externalType in PanelService to avoid undefined state

- Set default value for externalType to an empty string to prevent potential issues.
2025-11-13 20:16:01 +01:00
Carlos Santos b1fb3406a0 ov-components: Added missing translation 2025-11-13 13:41:54 +01:00
Carlos Santos f3e551fc4a ov-components: Refactor data handling in SessionComponent to improve payload processing and error logging 2025-11-12 17:51:26 +01:00
Carlos Santos ba80504c9e ov-components: Improve error handling for encryption errors in RoomEvent 2025-11-12 12:01:25 +01:00
Carlos Santos c50b4a6d2f Remove outdated package-lock.json from openvidu-components-angular project 2025-11-12 11:21:11 +01:00
Carlos Santos fb1dc9d95a ov-components: Enhance data handling in SessionComponent and add safeJsonParse utility 2025-11-11 17:30:38 +01:00
Carlos Santos 9d75a429a6 ov-components: Updated livekit-client dependency 2025-11-11 16:23:20 +01:00
Carlos Santos bb62986000 Updates participant name logic
Updates participant name input id.

Improves participant name visibility logic
by considering audio-only streams.
2025-11-10 17:29:58 +01:00
Carlos Santos 48eec08509 ov-components: Implement E2EE support for chat messages and participant names and add E2EE service 2025-11-10 12:49:54 +01:00
Carlos Santos 41bca24bfa Merge branch 'e2ee_support' 2025-11-05 17:51:57 +01:00
Carlos Santos 9950a2ba21 ov-components: Add E2EEKey directive and integrate E2EE configuration in OpenViduService
ov-components: Update E2EE error messages for improved clarity across multiple languages
2025-11-05 17:51:42 +01:00
Carlos Santos 4bf413cffc Merge branch 'gcp' 2025-10-29 18:28:54 +01:00
Piwccle d68cb4933e Add Terraform configuration for OpenVidu deployment on GCP
Changed structure to be more consistant with the terraform standard and removed some resources to try

Refactor terraform main file to be more alike with aws and azure scripts and fixed some things that were wrong in the install script. Changed variables.tf and output.tf as needed

Refactor firewall rules and streamline user data scripts for OpenVidu deployment on GCP

added Elastic deployment for GCP and changed default values of instance type in Single Node and Single Node PRO

openvidu-deployment_ gcp - changed output.tf in all deployments to output the link to secret manager; changed the name of the instance resource of openvidu single node pro; fixed some things that were broken in elastic terraform file and adjusted times for the lambda and the cronjob
2025-10-29 18:26:56 +01:00
Carlos Santos 17ed624e40 ov-components: Removes redundant computed property
Simplifies the template by directly using the `showLeaveButton` property.

Removes the now-unnecessary computed property that duplicated the value.
2025-10-28 17:18:18 +01:00
Carlos Santos 6c9a8a1bc2 ov-components: Update imports in global-config.service.ts 2025-10-21 20:19:31 +02:00
Carlos Santos de8639ad63 ov-components: Updated testapp dependencies 2025-10-21 19:35:26 +02:00
Carlos Santos c576133b42 ov-components: Adds build and copy scripts
Adds `cpx` and `rimraf` packages.
Updates library build script to remove destination directory and copy distribution files.
Updates library package configurations.
2025-10-21 19:22:12 +02:00
Carlos Santos bb47c3696c ov-components: Configures root injection for overlay
Configures root injection for the CDK overlay container.

This ensures the overlay container is properly initialized and available throughout the application.
It also makes the overlay available application wide and solves the problem of injecting multiple instances.
2025-10-21 19:10:27 +02:00
cruizba 1a3edb9a61 openvidu-deployment: Update Docker images and versions in deployment scripts 2025-10-17 19:55:18 +02:00
Piwccle b7e715361e openvidu-deployment: azure - changed "defaultName" of PublicIpAddress and changed Action Group name 2025-10-15 18:45:31 +02:00
cruizba 1d4bcdf54f local-meet: bumpt to 3.4.1 2025-10-13 20:41:05 +02:00
GitHub Actions 6ac6e1f2de Revert "Bump version to 3.4.1"
This reverts commit 6b9e9da2f1.
2025-10-13 16:57:47 +00:00
134 changed files with 33038 additions and 26069 deletions

View File

@ -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

View File

@ -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.");
});
});

View File

@ -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;
}
}

View File

@ -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');
}
});
});

View File

@ -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

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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
}
}
}

View File

@ -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"
}

View File

@ -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
})

View File

@ -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;
}

View File

@ -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() {}
}

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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();

View File

@ -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>

View File

@ -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

View File

@ -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,

View File

@ -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';

View File

@ -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');

View File

@ -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"

View File

@ -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>

View File

@ -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 {

View File

@ -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

View File

@ -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()"

View File

@ -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(
() =>

View File

@ -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 -->

View File

@ -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++;
}

View File

@ -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>
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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

View File

@ -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';

View File

@ -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({

View File

@ -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 });
}
}

View File

@ -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 });
}
}
}

View File

@ -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
) {}
}

View File

@ -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
]
})

View File

@ -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": "此参与者使用了不同的安全密钥。无法显示视频。"
}
}

View File

@ -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."
}
}

View File

@ -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."
}
}

View File

@ -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."
}
}

View File

@ -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."
}
}

View File

@ -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": "यह प्रतिभागी एक अलग सुरक्षा कुंजी का उपयोग कर रहा है। वीडियो प्रदर्शित नहीं किया जा सकता।"
}
}

View File

@ -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."
}
}

View File

@ -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": "この参加者は異なるセキュリティキーを使用しています。ビデオを表示できません。"
}
}

View File

@ -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."
}
}

View File

@ -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."
}
}

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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())
];

View File

@ -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() {
}
}

View File

@ -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();
}));
});

View File

@ -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() {

View File

@ -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);
});
});

View File

@ -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;
}
}
}

View File

@ -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 {

View File

@ -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');
}

View File

@ -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 [];
}
}

View File

@ -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);
});
});
});

View File

@ -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();
}
}
}

View File

@ -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);
});
});

View File

@ -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();
}
}

View File

@ -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}`);
}
}

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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 {

View File

@ -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
};
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
export const safeJsonParse = <T = any>(text: string): T | null => {
try {
return JSON.parse(text) as T;
} catch (e) {
return null;
}
};

View File

@ -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';

View File

@ -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>;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,10 @@
import { of } from 'rxjs';
export class TranslateServiceMock {
instant(key: string): string {
return key;
}
get(key: string) {
return of(key);
}
}

View File

@ -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"

View File

@ -245,7 +245,6 @@ services:
setup:
image: docker.io/busybox:1.37.0
platform: linux/amd64
restart: "no"
volumes:
- minio-data:/minio

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}"
}

View File

@ -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

View File

@ -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 = ""
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = ""
}

View File

@ -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
}

View File

@ -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)

View File

@ -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