Compare commits

..

No commits in common. "master" and "v3.4.1" have entirely different histories.

135 changed files with 26093 additions and 33337 deletions

View File

@ -1,4 +1,4 @@
name: openvidu-components-angular E2E
name: openvidu-components-angular Tests
on:
push:
@ -17,10 +17,6 @@ on:
required: false
default: ''
env:
NODE_VERSION: '20'
CHROME_IMAGE: selenium/standalone-chrome:138.0
jobs:
test_setup:
name: Test setup
@ -33,7 +29,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
node-version: '20'
- name: Commit URL
run: echo https://github.com/OpenVidu/openvidu/commit/${{ inputs.commit_sha || github.sha }}
- name: Send Dispatch Event
@ -49,41 +45,10 @@ 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"'"}}'
e2e_tests:
nested_events:
needs: test_setup
name: ${{ matrix.name }}
name: Nested events
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
@ -92,16 +57,116 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
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: Install dependencies
run: |
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
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
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -111,7 +176,303 @@ jobs:
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run ${{ matrix.script }} --prefix openvidu-components-angular
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
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main

View File

@ -1,232 +0,0 @@
# E2E Testing Documentation
## Table of Contents
1. [Overview](#overview)
2. [Technology Stack](#technology-stack)
3. [Test Coverage](#test-coverage)
4. [Test Types](#test-types)
5. [Test Files Structure](#test-files-structure)
6. [Running Tests](#running-tests)
## Overview
This directory contains end-to-end (E2E) tests for the OpenVidu Components Angular library. The test suite validates the complete functionality of the library components, including UI interactions, media handling, real-time communication features, and API directives.
## Technology Stack
The E2E test suite is built using the following technologies:
- **Selenium WebDriver**: Browser automation framework for UI testing
- **Jasmine**: Testing framework providing describe/it syntax and assertions
- **TypeScript**: Programming language for type-safe test development
- **ChromeDriver**: Chrome browser automation driver
- **Fake Media Devices**: Simulated audio/video devices for testing without real hardware
### Key Dependencies
- `selenium-webdriver` (v4.39.0): Core automation library
- `jasmine` (v5.3.1): Test runner and assertion framework
- `chromedriver` (v143.0.0): Chrome browser driver
- `@types/selenium-webdriver`: TypeScript type definitions
## Test Coverage
The test suite provides comprehensive coverage across the following functional areas:
### Core Features
- **API Directives**: Component configuration and display options (41 tests)
- **Events**: Component lifecycle and interaction events (24 tests)
- **Stream Management**: Video/audio stream handling and display (32 tests)
- **Media Devices**: Device selection, permissions, and virtual devices (7 tests)
- **Panels**: UI navigation and panel management (6 tests)
- **Toolbar**: Media control buttons and functionality (2 tests)
- **Chat**: Messaging functionality and UI (3 tests)
- **Screen Sharing**: Screen share capabilities and behavior (8 tests)
- **Virtual Backgrounds**: Background effects and manipulation (5 tests)
### Nested Components Testing
- **Structural Directives**: Custom component templates and layouts (30 tests)
- **Attribute Directives**: Component visibility and behavior controls (16 tests)
- **Events**: Nested component event handling (10 tests)
### Internal Functionality
- **Internal Directives**: Library-specific directive behavior (5 tests)
### Disabled Tests
- **Captions**: Captions feature tests (currently commented out, awaiting implementation)
## Test Types
### 1. UI Interaction Tests
Tests that validate user interface elements and their interactions:
- Button visibility and functionality
- Panel opening/closing
- Component rendering
- Layout behavior
- Visual element presence
**Example**: Testing microphone mute/unmute button functionality
### 2. Media Device Tests
Tests focused on audio/video device handling:
- Device selection and switching
- Virtual device integration
- Permission handling
- Track management
- Media stream validation
**Example**: Testing video device replacement with custom virtual devices
### 3. API Directive Tests
Tests verifying component configuration through Angular directives:
- Component display settings (minimal UI, language, prejoin)
- Feature toggles (buttons, panels, toolbar elements)
- Media settings (video/audio enabled/disabled)
- UI customization options
**Example**: Testing hiding toolbar buttons via directives
### 4. Event Tests
Tests validating event emission and handling:
- Component lifecycle events
- User interaction events
- Media state change events
- Panel state change events
- Recording/broadcasting events
**Example**: Testing onVideoEnabledChanged event emission
### 5. Multi-Participant Tests
Tests simulating multiple participants:
- Message exchange between participants
- Remote participant display
- Screen sharing with multiple users
- Participant panel functionality
**Example**: Testing chat message reception between two participants
### 6. Structural Customization Tests
Tests for component template customization:
- Custom toolbar templates
- Custom panel templates
- Custom layout templates
- Custom stream templates
- Additional component injection
**Example**: Testing custom toolbar rendering with additional buttons
### 7. Screen Sharing Tests
Tests specific to screen sharing features:
- Screen share toggle
- Pin/unpin behavior
- Multiple simultaneous screen shares
- Screen share with audio/video states
**Example**: Testing screen share video pinning behavior
### 8. Virtual Background Tests
Tests for background effects:
- Background panel interaction
- Effect application
- Background state management
- Prejoin and in-room background handling
**Example**: Testing background effect application in prejoin
## Test Files Structure
```
e2e/
├── api-directives.test.ts # API directive configuration tests (41 tests)
├── events.test.ts # Component event emission tests (24 tests)
├── stream.test.ts # Video/audio stream tests (32 tests)
├── media-devices.test.ts # Device handling tests (7 tests)
├── panels.test.ts # Panel navigation tests (6 tests)
├── toolbar.test.ts # Toolbar functionality tests (2 tests)
├── chat.test.ts # Chat feature tests (3 tests)
├── screensharing.test.ts # Screen sharing tests (8 tests)
├── virtual-backgrounds.test.ts # Virtual backgrounds tests (5 tests)
├── internal-directives.test.ts # Internal directive tests (5 tests)
├── captions.test.ts # Captions tests (currently disabled)
├── config.ts # Test configuration
├── selenium.conf.ts # Selenium browser configuration
├── utils.po.test.ts # Page Object utilities
└── nested-components/
├── structural-directives.test.ts # Template customization tests (30 tests)
├── attribute-directives.test.ts # Visibility directive tests (16 tests)
└── events.test.ts # Nested event tests (10 tests)
```
### Support Files
- **config.ts**: Global test configuration and timeout settings
- **selenium.conf.ts**: Browser capabilities, Chrome options, and test environment setup
- **utils.po.test.ts**: Page Object Model implementation with reusable helper methods
## Running Tests
### Individual Test Suites
Execute specific test files using npm scripts:
```bash
# API directives tests
npm run e2e:lib-directives
# Event tests
npm run e2e:lib-events
# Chat tests
npm run e2e:lib-chat
# Media devices tests
npm run e2e:lib-media-devices
# Panel tests
npm run e2e:lib-panels
# Screen sharing tests
npm run e2e:lib-screensharing
# Stream tests
npm run e2e:lib-stream
# Toolbar tests
npm run e2e:lib-toolbar
# Virtual backgrounds tests
npm run e2e:lib-virtual-backgrounds
# Internal directives tests
npm run e2e:lib-internal-directives
# All nested component tests
npm run e2e:nested-all
# Nested events tests
npm run e2e:nested-events
# Nested structural directives tests
npm run e2e:nested-structural-directives
# Nested attribute directives tests
npm run e2e:nested-attribute-directives
```
### Test Execution Process
1. Tests are compiled from TypeScript to JavaScript using `tsc --project ./e2e`
2. Jasmine executes the compiled tests from `./e2e/dist/` directory
3. Selenium WebDriver launches Chrome browser instances
4. Tests interact with the application running at `http://localhost:4200`
5. Test results are reported in the console
### Environment Modes
Tests support two execution modes:
- **DEV Mode**: Local development with visible browser
- **CI Mode**: Continuous integration with headless browser and additional Chrome flags
Mode is controlled via `LAUNCH_MODE` environment variable.

View File

@ -205,151 +205,112 @@ 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`);
// ==================== 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
// await utils.checkLayoutPresent();
it('should NOT have multiple screens pinned when both participants share screen', async () => {
const roomName = 'pinBugCase1';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
// // Clicking to screensharing button
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
// expect(await screenshareButton.isDisplayed()).toBeTrue();
// await screenshareButton.click();
// 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);
// await utils.waitForElement('.OV_big');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// 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)`);
// const muteVideoButton = await utils.waitForElement('#camera-btn');
// await muteVideoButton.click();
// Participant B joins
const tabs = await utils.openTab(fixedUrl);
await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent();
await browser.sleep(1000);
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// });
// 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)`);
// 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 shares screen
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
// await browser.get(`${url}&prejoin=false`);
// 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
// await utils.checkLayoutPresent();
// 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
// // Clicking to screensharing button
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
// await screenshareButton.click();
// 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)`);
// await utils.waitForElement('.OV_big');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
// 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.');
});
// // Muting camera video
// const muteVideoButton = await utils.waitForElement('#camera-btn');
// await muteVideoButton.click();
it('should NOT re-pin manually unpinned screen when new participant joins', async () => {
const roomName = 'pinBugCase2';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// 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);
// await browser.sleep(500);
// expect(await utils.isPresent('#status-mic')).toBeFalse();
// Verify A's screen is auto-pinned
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
// // Checking if audio is muted after join the room
// isAudioEnabled = await browser.executeScript(audioEnableScript);
// expect(isAudioEnabled).toBeTrue();
// 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);
// // Unmuting camera
// await muteVideoButton.click();
// await browser.sleep(1000);
// 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)`);
// await utils.waitForElement('.camera-type');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
// });
// 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);
}
// it('should camera come back with audio muted when screensharing', async () => {
// let element, isAudioEnabled;
// 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');
// const getAudioScript = (className: string) => {
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
// };
// 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 browser.get(`${url}&prejoin=false`);
// 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");
// await utils.checkLayoutPresent();
// Participant C joins the room
const tab3 = await utils.openTab(fixedUrl);
await browser.switchTo().window(tab3[2]);
await utils.checkLayoutPresent();
await browser.sleep(1500);
// // Clicking to screensharing button
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
// await screenshareButton.click();
// Switch back to B's tab
await browser.switchTo().window(tabs[1]);
await browser.sleep(1000);
// await utils.waitForElement('.screen-type');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
// 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)`);
// // Mute camera
// const muteVideoButton = await utils.waitForElement('#camera-btn');
// await muteVideoButton.click();
// 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.');
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// expect(await utils.isPresent('#status-mic')).toBeFalse();
// Switch back to A's tab to verify
await browser.switchTo().window(tabs[0]);
await browser.sleep(500);
// // Checking if audio is muted after join the room
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
// expect(isAudioEnabled).toBeTrue();
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
console.log(`[Tab A] After C joins: ${pinnedCountA2} pinned stream(s)`);
// // Mute audio
// const muteAudioButton = await utils.waitForElement('#mic-btn');
// await muteAudioButton.click();
// 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.");
});
// 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();
// });
});

View File

@ -56,8 +56,7 @@ 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)) as chrome.Options
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
};
export const NestedConfig: BrowserConfig = {
@ -65,14 +64,13 @@ 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)) as chrome.Options
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
};
export function getBrowserOptionsWithoutDevices() {
if (LAUNCH_MODE === 'CI') {
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI) as chrome.Options;
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI);
} else {
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices) as chrome.Options;
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices);
}
}

View File

@ -1,4 +1,4 @@
import { Builder, IRectangle, WebDriver } from 'selenium-webdriver';
import { Builder, ILocation, IRectangle, ISize, 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_participant.OV_screen');
await utils.waitForElement('#local-element-screen_share');
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_participant.OV_screen');
await utils.waitForElement('#local-element-screen_share');
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_participant.OV_screen');
await utils.waitForElement('#local-element-screen_share');
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_participant.OV_screen');
await utils.waitForElement('#local-element-screen_share');
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_participant.OV_screen');
await utils.waitForElement('#local-element-screen_share');
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,47 +681,12 @@ describe('Stream UI controls and interaction features', () => {
await browser.switchTo().window(tabs[0]);
// Wait with retries for audio detection to appear (handles timing issues)
const maxRetries = 5;
const retryInterval = 1000;
let audioDetected = false;
await utils.waitForElement('.OV_stream.remote.speaking');
expect(await utils.getNumberOfElements('.OV_stream.remote.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');
}
// 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);
});
});

View File

@ -1,7 +1,7 @@
import * as fs from 'fs';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
import * as fs from 'fs';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
type PNGWithMetadata = PNG & { data: Buffer };
export class OpenViduComponentsPO {
@ -279,50 +279,4 @@ 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,110 +1,108 @@
{
"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"
"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"
}

View File

@ -1,6 +1,6 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "./dist",
"dest": "../../dist/openvidu-components-angular",
"lib": {
"entryFile": "src/public-api.ts"
}

View File

@ -0,0 +1,418 @@
{
"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,8 +1,5 @@
{
"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"
},
@ -15,8 +12,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.16.0",
"@livekit/track-processors": "^0.7.0"
"livekit-client": "^2.15.0",
"@livekit/track-processors": "^0.6.0"
},
"version": "3.4.0"
}

View File

@ -5,13 +5,11 @@ import { Component } from '@angular/core';
*/
@Component({
selector: 'ov-audio-wave',
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>
`,
template: `<div class="audio-container">
<div class="stick normal play"></div>
<div class="stick loud play"></div>
<div class="stick normal play"></div>
</div>`,
styleUrls: ['./audio-wave.component.scss'],
standalone: false
})

View File

@ -0,0 +1,31 @@
.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,31 @@
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,15 +4,13 @@
#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_screen: track.isScreenTrack
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
}"
[id]="'participant-' + track.participant.identity"
[id]="'local-element-' + track.source"
cdkDrag
cdkDragBoundary=".layout"
[cdkDragDisabled]="!track.isMinimized"
@ -29,13 +27,11 @@
<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_screen: track.isScreenTrack
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
}"
>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>

View File

@ -8,13 +8,7 @@ import { Track } from 'livekit-client';
@Component({
selector: 'ov-media-element',
template: `
<ov-video-poster
@posterAnimation
[showAvatar]="showAvatar"
[nickname]="avatarName"
[color]="avatarColor"
[hasEncryptionError]="hasEncryptionError"
></ov-video-poster>
<ov-avatar-profile @posterAnimation *ngIf="showAvatar" [name]="avatarName" [color]="avatarColor"></ov-avatar-profile>
<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>
`,
@ -35,11 +29,10 @@ export class MediaElementComponent implements AfterViewInit, OnDestroy {
private _muted: boolean = false;
private previousTrack: Track | null = null;
@Input() showAvatar: boolean = false;
@Input() avatarColor: string = '#000000';
@Input() avatarName: string = 'User';
@Input() isLocal: boolean = false;
@Input() hasEncryptionError: boolean = false;
@Input() showAvatar: boolean;
@Input() avatarColor: string;
@Input() avatarName: string;
@Input() isLocal: boolean;
@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">{{ 'PANEL.ACTIVITIES.TITLE' | translate }}</h3>
<h3 class="panel-title">Activities</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.chatMessages$.pipe(takeUntil(this.destroy$)).subscribe((messages: ChatMessage[]) => {
this.chatService.messagesObs.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" [attr.data-participant-name]="participantDisplayName">
<div class="participant-container" [attr.data-participant-id]="_participant?.sid">
<!-- Avatar section with dynamic color -->
<div
class="participant-avatar"
[style.background-color]="_participant?.colorProfile"
[attr.aria-label]="'Avatar for ' + participantDisplayName"
>
<mat-icon>{{ _participant.hasEncryptionError ? 'lock_person' : 'person' }}</mat-icon>
<mat-icon>person</mat-icon>
</div>
<!-- Content section with name and status -->
@ -28,48 +28,42 @@
</div>
</div>
@if (!_participant.hasEncryptionError) {
<div class="participant-subtitle">
<span class="status-indicator">
{{ _participant | tracksPublishedTypes }}
</span>
</div>
}
<div class="participant-subtitle">
<span class="status-indicator">
{{ _participant | tracksPublishedTypes }}
</span>
</div>
</div>
<!-- Action buttons section -->
@if (!_participant.hasEncryptionError) {
<div class="participant-action-buttons">
<!-- Mute/Unmute button for remote participants -->
<button
mat-icon-button
id="mute-btn"
*ngIf="!isLocalParticipant && showMuteButton"
[class.warn-btn]="_participant?.isMutedForcibly"
(click)="toggleMuteForcibly()"
[disabled]="!_participant"
[disableRipple]="true"
[attr.aria-label]="
_participant?.isMutedForcibly
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
"
[matTooltip]="
_participant?.isMutedForcibly
? ('PANEL.PARTICIPANTS.UNMUTE' | translate)
: ('PANEL.PARTICIPANTS.MUTE' | translate)
"
>
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
</button>
<div class="participant-action-buttons">
<!-- Mute/Unmute button for remote participants -->
<button
mat-icon-button
id="mute-btn"
*ngIf="!isLocalParticipant && showMuteButton"
[class.warn-btn]="_participant?.isMutedForcibly"
(click)="toggleMuteForcibly()"
[disabled]="!_participant"
[disableRipple]="true"
[attr.aria-label]="
_participant?.isMutedForcibly
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
"
[matTooltip]="
_participant?.isMutedForcibly ? ('PANEL.PARTICIPANTS.UNMUTE' | translate) : ('PANEL.PARTICIPANTS.MUTE' | translate)
"
>
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
</button>
<!-- External item elements with improved structure -->
<div class="external-elements" *ngIf="hasExternalElements">
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
</div>
<!-- External item elements with improved structure -->
<div class="external-elements" *ngIf="hasExternalElements">
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
</div>
}
</div>
</div>
</mat-list-item>
</mat-list>

View File

@ -93,12 +93,6 @@
</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, ContentChild, EventEmitter, OnInit, Output, TemplateRef } from '@angular/core';
import { Component, EventEmitter, OnInit, Output } 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,7 +7,6 @@ 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
@ -24,14 +23,6 @@ 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;
@ -41,14 +32,6 @@ 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 '../../../test-helpers/action.service.mock';
import { ActionServiceMock } from '../../services/action/action.service.mock';
import { ChatService } from '../../services/chat/chat.service';
import { ChatServiceMock } from '../../services/chat/chat.service.mock';

View File

@ -50,8 +50,6 @@ 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
@ -140,8 +138,7 @@ export class SessionComponent implements OnInit, OnDestroy {
private backgroundService: VirtualBackgroundService,
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService,
protected viewportService: ViewportService,
private e2eeService: E2eeService
protected viewportService: ViewportService
) {
this.log = this.loggerSrv.get('SessionComponent');
this.setupTemplates();
@ -233,8 +230,7 @@ export class SessionComponent implements OnInit, OnDestroy {
}
// this.subscribeToCaptionLanguage();
this.subscribeToEncryptionErrors();
this.subscribeToActiveSpeakersChanged();
this.subcribeToActiveSpeakersChanged();
this.subscribeToParticipantConnected();
this.subscribeToTrackSubscribed();
this.subscribeToTrackUnsubscribed();
@ -265,18 +261,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error);
}
}
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() {
subcribeToActiveSpeakersChanged() {
this.room.on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {
this.participantService.setSpeaking(speakers);
});
@ -465,134 +450,81 @@ export class SessionComponent implements OnInit, OnDestroy {
private subscribeToDataMessage() {
this.room.on(
RoomEvent.DataReceived,
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;
}
(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;
const participantIdentity = storedParticipant?.identity || '';
const participantName = storedParticipant?.name || '';
case DataTopic.RECORDING_DELETED:
this.log.d('RECORDING_DELETED', event);
this.recordingService.deleteRecording(event);
break;
if (this.e2eeService.isEnabled) {
payload = await this.decryptIfNeeded(topic, payload, participantIdentity);
}
case DataTopic.RECORDING_FAILED:
this.log.d('RECORDING_FAILED', event);
this.recordingService.setRecordingFailed(event.error);
break;
const rawText = decoder.decode(payload);
this.log.d('DataReceived (raw)', { topic });
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 eventMessage = safeJsonParse(rawText);
if (!eventMessage) {
this.log.w('Discarding data: malformed JSON', rawText);
return;
}
case DataTopic.BROADCASTING_STOPPING:
this.broadcastingService.setBroadcastingStopping();
break;
case DataTopic.BROADCASTING_STOPPED:
this.broadcastingService.setBroadcastingStopped();
break;
this.log.d(`Data event received: ${topic}`);
case DataTopic.BROADCASTING_FAILED:
this.broadcastingService.setBroadcastingFailed(event.error);
break;
// Dispatch handling
this.handleDataEvent(topic, eventMessage, participantName || participantIdentity || 'Unknown');
} catch (err) {
this.log.e('Unhandled error processing DataReceived', err);
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 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="participant-name-input"
id="name-input"
type="text"
maxlength="20"
[(ngModel)]="name"

View File

@ -12,7 +12,7 @@
#streamContainer
>
<div
*ngIf="!isMinimal && showParticipantName && !_track.isAudioTrack || (_track.isAudioTrack && _track.participant.onlyHasAudioTracks)"
*ngIf="!isMinimal && showParticipantName"
id="participant-name-container"
class="participant-name"
[class.fullscreen]="isFullscreen"
@ -34,50 +34,47 @@
[avatarName]="_track.participant.name"
[muted]="_track.isMutedForcibly"
[isLocal]="_track.participant.isLocal"
[hasEncryptionError]="_track.participant.hasEncryptionError"
></ov-media-element>
@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 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>
</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,28 +47,25 @@
.stream-video-controls {
background-color: var(--ov-primary-action-color);
border-radius: var(--ov-video-radius);
backdrop-filter: blur(8px);
width: fit-content;
height: 44px;
height: 50px;
opacity: 0.5;
position: absolute;
display: inline-grid;
z-index: 9999;
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);
margin: auto;
bottom: 0;
right: 0;
left: 0;
top: 0;
// border: 2px solid var(--ov-text-primary-color);
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 = 2000;
private readonly HOVER_TIMEOUT = 3000;
/**
* @ignore

View File

@ -177,11 +177,6 @@
</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>
@ -203,7 +198,7 @@
}
<!-- Leave session button -->
@if (showLeaveButton) {
@if (showLeaveButtonDirect()) {
<button
mat-icon-button
(click)="onLeaveClick()"

View File

@ -1,9 +1,8 @@
import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef, computed, inject } from '@angular/core';
import { Component, 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
@ -72,21 +71,6 @@ 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;
@ -112,6 +96,9 @@ 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,12 +82,7 @@
(captionsToggled)="onCaptionsToggle()"
(settingsToggled)="toggleSettings()"
(leaveClicked)="disconnect()"
>
<!-- Inject additional menu items via content projection -->
<ng-container *ovToolbarMoreOptionsAdditionalMenuItems>
<ng-container *ngTemplateOutlet="externalMoreOptionsAdditionalMenuItems?.template"></ng-container>
</ng-container>
</ov-toolbar-media-buttons>
></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, ToolbarMoreOptionsAdditionalMenuItemsDirective } from '../../directives/template/internals.directive';
import { LeaveButtonDirective } from '../../directives/template/internals.directive';
/**
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
@ -80,28 +80,6 @@ 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
*/
@ -516,8 +494,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
this._externalAdditionalButtons,
this._externalAdditionalPanelButtons,
this._externalLeaveButton,
this._externalMoreOptionsAdditionalMenuItems
this._externalLeaveButton
);
// Apply templates to component properties for backward compatibility
@ -538,9 +515,6 @@ 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;
}
}
/**
@ -792,7 +766,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToChatMessages() {
this.chatService.chatMessages$.pipe(skip(1), takeUntil(this.destroy$)).subscribe((messages) => {
this.chatService.messagesObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((messages) => {
if (!this.panelService.isChatPanelOpened()) {
this.unreadMessages++;
}

View File

@ -1,31 +0,0 @@
@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

@ -1,74 +0,0 @@
.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

@ -1,20 +0,0 @@
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,11 +96,6 @@
<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>
@ -132,11 +127,7 @@
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
(onLangChanged)="onLangChanged.emit($event)"
>
<ng-container *ovSettingsPanelGeneralAdditionalElements>
<ng-container *ngTemplateOutlet="ovSettingsPanelGeneralAdditionalElementsTemplate"></ng-container>
</ng-container>
</ov-settings-panel>
></ov-settings-panel>
</ng-template>
<ng-template #activitiesPanel>

View File

@ -62,12 +62,9 @@ import {
LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective,
PreJoinDirective,
LeaveButtonDirective,
SettingsPanelGeneralAdditionalElementsDirective,
ToolbarMoreOptionsAdditionalMenuItemsDirective
LeaveButtonDirective
} 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.
@ -377,38 +374,6 @@ 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
*/
@ -511,14 +476,6 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal
*/
ovLayoutAdditionalElementsTemplate: TemplateRef<any>;
/**
* @internal
*/
ovSettingsPanelGeneralAdditionalElementsTemplate: TemplateRef<any>;
/**
* @internal
*/
ovToolbarMoreOptionsAdditionalMenuItemsTemplate: TemplateRef<any>;
/**
* @internal
@ -755,8 +712,7 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
private actionService: ActionService,
private libService: OpenViduComponentsConfigService,
private templateManagerService: TemplateManagerService,
private themeService: OpenViduThemeService,
private e2eeService: E2eeService
private themeService: OpenViduThemeService
) {
this.log = this.loggerSrv.get('VideoconferenceComponent');
@ -828,9 +784,7 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
layout: this.externalLayout,
stream: this.externalStream,
preJoin: this.externalPreJoin,
layoutAdditionalElements: this.externalLayoutAdditionalElements,
settingsPanelGeneralAdditionalElements: this.externalSettingsPanelGeneralAdditionalElements,
toolbarMoreOptionsAdditionalMenuItems: this.externalToolbarMoreOptionsAdditionalMenuItems
layoutAdditionalElements: this.externalLayoutAdditionalElements
};
const defaultTemplates: DefaultTemplates = {
@ -905,12 +859,6 @@ 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);
}
}
/**
@ -1094,9 +1042,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
}
});
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe(async (name: string) => {
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
if (name) {
this.latestParticipantName = await this.e2eeService.decrypt(name);
this.latestParticipantName = name;
this.storageSrv.setParticipantName(name);
// If we're waiting for a participant name to proceed with joining, do it now

View File

@ -1,9 +1,7 @@
import { OverlayContainer } from '@angular/cdk/overlay';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
@Injectable()
export class CdkOverlayContainer extends OverlayContainer {
private readonly cdkContainerClass: string = '.cdk-overlay-container';
private defaultSelector = 'body';

View File

@ -18,8 +18,7 @@ import {
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective,
ShowThemeSelectorDirective,
E2EEKeyDirective
ShowThemeSelectorDirective
} from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import {
@ -114,8 +113,7 @@ const directives = [
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective,
ShowThemeSelectorDirective,
E2EEKeyDirective
ShowThemeSelectorDirective
];
@NgModule({

View File

@ -58,10 +58,7 @@ 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(
@ -74,7 +71,7 @@ export class LayoutRemoteParticipantsDirective {
}
ngAfterViewInit() {
this.update(this._ovRemoteParticipants);
this.update(this.ovRemoteParticipants);
}
update(value: ParticipantModel[] | undefined) {
@ -573,51 +570,3 @@ 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,10 +535,8 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
update(participantName: string) {
if (participantName) {
this.libService.updateGeneralConfig({ participantName });
}
update(value: string) {
if (value) this.libService.updateGeneralConfig({ participantName: value });
}
}

View File

@ -178,7 +178,7 @@
* ```
* <!--ovPreJoin-end-tutorial-->
*
* @internal
* 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/).
*/
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
@ -308,73 +308,3 @@ 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,9 +19,7 @@ import {
ParticipantPanelAfterLocalParticipantDirective,
ParticipantPanelParticipantBadgeDirective,
PreJoinDirective,
LeaveButtonDirective,
SettingsPanelGeneralAdditionalElementsDirective,
ToolbarMoreOptionsAdditionalMenuItemsDirective
LeaveButtonDirective
} from './internals.directive';
@NgModule({
@ -42,9 +40,7 @@ import {
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective,
SettingsPanelGeneralAdditionalElementsDirective,
ToolbarMoreOptionsAdditionalMenuItemsDirective
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective
],
exports: [
@ -64,9 +60,7 @@ import {
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective,
SettingsPanelGeneralAdditionalElementsDirective,
ToolbarMoreOptionsAdditionalMenuItemsDirective
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective
]
})

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "消息已发送",
"OPEN_CHAT": "打开"
},
"ACTIVITIES": {
"TITLE": "活动"
},
"PARTICIPANTS": {
"TITLE": "参与者",
"CAMERA": "摄像头",
@ -178,8 +175,6 @@
"MEDIA_ERR_GENERIC": "加载视频时发生错误。",
"MEDIA_ERR_NETWORK": "网络错误导致视频下载中途失败。",
"MEDIA_ERR_DECODE": "由于损坏问题或视频使用了您的浏览器不支持的功能,视频播放被中止。",
"MEDIA_ERR_SRC_NOT_SUPPORTED": "流媒体录制错误:无法访问 S3。",
"E2EE_ERROR_TITLE": "房间密码错误",
"E2EE_ERROR_CONTENT": "此参与者使用了不同的安全密钥。无法显示视频。"
"MEDIA_ERR_SRC_NOT_SUPPORTED": "流媒体录制错误:无法访问 S3。"
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "Nachricht gesendet",
"OPEN_CHAT": "ÖFFNEN"
},
"ACTIVITIES": {
"TITLE": "Aktivitäten"
},
"PARTICIPANTS": {
"TITLE": "Teilnehmer",
"CAMERA": "KAMERA",
@ -173,8 +170,6 @@
"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.",
"E2EE_ERROR_TITLE": "Raum-Passwortfehler",
"E2EE_ERROR_CONTENT": "Dieser Teilnehmer verwendet einen anderen Sicherheitsschlüssel. Video kann nicht angezeigt werden."
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Fehler beim Streaming der Aufnahme: S3 konnte nicht erreicht werden."
}
}

View File

@ -97,9 +97,6 @@
"MUTE": "Mute",
"UNMUTE": "Unmute"
},
"ACTIVITIES": {
"TITLE": "Activities"
},
"SETTINGS": {
"TITLE": "Settings",
"GENERAL": "General",
@ -182,8 +179,6 @@
"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.",
"E2EE_ERROR_TITLE": "Room password error",
"E2EE_ERROR_CONTENT": "This participant is using a different encryption key. Video cannot be displayed."
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Error streaming recording: S3 could not be reached."
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "mensaje enviado",
"OPEN_CHAT": "ABRIR"
},
"ACTIVITIES": {
"TITLE": "Actividades"
},
"PARTICIPANTS": {
"TITLE": "Participantes",
"CAMERA": "CÁMARA",
@ -177,8 +174,6 @@
"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.",
"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."
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Error al transmitir la grabación: no se pudo conectar con S3."
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "message envoyé",
"OPEN_CHAT": "OUVRIR"
},
"ACTIVITIES": {
"TITLE": "Activités"
},
"PARTICIPANTS": {
"TITLE": "Participants",
"CAMERA": "CAMÉRA",
@ -178,8 +175,6 @@
"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.",
"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."
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Erreur de diffusion de l'enregistrement : S3 n'a pas pu être atteint."
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया",
"OPEN_CHAT": "खोलें"
},
"ACTIVITIES": {
"TITLE": "गतिविधियाँ"
},
"PARTICIPANTS": {
"TITLE": "सदस्य",
"CAMERA": "कैमरा",
@ -178,8 +175,6 @@
"MEDIA_ERR_GENERIC": "वीडियो लोड करते समय एक त्रुटि हुई।",
"MEDIA_ERR_NETWORK": "नेटवर्क त्रुटि के कारण वीडियो डाउनलोड बीच में ही विफल हो गया।",
"MEDIA_ERR_DECODE": "वीडियो प्लेबैक को एक भ्रष्टाचार समस्या या क्योंकि वीडियो ने आपके ब्राउज़र द्वारा समर्थित नहीं की गई सुविधाओं का उपयोग किया था, के कारण रोक दिया गया था।",
"MEDIA_ERR_SRC_NOT_SUPPORTED": "स्ट्रीमिंग रिकॉर्डिंग में त्रुटि: S3 तक पहुंचा नहीं जा सका।",
"E2EE_ERROR_TITLE": "कक्ष पासवर्ड त्रुटि",
"E2EE_ERROR_CONTENT": "यह प्रतिभागी एक अलग सुरक्षा कुंजी का उपयोग कर रहा है। वीडियो प्रदर्शित नहीं किया जा सकता।"
"MEDIA_ERR_SRC_NOT_SUPPORTED": "स्ट्रीमिंग रिकॉर्डिंग में त्रुटि: S3 तक पहुंचा नहीं जा सका।"
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "messaggio inviato",
"OPEN_CHAT": "APRI"
},
"ACTIVITIES": {
"TITLE": "Attività"
},
"PARTICIPANTS": {
"TITLE": "Partecipanti",
"CAMERA": "CAMERA",
@ -178,8 +175,6 @@
"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.",
"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."
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Errore nello streaming della registrazione: impossibile raggiungere S3."
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "メッセージを送信しました",
"OPEN_CHAT": "開く"
},
"ACTIVITIES": {
"TITLE": "アクティビティ"
},
"PARTICIPANTS": {
"TITLE": "参加者",
"CAMERA": "カメラ",
@ -178,8 +175,6 @@
"MEDIA_ERR_GENERIC": "ビデオの読み込み中にエラーが発生しました。",
"MEDIA_ERR_NETWORK": "ネットワークエラーにより、ビデオのダウンロードが途中で失敗しました。",
"MEDIA_ERR_DECODE": "破損の問題またはビデオがブラウザでサポートされていない機能を使用したために、ビデオの再生が中止されました。",
"MEDIA_ERR_SRC_NOT_SUPPORTED": "録画のストリーミングエラーS3にアクセスできませんでした。",
"E2EE_ERROR_TITLE": "ルームパスワードエラー",
"E2EE_ERROR_CONTENT": "この参加者は異なるセキュリティキーを使用しています。ビデオを表示できません。"
"MEDIA_ERR_SRC_NOT_SUPPORTED": "録画のストリーミングエラーS3にアクセスできませんでした。"
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "bericht verzonden",
"OPEN_CHAT": "OPENEN"
},
"ACTIVITIES": {
"TITLE": "Activiteiten"
},
"PARTICIPANTS": {
"TITLE": "Deelnemers",
"CAMERA": "CAMERA",
@ -178,8 +175,6 @@
"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.",
"E2EE_ERROR_TITLE": "Kamerwachtwoordfout",
"E2EE_ERROR_CONTENT": "Deze deelnemer gebruikt een andere beveiligingssleutel. Video kan niet worden weergegeven."
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Fout bij het streamen van de opname: S3 kon niet worden bereikt."
}
}

View File

@ -87,9 +87,6 @@
"MESSAGE_SENT_NOTIFICATION": "mensagem enviada",
"OPEN_CHAT": "ABRIR"
},
"ACTIVITIES": {
"TITLE": "Atividades"
},
"PARTICIPANTS": {
"TITLE": "Participantes",
"CAMERA": "CÂMERA",
@ -179,8 +176,6 @@
"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.",
"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."
"MEDIA_ERR_SRC_NOT_SUPPORTED": "Erro ao transmitir a gravação: não foi possível acessar o S3."
}
}

View File

@ -128,8 +128,6 @@ 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;
@ -172,16 +170,8 @@ export class ParticipantModel {
* @returns string
*/
get name(): string | undefined {
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;
return this.participant.name;
// return this.identity;
}
/**
@ -560,31 +550,4 @@ 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,6 +30,7 @@ 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';
@ -47,7 +48,6 @@ 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,
VideoPosterComponent,
AvatarProfileComponent,
MediaElementComponent,
VideoDevicesComponent,
AudioDevicesComponent,

View File

@ -23,7 +23,6 @@ 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],
@ -39,7 +38,7 @@ export class OpenViduComponentsModule {
BroadcastingService,
// CaptionService,
CdkOverlayContainer,
{ provide: OverlayContainer, useExisting: CdkOverlayContainer },
{ provide: OverlayContainer, useClass: CdkOverlayContainer },
ChatService,
DeviceService,
DocumentService,
@ -53,7 +52,6 @@ export class OpenViduComponentsModule {
StorageService,
VirtualBackgroundService,
ViewportService,
E2eeService,
provideHttpClient(withInterceptorsFromDi())
];

View File

@ -0,0 +1,18 @@
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,128 +1,81 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { ActionService } from './action.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialogMock } from '../../../test-helpers/action.service.mock';
import { TranslateServiceMock } from '../../../test-helpers/translate.service.mock';
describe('ActionService (characterization)', () => {
import { ActionService } from './action.service';
import { TranslateService } from '../translate/translate.service';
import { TranslateServiceMock } from '../translate/translate.service.mock';
export class MatDialogMock {
open() {
return { close: () => {} } as MatDialogRef<any>;
}
}
describe('ActionService', () => {
let service: ActionService;
let dialog: MatDialogMock;
let dialog: MatDialog;
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) as unknown as MatDialogMock;
dialog = TestBed.inject(MatDialog);
});
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 be created', () => {
expect(service).toBeTruthy();
});
it('does not open a new dialog if one is already open (repeated calls)', () => {
const spy = spyOn(dialog, 'open').and.callThrough();
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();
}));
service.openConnectionDialog('Title', 'Description', false);
// repeated calls simulate concurrent/repeated user attempts
service.openConnectionDialog('Title', 'Description', false);
service.openConnectionDialog('Title', 'Description', false);
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);
// observed behavior: open called only once
expect(spy).toHaveBeenCalledTimes(1);
expect(dialogSpy).not.toHaveBeenCalled();
});
it('closes the opened dialog when requested and allows opening a new one afterwards', fakeAsync(() => {
const openSpy = spyOn(dialog, 'open').and.callThrough();
it('should close connection dialog and reset state', fakeAsync(() => {
service.openConnectionDialog('Test Title', 'Test Description', false);
service.openConnectionDialog('T', 'D', false);
tick(10); // advance microtasks if the service uses timers/async internally
tick(2000);
// Behavior: closing should invoke close() on the MatDialogRef
const ref = dialog.lastRef!;
expect(ref).toBeTruthy();
expect(ref.close).not.toHaveBeenCalled();
expect(service['isConnectionDialogOpen']).toBeTrue();
service.closeConnectionDialog();
expect(ref.close).toHaveBeenCalledTimes(1);
// After closing, opening again should create another instance (another open call)
service.openConnectionDialog('T', 'D', false);
expect(openSpy).toHaveBeenCalledTimes(2);
expect(service['isConnectionDialogOpen']).toBeFalse();
}));
it('handles rapid consecutive calls by creating a single dialog (reentrancy protection)', fakeAsync(() => {
const spy = spyOn(dialog, 'open').and.callThrough();
it('should open connection dialog only once', fakeAsync(() => {
// Spy on the dialog open method
const dialogSpy = spyOn(dialog, 'open').and.callThrough();
// several almost-simultaneous calls
service.openConnectionDialog('T', 'D', false);
service.openConnectionDialog('T', 'D', false);
tick(0);
service.openConnectionDialog('T', 'D', false);
tick(0);
service.openConnectionDialog('Test Title', 'Test Description', false);
// Verify that the dialog has been called only once
expect(dialogSpy).toHaveBeenCalledTimes(1);
expect(service['isConnectionDialogOpen']).toBeTrue();
expect(spy).toHaveBeenCalledTimes(1);
}));
// 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);
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();
// Verify that the dialog has been called only once
expect(dialogSpy).toHaveBeenCalledTimes(1);
}));
});

View File

@ -19,8 +19,9 @@ export class ActionService {
private dialogRef:
| MatDialogRef<DialogTemplateComponent | RecordingDialogComponent | DeleteDialogComponent | ProFeatureDialogTemplateComponent>
| undefined;
private dialogSubscription: Subscription;
private connectionDialogRef: MatDialogRef<DialogTemplateComponent> | undefined;
private isConnectionDialogOpen = false;
private isConnectionDialogOpen: boolean = false;
constructor(
private snackBar: MatSnackBar,
@ -28,7 +29,7 @@ export class ActionService {
private translateService: TranslateService
) {}
launchNotification(options: INotificationOptions, callback?: () => void): void {
launchNotification(options: INotificationOptions, callback): void {
if (!options.config) {
options.config = {
duration: 3000,
@ -40,23 +41,28 @@ export class ActionService {
const notification = this.snackBar.open(options.message, options.buttonActionText, options.config);
if (callback) {
// subscribe and complete immediately after calling callback
const sub = notification.onAction().subscribe(() => {
sub.unsubscribe();
notification.onAction().subscribe(() => {
callback();
});
}
}
openDialog(titleMessage: string, descriptionMessage: string, allowClose = true) {
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));
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();
});
}
}
openConnectionDialog(titleMessage: string, descriptionMessage: string, allowClose = false) {
@ -69,44 +75,47 @@ export class ActionService {
this.connectionDialogRef = this.dialog.open(DialogTemplateComponent, config);
this.isConnectionDialogOpen = true;
this.connectionDialogRef.afterClosed().subscribe(() => {
this.isConnectionDialogOpen = false;
this.connectionDialogRef = undefined;
});
}
openDeleteRecordingDialog(successCallback: () => void) {
this.closeDialog();
this.dialogRef = this.dialog.open(DeleteDialogComponent);
this.dialogRef.afterClosed().subscribe((result) => {
if (result) {
successCallback();
}
this.dialogRef = 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();
}
});
}
}
openRecordingPlayerDialog(src: string, allowClose = true) {
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;
});
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();
});
}
}
closeDialog() {
if (this.dialogRef) {
this.dialogRef.close();
this.dialogRef = undefined;
}
this.dialogRef?.close();
}
closeConnectionDialog() {

View File

@ -1,147 +0,0 @@
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,7 +13,6 @@ 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
@ -22,7 +21,7 @@ import { E2eeService } from '../e2ee/e2ee.service';
providedIn: 'root'
})
export class ChatService {
chatMessages$: Observable<ChatMessage[]>;
messagesObs: Observable<ChatMessage[]>;
private messageSound: HTMLAudioElement;
private _messageList = <BehaviorSubject<ChatMessage[]>>new BehaviorSubject<ChatMessage[]>([]);
private messageList: ChatMessage[] = [];
@ -32,11 +31,10 @@ export class ChatService {
private participantService: ParticipantService,
private panelService: PanelService,
private actionService: ActionService,
private translateService: TranslateService,
private e2eeService: E2eeService
private translateService: TranslateService
) {
this.log = this.loggerSrv.get('ChatService');
this.chatMessages$ = this._messageList.asObservable();
this.messagesObs = 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'
);
@ -62,32 +60,13 @@ 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) {
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;
}
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()!);
}
}

View File

@ -96,7 +96,6 @@ interface GeneralConfig {
showDisconnectionDialog: boolean;
showThemeSelector: boolean;
recordingStreamBaseUrl: string;
e2eeKey?: string;
}
/**
@ -303,8 +302,7 @@ export class OpenViduComponentsConfigService {
prejoinDisplayParticipantName: true,
showDisconnectionDialog: true,
showThemeSelector: false,
recordingStreamBaseUrl: 'call/api/recordings',
e2eeKey: undefined
recordingStreamBaseUrl: 'call/api/recordings'
});
private toolbarConfig = this.createToolbarConfigItem({
@ -415,11 +413,6 @@ 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));
@ -572,10 +565,6 @@ export class OpenViduComponentsConfigService {
return baseUrl;
}
getE2EEKey(): string | undefined {
return this.generalConfig.subject.getValue().e2eeKey;
}
// Stream configuration methods
isVideoEnabled(): boolean {

View File

@ -1,6 +1,5 @@
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable, DOCUMENT } from '@angular/core';
import { ParticipantFactoryFunction, OpenViduComponentsConfig } from '../../config/openvidu-components-angular.config';
/**
@ -17,6 +16,7 @@ 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,15 +179,23 @@ export class DeviceService {
* @returns A promise that resolves to an array of `MediaDeviceInfo` objects representing the available local devices.
*/
private async getLocalDevices(): Promise<MediaDeviceInfo[]> {
const strategies = this.getPermissionStrategies();
// Forcing media permissions request.
const strategies = [
{ audio: true, video: true },
{ audio: true, video: false },
{ audio: false, video: true }
];
for (const strategy of strategies) {
try {
this.log.d(`Trying strategy: audio=${strategy.audio}, video=${strategy.video}`);
const devices = await this.tryPermissionStrategy(strategy);
if (devices) {
return this.filterValidDevices(devices);
}
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');
} catch (error: any) {
this.log.w(`Strategy failed: audio=${strategy.audio}, video=${strategy.video}`, error);
@ -201,38 +209,6 @@ 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 });
@ -243,28 +219,20 @@ export class DeviceService {
this.log.w('All permission strategies failed, trying device enumeration without permissions');
try {
return await this.handleFallbackByErrorType(error);
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 [];
} 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

@ -1,295 +0,0 @@
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,83 +11,37 @@ export class DocumentService {
constructor() {}
toggleFullscreen(elementId: string) {
const document: any = this.getDocument();
const fs = this.getElementById(elementId);
if (this.isInFullscreen()) {
this.exitFullscreen(document);
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();
}
} else {
this.requestFullscreen(fs);
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
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

@ -1,211 +0,0 @@
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

@ -1,337 +0,0 @@
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 { BackgroundProcessor, /*BackgroundProcessorWrapper,*/ SwitchBackgroundProcessorOptions } from '@livekit/track-processors';
import { ILogger } from '../../models/logger.model';
import { DeviceService } from '../device/device.service';
import { LoggerService } from '../logger/logger.service';
import {
AudioCaptureOptions,
ConnectionState,
CreateLocalTracksOptions,
E2EEOptions,
ExternalE2EEKeyProvider,
LocalAudioTrack,
LocalTrack,
LocalVideoTrack,
@ -16,14 +16,8 @@ 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'
})
@ -37,7 +31,6 @@ export class OpenViduService {
// private _isSttReady: BehaviorSubject<boolean> = new BehaviorSubject(true);
private room: Room;
private keyProvider: ExternalE2EEKeyProvider | undefined;
/**
* @internal
@ -54,24 +47,16 @@ 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 configService: OpenViduComponentsConfigService
private storageService: StorageService
) {
this.log = this.loggerSrv.get('OpenViduService');
// this.isSttReadyObs = this._isSttReady.asObservable();
this.backgroundProcessor = BackgroundProcessor({ mode: 'disabled' });
}
/**
@ -79,25 +64,14 @@ export class OpenViduService {
* @internal
*/
initRoom(): void {
// 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) {
// If room already exists, don't recreate it
if (this.room) {
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,
@ -119,47 +93,17 @@ 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);
@ -275,18 +219,6 @@ 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
**/
@ -359,14 +291,6 @@ 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();
@ -510,10 +434,11 @@ export class OpenViduService {
}
} catch (error) {
this.log.e('Failed to create new video track:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to switch camera: ${message}`);
throw new Error(`Failed to switch camera: ${error.message}`);
}
} /**
}
/**
* Switches the microphone device when the room is not connected (prejoin page)
* @param deviceId new audio device to use
* @internal
@ -560,8 +485,7 @@ export class OpenViduService {
}
} catch (error) {
this.log.e('Failed to create new audio track:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to switch microphone: ${message}`);
throw new Error(`Failed to switch microphone: ${error.message}`);
}
}

View File

@ -1,124 +0,0 @@
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,7 +20,6 @@ import {
VideoPresets
} from 'livekit-client';
import { StorageService } from '../storage/storage.service';
import { E2eeService } from '../e2ee/e2ee.service';
@Injectable({
providedIn: 'root'
@ -51,8 +50,7 @@ export class ParticipantService {
private directiveService: OpenViduComponentsConfigService,
private openviduService: OpenViduService,
private storageSrv: StorageService,
private loggerSrv: LoggerService,
private e2eeService: E2eeService
private loggerSrv: LoggerService
) {
this.log = this.loggerSrv.get('ParticipantService');
this.localParticipant$ = this.localParticipantBS.asObservable();
@ -286,26 +284,6 @@ 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.
*/
@ -555,45 +533,11 @@ export class ParticipantService {
}
}
private newParticipant(props: ParticipantProperties): ParticipantModel {
let participant: ParticipantModel;
private newParticipant(props: ParticipantProperties) {
if (this.globalService.hasParticipantFactory()) {
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 this.globalService.getParticipantFactory().apply(this, [props]);
}
return new ParticipantModel(props);
}
private getScreenCaptureOptions(): ScreenShareCaptureOptions {

View File

@ -19,9 +19,7 @@ import {
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
LeaveButtonDirective,
SettingsPanelGeneralAdditionalElementsDirective,
ToolbarMoreOptionsAdditionalMenuItemsDirective
LeaveButtonDirective
} from '../../directives/template/internals.directive';
/**
@ -51,12 +49,6 @@ 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>;
}
@ -80,7 +72,6 @@ export interface ToolbarTemplateConfiguration {
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
toolbarLeaveButtonTemplate?: TemplateRef<any>;
toolbarMoreOptionsAdditionalMenuItemsTemplate?: TemplateRef<any>;
}
/**
@ -135,8 +126,6 @@ export interface ExternalDirectives {
stream?: StreamDirective;
preJoin?: PreJoinDirective;
layoutAdditionalElements?: LayoutAdditionalElementsDirective;
settingsPanelGeneralAdditionalElements?: SettingsPanelGeneralAdditionalElementsDirective;
toolbarMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective;
}
/**
@ -219,16 +208,6 @@ 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;
}
@ -389,16 +368,14 @@ export class TemplateManagerService {
setupToolbarTemplates(
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective,
externalLeaveButton?: LeaveButtonDirective,
externalMoreOptionsAdditionalMenuItems?: ToolbarMoreOptionsAdditionalMenuItemsDirective
externalLeaveButton?: LeaveButtonDirective
): ToolbarTemplateConfiguration {
this.log.v('Setting up toolbar templates...');
return {
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template,
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template,
toolbarLeaveButtonTemplate: externalLeaveButton?.template,
toolbarMoreOptionsAdditionalMenuItemsTemplate: externalMoreOptionsAdditionalMenuItems?.template
toolbarLeaveButtonTemplate: externalLeaveButton?.template
};
}

View File

@ -1,11 +1,13 @@
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 { ILogger } from '../../models/logger.model';
import { LoggerService } from '../logger/logger.service';
import { ParticipantService } from '../participant/participant.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
@ -45,8 +47,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
@ -69,59 +71,162 @@ export class VirtualBackgroundService {
if (!!bgId) {
const background = this.backgrounds.find((bg) => bg.id === bgId);
if (background) {
await this.applyBackground(background);
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 {
const options = this.getBackgroundOptions(bg);
await this.openviduService.switchBackgroundMode(options);
// 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);
}
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');
try {
await this.openviduService.switchBackgroundMode({ mode: 'disabled' });
} catch (e) {
this.log.w('Error disabling processor:', e);
const cameraTrack = this.getCameraTrack();
if (cameraTrack) {
try {
await cameraTrack.stopProcessor();
} catch (e) {
this.log.w('Error stopping processor:', e);
}
}
this.storageService.removeBackground();
}
}
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
};
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;
}
return { mode: 'disabled' };
return undefined;
}
/**
* Gets the camera track from either the published tracks (if in room) or local tracks (if in prejoin)
* @returns The camera LocalTrack or undefined if not found
* @private
*/
private getCameraTrack(): LocalVideoTrack | undefined {
// First, try to get from published tracks (when in room)
if (this.openviduService.isRoomConnected()) {
const localParticipant = this.participantService.getLocalParticipant();
const cameraTrackPublication = localParticipant?.cameraTracks?.[0];
if (cameraTrackPublication?.track) {
return cameraTrackPublication.track as LocalVideoTrack;
}
}
// Fallback to local tracks (when in prejoin or tracks not yet published)
const localTracks = this.openviduService.getLocalTracks();
const cameraTrack = localTracks.find((track) => track.kind === Track.Kind.Video);
return cameraTrack as LocalVideoTrack | undefined;
}
/**
* Applies a background processor to the camera track
* @param cameraTrack The camera track to apply the processor to
* @param processor The background processor to apply
* @private
*/
private async applyProcessorToCameraTrack(cameraTrack: LocalVideoTrack, processor: ProcessorWrapper<BackgroundOptions>): Promise<void> {
await cameraTrack.setProcessor(processor);
}
private backgroundIsAlreadyApplied(backgroundId: string): boolean {
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

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

View File

@ -66,10 +66,8 @@ 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

@ -1,51 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,10 +0,0 @@
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 // TODO Livekit track processors fails with typescript types checking
// "skipLibCheck": true // Livekit track processors fails with typescript types checking
},
"angularCompilerOptions": {
"compilationMode": "partial"

View File

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

View File

@ -1,7 +1,8 @@
services:
caddy-proxy:
image: docker.io/openvidu/openvidu-caddy-local:main
image: docker.io/openvidu/openvidu-caddy-local:3.4.0
platform: linux/amd64
restart: unless-stopped
extra_hosts:
- host.docker.internal:host-gateway
@ -18,7 +19,6 @@ 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:8.2.2-alpine
image: docker.io/redis:7.4.4-alpine
platform: linux/amd64
restart: unless-stopped
ports:
- 6379:6379
volumes:
- redis:/data
- /etc/localtime:/etc/localtime:ro
command: >
redis-server
--bind 0.0.0.0
@ -47,7 +47,8 @@ services:
condition: service_completed_successfully
minio:
image: docker.io/openvidu/minio:2025.9.7-debian-12-r3
image: docker.io/openvidu/minio:2025.5.24-debian-12-r1
platform: linux/amd64
restart: unless-stopped
ports:
- 9000:9000
@ -57,23 +58,21 @@ 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.15-r0
image: docker.io/openvidu/mongodb:8.0.9
platform: linux/amd64
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}
@ -81,26 +80,27 @@ 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:main
image: docker.io/openvidu/openvidu-dashboard:3.4.0
platform: linux/amd64
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:main
image: docker.io/openvidu/openvidu-server:3.4.0
platform: linux/amd64
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:main
image: docker.io/openvidu/ingress:3.4.0
platform: linux/amd64
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:main
image: docker.io/openvidu/egress:3.4.0
platform: linux/amd64
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:main
image: docker.io/openvidu/openvidu-operator:3.4.0
platform: linux/amd64
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,7 +177,8 @@ services:
condition: service_completed_successfully
openvidu-meet:
image: docker.io/openvidu/openvidu-meet:main
image: docker.io/openvidu/openvidu-meet:3.4.0
platform: linux/amd64
restart: on-failure
ports:
- 9080:6080
@ -208,17 +209,16 @@ 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:main
image: docker.io/openvidu/openvidu-operator:3.4.0
platform: linux/amd64
restart: on-failure
environment:
- MODE=local-ready-check
@ -235,7 +235,6 @@ 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,27 +81,42 @@ Parameters:
Type: String
Default: c6a.xlarge
AllowedValues:
- t3.nano
- t3.micro
- t3.small
- t2.large
- t2.xlarge
- t2.2xlarge
- t3.medium
- t3.large
- t3.xlarge
- t3.2xlarge
- 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
- 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
- c5.large
- c5.xlarge
- c5.2xlarge
@ -110,39 +125,6 @@ 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
@ -154,32 +136,6 @@ 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
@ -190,26 +146,6 @@ 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
@ -222,40 +158,6 @@ 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
@ -267,77 +169,20 @@ Parameters:
- c7i.48xlarge
- c7i.metal-24xl
- c7i.metal-48xl
- 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
- 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
- m6in.large
- m6in.xlarge
- m6in.2xlarge
@ -347,67 +192,14 @@ Parameters:
- m6in.16xlarge
- m6in.24xlarge
- m6in.32xlarge
- 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
- r5n.large
- r5n.xlarge
- r5n.2xlarge
- r5n.4xlarge
- r5n.8xlarge
- r5n.12xlarge
- r5n.16xlarge
- r5n.24xlarge
ConstraintDescription: "Must be a valid EC2 instance type"
KeyName:
@ -416,11 +208,10 @@ Parameters:
AllowedPattern: ^.+$
ConstraintDescription: must be the name of an existing EC2 KeyPair.
OperatingSystem:
Description: VSCode Server EC2 operating system
Type: String
Default: "Ubuntu-24"
AllowedValues: ['Ubuntu-22', 'Ubuntu-24']
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
S3AppDataBucketName:
Type: String
@ -447,7 +238,7 @@ Metadata:
Parameters:
- InstanceType
- KeyName
- OperatingSystem
- AmiId
- Label:
default: S3 bucket for application data and recordings
Parameters:
@ -467,31 +258,6 @@ 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:
@ -618,7 +384,7 @@ Resources:
'/usr/local/bin/install.sh':
content: !Sub |
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.4.1
DOMAIN=
YQ_VERSION=v4.44.5
@ -1061,10 +827,7 @@ Resources:
owner: "root"
group: "root"
Properties:
ImageId: !If
- IsGraviton
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
ImageId: !Ref AmiId
LaunchTemplate:
# Enable IMDSv2 by default
LaunchTemplateId: !Ref IMDSv2LaunchTemplate
@ -1082,8 +845,6 @@ Resources:
#!/bin/bash -x
set -eu -o pipefail
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
python3-pip \
ec2-instance-connect

View File

@ -45,8 +45,117 @@ param initialMeetApiKey string = ''
// Azure instance config
@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('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('Username for the Virtual Machine.')
param adminUsername string
@ -65,15 +174,6 @@ 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'
@ -89,8 +189,8 @@ var openviduVMSettings = {
osDiskType: 'StandardSSD_LRS'
ubuntuOSVersion: {
publisher: 'Canonical'
offer: 'ubuntu-24_04-lts'
sku: ubuntuSku
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
linuxConfiguration: {
@ -175,11 +275,9 @@ var stringInterpolationParams = {
var installScriptTemplate = '''
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.4.1
DOMAIN=
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
curl \
unzip \
@ -752,8 +850,6 @@ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
az login --identity --allow-no-subscriptions
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y
export HOME="/root"

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_B4s",
"Standard_B4ms",
"Standard_D4ps_v5",
"Standard_D4pls_v5"
"Standard_B2s",
"Standard_B4ms"
],
"constraints": {
"allowedSizes": [],
"excludedSizes": []
"excludedSizes": [],
"numAvailabilityZonesRequired": 3,
"zone": "3"
},
"options": {
"hideDiskTypeFilter": false

View File

@ -1,5 +1,13 @@
# ------------------------- outputs.tf -------------------------
output "secrets_manager" {
value = "https://console.cloud.google.com/security/secret-manager?project=${var.projectId}"
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
}

View File

@ -8,28 +8,10 @@ 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 = local.isEmpty ? 1 : 0
name = "${var.projectId}-${var.stackName}-${random_id.bucket_suffix.hex}"
count = 1
name = local.isEmpty ? "${var.projectId}-${random_id.bucket_suffix.hex}" : var.bucketName
location = var.region
force_destroy = true
uniform_bucket_level_access = true
@ -84,14 +66,6 @@ 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")
@ -102,7 +76,7 @@ resource "google_compute_instance" "openvidu_server" {
boot_disk {
initialize_params {
image = local.ubuntu_image
image = "projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts"
size = 100
type = "pd-standard"
}
@ -117,7 +91,7 @@ resource "google_compute_instance" "openvidu_server" {
metadata = {
# metadata values are accessible from the instance
publicIpAddress = coalesce(var.publicIpAddress, google_compute_address.public_ip_address[0].address)
publicIpAddress = google_compute_address.public_ip_address[0].address
region = var.region
stackName = var.stackName
certificateType = var.certificateType
@ -151,11 +125,9 @@ locals {
#!/bin/bash -x
set -e
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.4.1
DOMAIN=
YQ_VERSION=v4.44.5
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
curl \
unzip \
@ -166,8 +138,8 @@ apt-get update && apt-get install -y \
lsb-release \
openssl
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
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
# Configure gcloud with instance service account
gcloud auth activate-service-account --key-file=/dev/null 2>/dev/null || true
@ -177,6 +149,31 @@ 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
@ -342,7 +339,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=$(get_meta "instance/attributes/bucketName")
EXTERNAL_S3_BUCKET_APP_DATA=${google_storage_bucket.bucket[0].name}
# Update egress.yaml to use hardcoded credentials instead of env variable
if [ -f "$${CONFIG_DIR}/egress.yaml" ]; then
@ -678,7 +675,7 @@ ${local.config_s3_script}
CONFIG_S3_EOF
chmod +x /usr/local/bin/config_s3.sh
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y
# Install google cli

View File

@ -9,13 +9,13 @@ variable "projectId" {
variable "region" {
description = "GCP region where resources will be created."
type = string
default = "europe-west2"
default = "europe-west1"
}
variable "zone" {
description = "GCP zone that some resources will use."
type = string
default = "europe-west2-b"
default = "europe-west1-b"
}
variable "stackName" {
@ -88,7 +88,11 @@ variable "initialMeetApiKey" {
variable "instanceType" {
description = "Specifies the GCE machine type for your OpenVidu instance"
type = string
default = "e2-standard-2"
default = "e2-standard-8"
validation {
condition = can(regex("^(e2-(micro|small|medium|standard-[2-9]|standard-1[0-6]|highmem-[2-9]|highmem-1[0-6]|highcpu-[2-9]|highcpu-1[0-6])|n1-(standard-[1-9]|standard-[1-9][0-9]|highmem-[2-9]|highmem-[1-9][0-9]|highcpu-[1-9]|highcpu-[1-9][0-9])|n2-(standard-[2-9]|standard-[1-9][0-9]|standard-1[0-2][0-8]|highmem-[2-9]|highmem-[1-9][0-9]|highmem-1[0-2][0-8]|highcpu-[1-9][0-9]|highcpu-1[0-2][0-8])|n2d-(standard-[2-9]|standard-[1-9][0-9]|standard-2[0-2][0-4]|highmem-[2-9]|highmem-[1-9][0-9]|highmem-9[0-6]|highcpu-[1-9][0-9]|highcpu-2[0-2][0-4])|c2-(standard-[4-9]|standard-[1-5][0-9]|standard-60)|c2d-(standard-[2-9]|standard-[1-9][0-9]|standard-1[0-1][0-2]|highmem-[2-9]|highmem-[1-9][0-9]|highmem-1[0-1][0-2]|highcpu-[1-9][0-9]|highcpu-1[0-1][0-2])|m1-(ultramem-[4-9][0-9]|ultramem-160)|m2-(ultramem-208|ultramem-416|megamem-416)|m3-(ultramem-32|ultramem-64|ultramem-128|megamem-64|megamem-128)|a2-(standard-[1-9]|standard-[1-9][0-9]|standard-96|highmem-1g|ultramem-1g|megamem-1g)|a3-(standard-[1-9]|standard-[1-9][0-9]|standard-80|highmem-1g|megamem-1g)|g2-(standard-[4-9]|standard-[1-9][0-9]|standard-96)|t2d-(standard-[1-9]|standard-[1-9][0-9]|standard-60)|t2a-(standard-[1-9]|standard-[1-9][0-9]|standard-48)|h3-(standard-88)|f1-(micro)|t4g-(micro|small|medium|standard-[1-9]|standard-[1-9][0-9]))$", var.instanceType))
error_message = "The instance type is not valid"
}
}
variable "bucketName" {
@ -123,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:-29.0.2}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
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 INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
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 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 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.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}"
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}"
# Function to compare two version strings
compare_versions() {
@ -158,13 +158,11 @@ if [ "$DOCKER_COMPOSE_NEEDED" = true ]; then
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
fi
# 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
# Restart Docker and wait for it to start
systemctl enable docker
systemctl stop docker
systemctl start docker
wait_for_docker
# 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:-29.0.2}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
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 INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
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 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 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.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}"
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}"
# Function to compare two version strings
compare_versions() {
@ -158,13 +158,11 @@ if [ "$DOCKER_COMPOSE_NEEDED" = true ]; then
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
fi
# 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
# Restart Docker and wait for it to start
systemctl enable docker
systemctl stop docker
systemctl start docker
wait_for_docker
# Create random temp directory
TMP_DIR=$(mktemp -d)

View File

@ -95,27 +95,42 @@ Parameters:
Type: String
Default: c6a.xlarge
AllowedValues:
- t3.nano
- t3.micro
- t3.small
- t2.large
- t2.xlarge
- t2.2xlarge
- t3.medium
- t3.large
- t3.xlarge
- t3.2xlarge
- 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
- 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
- c5.large
- c5.xlarge
- c5.2xlarge
@ -124,39 +139,6 @@ 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
@ -168,32 +150,6 @@ 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
@ -204,26 +160,6 @@ 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
@ -236,40 +172,6 @@ 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
@ -281,77 +183,20 @@ Parameters:
- c7i.48xlarge
- c7i.metal-24xl
- c7i.metal-48xl
- 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
- 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
- m6in.large
- m6in.xlarge
- m6in.2xlarge
@ -361,67 +206,14 @@ Parameters:
- m6in.16xlarge
- m6in.24xlarge
- m6in.32xlarge
- 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
- r5n.large
- r5n.xlarge
- r5n.2xlarge
- r5n.4xlarge
- r5n.8xlarge
- r5n.12xlarge
- r5n.16xlarge
- r5n.24xlarge
ConstraintDescription: "Must be a valid EC2 instance type"
MediaNodeInstanceType:
@ -429,27 +221,42 @@ Parameters:
Type: String
Default: c6a.xlarge
AllowedValues:
- t3.nano
- t3.micro
- t3.small
- t2.large
- t2.xlarge
- t2.2xlarge
- t3.medium
- t3.large
- t3.xlarge
- t3.2xlarge
- 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
- 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
- c5.large
- c5.xlarge
- c5.2xlarge
@ -458,39 +265,6 @@ 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
@ -502,32 +276,6 @@ 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
@ -538,26 +286,6 @@ 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
@ -570,40 +298,6 @@ 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
@ -615,77 +309,20 @@ Parameters:
- c7i.48xlarge
- c7i.metal-24xl
- c7i.metal-48xl
- 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
- 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
- m6in.large
- m6in.xlarge
- m6in.2xlarge
@ -695,67 +332,14 @@ Parameters:
- m6in.16xlarge
- m6in.24xlarge
- m6in.32xlarge
- 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
- r5n.large
- r5n.xlarge
- r5n.2xlarge
- r5n.4xlarge
- r5n.8xlarge
- r5n.12xlarge
- r5n.16xlarge
- r5n.24xlarge
ConstraintDescription: "Must be a valid EC2 instance type"
KeyName:
@ -764,11 +348,10 @@ Parameters:
AllowedPattern: ^.+$
ConstraintDescription: must be the name of an existing EC2 KeyPair.
OperatingSystem:
Description: OpenVidu EC2 operating system
Type: String
Default: "Ubuntu-24"
AllowedValues: ['Ubuntu-22', 'Ubuntu-24']
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
InitialNumberOfMediaNodes:
Type: Number
@ -839,7 +422,7 @@ Metadata:
- MasterNodeInstanceType
- MediaNodeInstanceType
- KeyName
- OperatingSystem
- AmiId
- Label:
default: Media Nodes Autoscaling Group configuration
Parameters:
@ -872,41 +455,6 @@ 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:
@ -1136,7 +684,7 @@ Resources:
content: !Sub |
#!/bin/bash
set -e
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.4.1
DOMAIN=
YQ_VERSION=v4.44.5
@ -1624,10 +1172,7 @@ Resources:
owner: "root"
group: "root"
Properties:
ImageId: !If
- IsMasterGraviton
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
ImageId: !Ref AmiId
LaunchTemplate:
# Enable IMDSv2 by default
LaunchTemplateId: !Ref IMDSv2LaunchTemplateMasterNode
@ -1646,8 +1191,6 @@ Resources:
#!/bin/bash
set -eu -o pipefail
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
python3-pip \
ec2-instance-connect
@ -1877,10 +1420,7 @@ Resources:
Arn: !GetAtt OpenViduMediaNodeInstanceProfile.Arn
SecurityGroupIds:
- !GetAtt OpenViduMediaNodeSG.GroupId
ImageId: !If
- IsMediaGraviton
- !FindInMap [ArmImage, !Ref OperatingSystem, ImageId]
- !FindInMap [AmdImage, !Ref OperatingSystem, ImageId]
ImageId: !Ref AmiId
KeyName: !Ref KeyName
InstanceType: !Ref MediaNodeInstanceType
UserData:
@ -1888,8 +1428,6 @@ Resources:
#!/bin/bash
set -eu -o pipefail
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
python3-pip \
ec2-instance-connect

View File

@ -53,11 +53,229 @@ param initialMeetAdminPassword string = ''
@secure()
param initialMeetApiKey string = ''
@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 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 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('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('Username for the Virtual Machine.')
param adminUsername string
@ -86,25 +304,13 @@ 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: 'ubuntu-24_04-lts'
sku: masterUbuntuSku
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
linuxConfiguration: {
@ -125,8 +331,8 @@ var mediaNodeVMSettings = {
osDiskType: 'StandardSSD_LRS'
ubuntuOSVersion: {
publisher: 'Canonical'
offer: 'ubuntu-24_04-lts'
sku: mediaUbuntuSku
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts-gen2'
version: 'latest'
}
linuxConfiguration: {
@ -223,13 +429,11 @@ var stringInterpolationParamsMaster = {
var installScriptTemplateMaster = '''
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.4.1
DOMAIN=
# Assume azure cli is installed
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
curl \
unzip \
@ -741,7 +945,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
@ -850,8 +1054,6 @@ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
az login --identity --allow-no-subscriptions
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y
export HOME="/root"
@ -935,8 +1137,6 @@ set -e
DOMAIN=
# Install dependencies
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y \
curl \
unzip \
@ -1084,8 +1284,6 @@ chmod +x /usr/local/bin/stop_media_node.sh
echo ${base64delete} | base64 -d > /usr/local/bin/delete_media_node.sh
chmod +x /usr/local/bin/delete_media_node.sh
echo "DPkg::Lock::Timeout \"-1\";" > /etc/apt/apt.conf.d/99timeout
apt-get update && apt-get install -y
apt-get install -y jq
@ -1332,7 +1530,7 @@ module webhookModule '../../shared/webhookdeployment.json' = {
}
resource actionGroupScaleIn 'Microsoft.Insights/actionGroups@2023-01-01' = {
name: 'actiongroupScaleIn'
name: 'actiongrouptest'
location: 'global'
properties: {
groupShortName: 'scaleinag'
@ -1477,11 +1675,9 @@ 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": "ov-publicIpAddress"
"publicIpAddressName": "defaultName"
},
"options": {
"hideNone": true,
@ -260,14 +260,13 @@
"label": "Master Node Instance Type",
"toolTip": "Specifies the Azure instance type for your OpenVidu Master Node",
"recommendedSizes": [
"Standard_B4s",
"Standard_B4ms",
"Standard_D4ps_v5",
"Standard_D4pls_v5"
"Standard_B2s"
],
"constraints": {
"allowedSizes": [],
"excludedSizes": []
"excludedSizes": [],
"numAvailabilityZonesRequired": 3,
"zone": "3"
},
"options": {
"hideDiskTypeFilter": false
@ -281,14 +280,14 @@
"label": "Media Node Instance Type",
"toolTip": "Specifies the Azure instance type for your OpenVidu Media Nodes",
"recommendedSizes": [
"Standard_B4s",
"Standard_B4ms",
"Standard_D4ps_v5",
"Standard_D4pls_v5"
"Standard_B2s",
"Standard_B4ms"
],
"constraints": {
"allowedSizes": [],
"excludedSizes": []
"excludedSizes": [],
"numAvailabilityZonesRequired": 3,
"zone": "3"
},
"options": {
"hideDiskTypeFilter": false

View File

@ -1,6 +0,0 @@
# ------------------------- 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

@ -1,172 +0,0 @@
# ------------------------- variables -------------------------
# Variables used by the configuration
variable "projectId" {
description = "GCP project id where the resourw es will be created."
type = string
}
variable "region" {
description = "GCP region where resources will be created."
type = string
default = "europe-west2"
}
variable "zone" {
description = "GCP zone that some resources will use."
type = string
default = "europe-west2-b"
}
variable "stackName" {
description = "Stack name for OpenVidu deployment."
type = string
}
variable "certificateType" {
description = "[selfsigned] Not recommended for production use. Just for testing purposes or development environments. You don't need a FQDN to use this option. [owncert] Valid for production environments. Use your own certificate. You need a FQDN to use this option. [letsencrypt] Valid for production environments. Can be used with or without a FQDN (if no FQDN is provided, a random sslip.io domain will be used)."
type = string
default = "letsencrypt"
validation {
condition = contains(["selfsigned", "owncert", "letsencrypt"], var.certificateType)
error_message = "certificateType must be one of: selfsigned, owncert, letsencrypt"
}
}
variable "publicIpAddress" {
description = "Previously created Public IP address for the OpenVidu Deployment. Blank will generate a public IP."
type = string
default = ""
validation {
condition = can(regex("^$|^([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.([01]?\\d{1,2}|2[0-4]\\d|25[0-5])\\.([01]?\\d{1,2}|2[0-4]\\d|25[0-5])$", var.publicIpAddress))
error_message = "The Public Elastic IP does not have a valid IPv4 format"
}
}
variable "domainName" {
description = "Domain name for the OpenVidu Deployment."
type = string
default = ""
validation {
condition = can(regex("^$|^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$", var.domainName))
error_message = "The domain name does not have a valid domain name format"
}
}
variable "ownPublicCertificate" {
description = "If certificate type is 'owncert', this parameter will be used to specify the public certificate"
type = string
default = ""
}
variable "ownPrivateCertificate" {
description = "If certificate type is 'owncert', this parameter will be used to specify the private certificate"
type = string
default = ""
}
variable "initialMeetAdminPassword" {
description = "Initial password for the 'admin' user in OpenVidu Meet. If not provided, a random password will be generated."
type = string
default = ""
validation {
condition = can(regex("^[A-Za-z0-9_-]*$", var.initialMeetAdminPassword))
error_message = "Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to generate a random password."
}
}
variable "initialMeetApiKey" {
description = "Initial API key for OpenVidu Meet. If not provided, no API key will be set and the user can set it later from Meet Console."
type = string
default = ""
validation {
condition = can(regex("^[A-Za-z0-9_-]*$", var.initialMeetApiKey))
error_message = "Must contain only alphanumeric characters (A-Z, a-z, 0-9). Leave empty to not set an initial API key."
}
}
variable "masterNodeInstanceType" {
description = "Specifies the GCE machine type for your OpenVidu Master Node"
type = string
default = "e2-standard-2"
}
variable "mediaNodeInstanceType" {
description = "Specifies the GCE machine type for your OpenVidu Media Nodes"
type = string
default = "e2-standard-2"
}
variable "initialNumberOfMediaNodes" {
description = "Number of initial media nodes to deploy"
type = number
default = 1
}
variable "minNumberOfMediaNodes" {
description = "Minimum number of media nodes to deploy"
type = number
default = 1
}
variable "maxNumberOfMediaNodes" {
description = "Maximum number of media nodes to deploy"
type = number
default = 2
}
variable "scaleTargetCPU" {
description = "Target CPU percentage to scale up or down"
type = number
default = 50
}
variable "bucketName" {
description = "Name of the GCS bucket to store data and recordings. If empty, a bucket will be created"
type = string
default = ""
}
variable "openviduLicense" {
description = "Visit https://openvidu.io/account"
type = string
sensitive = true
}
variable "rtcEngine" {
description = "RTCEngine media engine to use"
type = string
default = "pion"
validation {
condition = contains(["pion", "mediasoup"], var.rtcEngine)
error_message = "rtcEngine must be one of: pion, mediasoup"
}
}
variable "additionalInstallFlags" {
description = "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g.,'--flag1=value, --flag2')."
type = string
default = ""
validation {
condition = can(regex("^[A-Za-z0-9, =_.\\-]*$", var.additionalInstallFlags))
error_message = "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)."
}
}
variable "turnDomainName" {
description = "(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls"
type = string
default = ""
}
variable "turnOwnPublicCertificate" {
description = "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified."
type = string
default = ""
}
variable "turnOwnPrivateCertificate" {
description = "(Optional) This setting is applicable if the certificate type is set to 'owncert' and the TurnDomainName is specified."
type = string
default = ""
}

View File

@ -1,20 +0,0 @@
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:-29.0.2}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.40.3}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
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 INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
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 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 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.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}"
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}"
# Function to compare two version strings
compare_versions() {
@ -158,13 +158,11 @@ if [ "$DOCKER_COMPOSE_NEEDED" = true ]; then
ln -sf /usr/local/bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose
fi
# 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
# Restart Docker and wait for it to start
systemctl enable docker
systemctl stop docker
systemctl start docker
wait_for_docker
# Create random temp directory
TMP_DIR=$(mktemp -d)

Some files were not shown because too many files have changed in this diff Show More