Compare commits

..

101 Commits

Author SHA1 Message Date
Carlos Santos efada4c166 ov-components: (test) Add check for speaker element when local participant is muted 2025-08-14 12:54:30 +02:00
Carlos Santos 3fc0193260 Revert "ov-components: (test) Enhance element counting method to handle timeouts gracefully"
This reverts commit 61a3589dd7.
2025-08-14 12:53:31 +02:00
Carlos Santos 61a3589dd7 ov-components: (test) Enhance element counting method to handle timeouts gracefully 2025-08-14 12:43:10 +02:00
Carlos Santos 8d1c2468f5 ov-components: Refactor VideoconferenceComponent to use private properties for external structural directives, enhancing encapsulation and template setup 2025-08-14 12:16:54 +02:00
Carlos Santos 020413257f ov-components: Implement toolbar room name directive and update toolbar to display room name dynamically 2025-08-13 12:49:03 +02:00
Carlos Santos 6c78abdcc0 ov-components: Remove unnecessary mic button interaction in stream UI tests 2025-08-11 14:17:36 +02:00
Carlos Santos e31a78d153 ov-components: refactor(storage): Enhance tab management and cleanup mechanisms in StorageService 2025-08-11 13:57:23 +02:00
Carlos Santos b1d0269211 ov-components: Add participant badges directive for enhanced participant panel functionality 2025-08-05 17:39:29 +02:00
Carlos Santos 00fcb0b115 ov-components: Revamp participant panel item for improved UI/UX and accessibility; add mute/unmute functionality and translations 2025-08-05 16:35:20 +02:00
Carlos Santos 4bf351b2df ov-components: Add layout additional elements directive for customizable UI extensions 2025-07-31 13:50:47 +02:00
Carlos Santos e9ecceeb77 ov-components: Add participant panel directive for enhanced user experience 2025-07-30 19:37:40 +02:00
Carlos Santos 413dec3e0f ov-components: Close connection dialog on room disconnection event 2025-07-30 12:38:09 +02:00
Carlos Santos 414c26c31b ov-components: Enhance recording status messages with improved styling and structure for starting and stopping states 2025-07-30 12:26:19 +02:00
Carlos Santos fce026766b ov-components: Enhance recording activity component: update view recordings button visibility based on recording status and improve toolbar button text for active recording state 2025-07-29 19:10:35 +02:00
Carlos Santos fe3f90d266 ov-components: Implement error handling for recording start failures and enhance UI interactions 2025-07-29 18:19:16 +02:00
Carlos Santos 5f6b404576 ov-components: Enhance error handling UI in recording activity component with modern styling 2025-07-29 17:50:15 +02:00
Carlos Santos 9b8348bc04 ov-components: Enhance recording activity component with improved status handling and styling 2025-07-29 17:35:28 +02:00
Carlos Santos 1403d062e9 ov-components: Fixed wrong initialize value in show recordings directive 2025-07-29 15:50:31 +02:00
Carlos Santos 7bf0e0036c ov-components: optimize participant name subscription with filter and tap operators 2025-07-29 15:46:05 +02:00
Carlos Santos 76c957903f ov-components: Refactors config service to use RxJS Subjects
Updates the configuration service to use RxJS BehaviorSubjects and Observables for managing configuration values.

This change improves the reactivity and maintainability of the configuration system by providing a consistent and type-safe way to manage application settings.

Specifically, it introduces a helper method to create configuration items with BehaviorSubject and Observable, and uses distinctUntilChanged and shareReplay operators to optimize the observable streams.

ov-components: Refactor configuration management in OpenVidu components

- Updated directive methods to use centralized configuration updates for general, stream, and toolbar settings.
- Replaced individual setter methods with batch update methods for improved performance and maintainability.
- Introduced specific comparison methods for configuration objects to optimize change detection.
- Enhanced the structure of configuration interfaces for better clarity and organization.
- Removed redundant code and streamlined the configuration service for better readability.

ov-components: Enhance participant name handling in PreJoin and Videoconference components
2025-07-29 14:05:14 +02:00
Carlos Santos 68ea8001f1 ci: Update Selenium Chrome version to 138.0 and add internal directives tests workflow 2025-07-29 11:42:56 +02:00
Carlos Santos 5a249fc3e1 ov-components: Add internal directives tests and update related components for recording functionality 2025-07-29 11:38:46 +02:00
Carlos Santos 9bac0f6490 ov-components: Add directive to control visibility of recording list and update related services and components 2025-07-29 10:47:22 +02:00
Carlos Santos fa664c97f1 ov-components: Add view recordings button functionality and related directives 2025-07-29 10:26:25 +02:00
Carlos Santos 8e218ade3c ov-components: Add start/stop recording button directive and update related components 2025-07-28 18:51:20 +02:00
Carlos Santos 22af5c7df6 ov-components: Implement centralized template management for videoconference components 2025-07-22 19:24:07 +02:00
Carlos Santos 04b8b741e2 ov-components: Refactor videoconference component to use centralized state management 2025-07-22 18:17:57 +02:00
Carlos Santos 8af9f75a10 ov-components: Prevent prejoin from showing again after user initiates join process 2025-07-22 17:45:03 +02:00
Carlos Santos fabeaf1471 ov-components: Add ID to external view recording buttons for better accessibility 2025-07-22 17:05:33 +02:00
Carlos Santos 1ffd7ea9d6 ov-components: Fixed bug showing prejoin
- Rename joinSession method to join and update related calls for consistency
refactor
- Change isPrejoin method to showPrejoin in directive config service
2025-07-22 16:23:33 +02:00
cruizba 3af490522e openvidu-deployment: Change DefaultApp string literals to Meet in templates. 2025-07-22 14:08:25 +02:00
cruizba 0280b64084 openvidu-deployment: Azure HA - Replace Default App (OpenVidu Call) with OpenVidu Meet 2025-07-22 14:00:56 +02:00
cruizba 83aad06574 openvidu-deployment: Azure HA - Fix port priority rules. 2025-07-22 13:52:54 +02:00
cruizba 892c6efed2 openvidu-deployment: Azure Elastic - Fix port priority rules. 2025-07-21 21:21:04 +02:00
cruizba 5b888aafc0 openvidu-deployment: Build azure bicep of Elastic. 2025-07-21 21:02:53 +02:00
cruizba d918e8059a openvidu-deployment: Azure Elastic - Replace Default App (OpenVidu Call) with OpenVidu Meet 2025-07-21 21:00:23 +02:00
Carlos Santos 82ddca6b50 ov-components: Add IDs to action buttons in recording activity component for improved accessibility 2025-07-21 17:36:11 +02:00
Carlos Santos 6fb7d9583c ov-components: Enhance recording activity UI with additional status indicators and style adjustments 2025-07-21 17:03:22 +02:00
Carlos Santos 181c5f0789 ov-components: Improves recording activity UI
Refactors the recording activity component's template and styles
to use cards for displaying recording information.

Enhances the display of recording metadata, including duration,
size, and date, with appropriate icons.

Adds visual cues for active recordings and improves overall
responsiveness of the recording list.
2025-07-21 14:12:28 +02:00
Carlos Santos e486665efd ov-components: Updated recording activity component 2025-07-21 14:11:40 +02:00
Carlos Santos b659400c88 ov-components: implement read-only mode and customizable controls for recording activity 2025-07-21 14:11:40 +02:00
cruizba 98c7e3f751 openvidu-deployment: Build azure bicep of Single Node - PRO 2025-07-20 22:56:50 +02:00
cruizba 5e1df8b511 openvidu-deployment: Fix wrong environment variable in Azure Single Node - PRO 2025-07-20 22:42:23 +02:00
cruizba 16ec1f3920 openvidu-deployment: Replace Default App (OpenVidu Call) with OpenVidu Meet in Single Node Pro 2025-07-20 22:23:19 +02:00
cruizba b01e8f4d23 openvidu-deployment: update enabled modules to include openviduMeet in deployment script 2025-07-20 20:59:56 +02:00
cruizba 2413a0bb6d openvidu-deployment: Generate json from bicep Single Node - Community 2025-07-20 20:12:10 +02:00
cruizba bf6091e997 openvidu-deployment: Replace Default App (OpenVidu Call) with OpenVidu Meet in Single Node - Community 2025-07-20 20:10:25 +02:00
cruizba 9e0034dfac openvidu-deployment: Replace Default App (OpenVidu Call) with OpenVidu Meet 2025-07-18 21:53:32 +02:00
Carlos Santos 637142cec6 ov-components: add room initialization checks and error handling in SessionComponent 2025-07-17 17:03:18 +02:00
Carlos Santos c304c9c761 ov-components: add PreJoin directive to support custom pre-join templates in VideoconferenceComponent 2025-07-17 16:53:17 +02:00
Carlos Santos 4dd007395f ov-components: Refactor components to use takeUntil for unsubscribing from observables
- Replaced manual subscription management with takeUntil pattern
- Introduced a destroy$ Subject in each component to handle unsubscriptions on component destruction.
- Improved memory management and code readability by eliminating multiple subscription variables.
2025-07-17 15:44:37 +02:00
Carlos Santos 7573656060 ov-components: refactor VideoconferenceComponent to improve template setup and icon management 2025-07-17 14:00:51 +02:00
Carlos Santos 8407363aaf ov-components: add track subscription and manage room tracks published state 2025-07-17 13:29:17 +02:00
Carlos Santos 55fd64c254 ov-components: enhance recording functionality with track checks and UI updates 2025-07-17 13:29:17 +02:00
cruizba d151834048 openvidu-deployment: Replace OPENVIDU_CALL_SERVER_IMAGE with OPENVIDU_MEET_SERVER_IMAGE in deployment scripts 2025-07-16 12:36:15 +02:00
pabloFuente ce47224400 openvidu-testapp: make update interval for dialog optional 2025-07-14 22:18:12 +02:00
cruizba 61fbf9850b Add TCP port rules for WebRTC traffic on port 7881 and 50000-60000 across multiple deployment configurations 2025-07-11 21:33:05 +02:00
Piwccle ba1df4660c openvidu-testapp: RTCIceCandidate stats for publisher and subscriber peer connections fixed in firefox 2025-07-10 16:12:08 +02:00
pabloFuente d44e24592d openvidu-testapp: RTCIceCandidate stats for publisher and subscriber peer connections 2025-07-09 18:24:00 +02:00
Carlos Santos 91aa127dad ov-components: replace and improve recordingElapsedTime logic 2025-07-04 17:51:30 +02:00
Carlos Santos 1be876678c ov-components: add subscription for virtual background effects management 2025-07-04 15:55:22 +02:00
Carlos Santos 388981be31 ov-components: reorder imports and add ShowDisconnectionDialogDirective to ApiDirectiveModule 2025-07-04 12:51:57 +02:00
Carlos Santos 6497751375 ov-component: add showDisconnectionDialog directive and update service for disconnection dialog management 2025-07-04 12:45:34 +02:00
pabloFuente 5f0639c157 openvidu-test-e2e: adapted egress test to tolerate mediasoup limitation 2025-07-03 14:23:10 +02:00
pabloFuente e435a1a937 openvidu-test-e2e: fix signalTest 2025-07-02 21:53:45 +02:00
pabloFuente 932eda8115 openvidu-test-e2e: include event RoomEvent.ParticipantActive 2025-07-02 18:35:44 +02:00
pabloFuente 5d91f4d343 openvidu-testapp: add RoomEvent.ParticipantActive 2025-07-02 18:14:45 +02:00
pabloFuente 703698b25f openvidu-components-angular: fix comment links 2025-07-02 18:14:07 +02:00
pabloFuente b884e924b6 openvidu-test-e2e: added Egress tests 2025-06-30 14:34:37 +02:00
pabloFuente ee8847c5cb openvidu-testapp: add checkbox to force relay from browser 2025-06-30 12:11:09 +02:00
GitHub Actions 99c787c4f5 Revert "Bump version to 3.3.0"
This reverts commit 2083c078fd.
2025-06-26 20:02:58 +00:00
GitHub Actions 2083c078fd Bump version to 3.3.0 2025-06-26 20:02:55 +00:00
github-actions bc42a72836 openvidu-components-angular: Bumped version to 3.3.0 2025-06-26 18:43:30 +00:00
cruizba c3b7c6f4bb openvidu-deployment: Shutdown gracefully agents in aws and azure. 2025-06-25 19:03:10 +02:00
pabloFuente b913dbb3b8 openvidu-testapp: updated dependencies 2025-06-25 13:26:42 +02:00
pabloFuente f16eefa9df openvidu-testapp: differ between final and non-final transcription events 2025-06-25 13:26:28 +02:00
pabloFuente 96132553ae openvidu-testapp: add lk.transcription handler 2025-06-24 17:26:38 +02:00
Carlos Santos 709779b7fd ov-components: Remove debugger statement from setRecordingStarted method in RecordingService 2025-06-24 16:18:18 +02:00
cruizba 11ac3d32eb openvidu-deployment: Fix wrong image 2025-06-24 11:34:54 +02:00
cruizba c8bbcfed56 openvidu-deployment: Add OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE environment variable to installation scripts 2025-06-24 11:33:21 +02:00
Piwccle 76a6f6c301 openvidu-deployment: azure - Add support for additional install flags in all the deployments 2025-06-18 16:26:50 +02:00
Carlos Santos 2de2920acf ov-components: Add check for existing Material Icons link in VideoconferenceComponent 2025-06-18 10:12:39 +02:00
cruizba fd8be9f23f openvidu-deployment: - AWS HA - Add experimental TURN TLS 2025-06-14 02:02:29 +02:00
cruizba e66e5a23e1 openvidu-deployment: - HA - Open port 5349 in media nodes for master nodes if Turn Domain is not configured 2025-06-13 22:02:13 +02:00
cruizba 335fd8e3c3 Remove unnecessary condition for SecurityGroupIngress in CloudFormation templates 2025-06-13 21:37:39 +02:00
cruizba 237ebe1d59 openvidu-deployment: Add "Additional Installer Flag" 2025-06-13 19:20:25 +02:00
cruizba 5e5e404d7d openvidu-deployment: Remove non official Cloudformation 2025-06-13 19:20:25 +02:00
Carlos Santos 16e869c5da ov-components: update API directive documentation 2025-06-11 12:50:08 +02:00
Carlos Santos c628b2ab68 ov-components: enhance directive table generation logic and improve error handling 2025-06-11 12:49:41 +02:00
Carlos Santos b7d9f822de ov-components: apply background from storage based on background effects button status 2025-06-10 17:28:00 +02:00
Piwccle fe606cbf15 openvidu-deployment: azure - added cluster data container for caddy and observability info 2025-06-10 16:30:41 +02:00
Piwccle 7c71f41d95 openvidu-deployment: azure - removed one '}' that was making TURN HA fail 2025-06-10 12:48:12 +02:00
Piwccle 9728d966ad openvidu-deployment: azure - bugfix: added dependsOn to remove possible race condition 2025-06-10 12:17:10 +02:00
Piwccle ce610cab03 openvidu-deployment: azure - removed parameter from HA CUID 2025-06-10 11:46:42 +02:00
Piwccle d8f14c6905 openvidu-deployment: azure - changes to let TURN work in HA deployment 2025-06-10 11:34:57 +02:00
Piwccle bdf4f07a28 openvidu-deployment: azure - removed one line of the install script that was there to try the new delete_media_node 2025-06-10 11:08:55 +02:00
Piwccle 206a51baf7 openvidu-deployment: azure - added scripts to delete media node if fails 2025-06-10 10:56:50 +02:00
Carlos Santos 94b51b9971 ov-components: Update language files to replace "session" with "room" for consistency across translations 2025-06-09 11:21:35 +02:00
github-actions 8984cfdcad openvidu-components-angular: Bumped version to 3.2.1 2025-06-05 11:01:42 +00:00
Carlos Santos 022d18578e ov-components: Remove unnecessary mat-line directive from settings panel options 2025-06-05 12:49:53 +02:00
GitHub Actions 9beb8b435a Revert "Bump version to 3.2.0"
This reverts commit 26b790bfd8.
2025-06-04 15:01:06 +00:00
114 changed files with 7294 additions and 4901 deletions

View File

@ -63,7 +63,7 @@ jobs:
# - 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:127.0
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
@ -100,7 +100,7 @@ jobs:
# - 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:127.0
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
@ -133,7 +133,7 @@ jobs:
# - 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:127.0
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
@ -166,7 +166,7 @@ jobs:
# - 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:127.0
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
@ -180,6 +180,38 @@ jobs:
- 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
@ -199,7 +231,7 @@ jobs:
# - 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:127.0
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
@ -232,7 +264,7 @@ jobs:
# - 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:127.0
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
@ -265,7 +297,7 @@ jobs:
# - 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:127.0
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
@ -298,7 +330,7 @@ jobs:
# - 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:127.0
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
@ -331,7 +363,7 @@ jobs:
# - 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:127.0
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
@ -364,7 +396,7 @@ jobs:
# - 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:127.0
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
@ -397,7 +429,7 @@ jobs:
# - 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:127.0
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

View File

@ -0,0 +1,84 @@
import { Builder, WebDriver } from 'selenium-webdriver';
import { OpenViduComponentsPO } from './utils.po.test';
import { TestAppConfig } from './selenium.conf';
let url = '';
describe('Testing Internal Directives', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
url = `${TestAppConfig.appUrl}&roomName=INTERNAL_DIRECTIVES_${Math.floor(Math.random() * 1000)}`;
});
afterEach(async () => {
try {
} catch (error) {}
await browser.sleep(500);
await browser.quit();
});
it('should show/hide toolbar view recording button with toolbarViewRecordingsButton directive', async () => {
await browser.get(`${url}&prejoin=false&toolbarViewRecordingsButton=true`);
await utils.checkSessionIsPresent();
await utils.toggleToolbarMoreOptions();
expect(await utils.isPresent('#view-recordings-btn')).toBeTrue();
await browser.get(`${url}&prejoin=false`);
await browser.navigate().refresh();
await utils.checkSessionIsPresent();
await utils.toggleToolbarMoreOptions();
expect(await utils.isPresent('#view-recordings-btn')).toBeFalse();
});
it('should show/hide participant name in prejoin with prejoinDisplayParticipantName directive', async () => {
await browser.get(`${url}&prejoin=true`);
await utils.checkPrejoinIsPresent();
expect(await utils.isPresent('.participant-name-container')).toBeTrue();
await browser.get(`${url}&prejoin=true&prejoinDisplayParticipantName=false`);
await browser.navigate().refresh();
await utils.checkPrejoinIsPresent();
expect(await utils.isPresent('.participant-name-container')).toBeFalse();
});
it('should show/hide view recordings button with recordingActivityViewRecordingsButton directive', async () => {
await browser.get(`${url}&prejoin=false&recordingActivityViewRecordingsButton=true`);
await utils.checkSessionIsPresent();
await utils.togglePanel('activities');
await utils.clickOn('#recording-activity');
expect(await utils.isPresent('#view-recordings-btn')).toBeTrue();
await browser.get(`${url}&prejoin=false`);
await browser.navigate().refresh();
await utils.checkSessionIsPresent();
await utils.togglePanel('activities');
await utils.clickOn('#recording-activity');
expect(await utils.isPresent('#view-recordings-btn')).toBeFalse();
});
it('should show/hide start/stop recording buttons with recordingActivityStartStopRecordingButton directive', async () => {
await browser.get(`${url}&prejoin=false&recordingActivityStartStopRecordingButton=false`);
await utils.checkSessionIsPresent();
await utils.togglePanel('activities');
await utils.clickOn('#recording-activity');
expect(await utils.isPresent('#start-recording-btn')).toBeFalse();
await browser.sleep(3000);
await browser.get(`${url}&prejoin=false`);
await browser.navigate().refresh();
await utils.checkSessionIsPresent();
await utils.togglePanel('activities');
await utils.clickOn('#recording-activity');
expect(await utils.isPresent('#start-recording-btn')).toBeTrue();
});
});

View File

@ -679,12 +679,13 @@ describe('Stream UI controls and interaction features', () => {
await browser.sleep(1000);
const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[1]);
await utils.clickOn('#mic-btn');
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote.speaking');
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
// 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

@ -195,5 +195,7 @@ export class OpenViduComponentsPO {
await this.clickOn('#toolbar-settings-btn');
break;
}
await this.browser.sleep(500);
}
}

View File

@ -1,12 +1,12 @@
{
"name": "openvidu-components-testapp",
"version": "3.2.0",
"version": "3.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvidu-components-testapp",
"version": "3.2.0",
"version": "3.3.0",
"dependencies": {
"@angular/animations": "19.2.8",
"@angular/cdk": "19.2.11",
@ -35,7 +35,7 @@
"@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12",
"chromedriver": "136.0.2",
"chromedriver": "138.0.0",
"concat": "^1.0.3",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0",
@ -8126,9 +8126,9 @@
}
},
"node_modules/chromedriver": {
"version": "136.0.2",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-136.0.2.tgz",
"integrity": "sha512-yJ52GN01edLYWYK/OspYBv3plzF08Ucdq4ukYigJGOX8dWr/tP5PXSZPWFPVarmbmcO57pNLP9Im8hsYljMEjw==",
"version": "138.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.0.tgz",
"integrity": "sha512-bJ/DNm5Y0TbqM71ARaAohTWVwcQ2SsWciYC5Q9Ul7DC/oTxm6B1vI2h6WscFCOOi49ul4tXZVjA/LOruljjmjA==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@ -8145,7 +8145,7 @@
"chromedriver": "bin/chromedriver"
},
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/cli-cursor": {

View File

@ -27,7 +27,7 @@
"@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12",
"chromedriver": "136.0.2",
"chromedriver": "138.0.0",
"concat": "^1.0.3",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0",
@ -89,6 +89,7 @@
"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",
@ -99,5 +100,5 @@
"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.2.0"
"version": "3.3.0"
}

View File

@ -3,168 +3,334 @@ const glob = require('glob');
const startApiLine = '<!-- start-dynamic-api-directives-content -->';
const apiDirectivesTable =
'| **Parameter** | **Type** | **Reference** | \n' +
'|:--------------------------------: | :-------: | :---------------------------------------------: |';
'| **Parameter** | **Type** | **Reference** | \n' +
'|:--------------------------------: | :-------: | :---------------------------------------------: |';
const endApiLine = '<!-- end-dynamic-api-directives-content -->';
/**
* Get all directive files from the API directives directory
*/
function getDirectiveFiles() {
// Directory where directive files are located
const directivesDir = 'projects/openvidu-components-angular/src/lib/directives/api';
return listFiles(directivesDir, '.directive.ts');
const directivesDir = 'projects/openvidu-components-angular/src/lib/directives/api';
return listFiles(directivesDir, '.directive.ts');
}
/**
* Get all component files
*/
function getComponentFiles() {
// Directory where component files are located
const componentsDir = 'projects/openvidu-components-angular/src/lib/components';
return listFiles(componentsDir, '.component.ts');
const componentsDir = 'projects/openvidu-components-angular/src/lib/components';
return listFiles(componentsDir, '.component.ts');
}
/**
* Get all admin files
*/
function getAdminFiles() {
// Directory where component files are located
const componentsDir = 'projects/openvidu-components-angular/src/lib/admin';
return listFiles(componentsDir, '.component.ts');
const componentsDir = 'projects/openvidu-components-angular/src/lib/admin';
return listFiles(componentsDir, '.component.ts');
}
/**
* List all files with specific extension in directory
*/
function listFiles(directoryPath, fileExtension) {
const files = glob.sync(`${directoryPath}/**/*${fileExtension}`);
if (files.length === 0) {
throw new Error(`No ${fileExtension} files found in ${directoryPath}`);
}
return files;
const files = glob.sync(`${directoryPath}/**/*${fileExtension}`);
if (files.length === 0) {
throw new Error(`No ${fileExtension} files found in ${directoryPath}`);
}
return files;
}
/**
* Extract component selector from component file
*/
function getComponentSelector(componentFile) {
const componentContent = fs.readFileSync(componentFile, 'utf8');
const selectorMatch = componentContent.match(/@Component\({[^]*?selector:\s*['"]([^'"]+)['"][^]*?}\)/s);
if (!selectorMatch) {
throw new Error(`Unable to find selector in component file: ${componentFile}`);
}
return selectorMatch[1];
}
/**
* Check if a directive class has @internal annotation
*/
function isInternalDirective(directiveContent, className) {
const classRegex = new RegExp(`(/\\*\\*[\\s\\S]*?\\*/)?\\s*@Directive\\([\\s\\S]*?\\)\\s*export\\s+class\\s+${escapeRegex(className)}`, 'g');
const match = classRegex.exec(directiveContent);
if (match && match[1]) {
return match[1].includes('@internal');
}
return false;
}
/**
* Extract attribute name from selector for a specific component
*/
function extractAttributeForComponent(selector, componentSelector) {
// Split selector by comma and trim whitespace
const selectorParts = selector.split(',').map(part => part.trim());
// Find the part that matches our component
for (const part of selectorParts) {
if (part.includes(componentSelector)) {
// Extract attribute from this specific part
const attributeMatch = part.match(/\[([^\]]+)\]/);
if (attributeMatch) {
return attributeMatch[1];
}
}
}
// Fallback: if no specific match, return the first attribute found
const fallbackMatch = selector.match(/\[([^\]]+)\]/);
return fallbackMatch ? fallbackMatch[1] : null;
}
/**
* Extract all directive classes from a directive file
*/
function extractDirectiveClasses(directiveContent) {
const classes = [];
// Regex to find all directive class definitions with their preceding @Directive decorators
const directiveClassRegex = /@Directive\(\s*{\s*selector:\s*['"]([^'"]+)['"][^}]*}\s*\)\s*export\s+class\s+(\w+)/gs;
let match;
while ((match = directiveClassRegex.exec(directiveContent)) !== null) {
const selector = match[1];
const className = match[2];
// Skip internal directives
if (isInternalDirective(directiveContent, className)) {
console.log(`Skipping internal directive: ${className}`);
continue;
}
classes.push({
selector,
className
});
}
return classes;
}
/**
* Extract all directives from a directive file that match a component selector
*/
function extractDirectivesForComponent(directiveFile, componentSelector) {
const directiveContent = fs.readFileSync(directiveFile, 'utf8');
const directives = [];
// Get all directive classes in the file (excluding internal ones)
const directiveClasses = extractDirectiveClasses(directiveContent);
// Filter classes that match the component selector
const matchingClasses = directiveClasses.filter(directiveClass =>
directiveClass.selector.includes(componentSelector)
);
// For each matching class, extract input type information
matchingClasses.forEach(directiveClass => {
// Extract the correct attribute name for this component
const attributeName = extractAttributeForComponent(directiveClass.selector, componentSelector);
if (attributeName) {
const inputInfo = extractInputInfo(directiveContent, attributeName, directiveClass.className);
if (inputInfo) {
directives.push({
attribute: attributeName,
type: inputInfo.type,
className: directiveClass.className
});
}
}
});
return directives;
}
/**
* Extract input information (type) for a specific attribute and class
*/
function extractInputInfo(directiveContent, attributeName, className) {
// Create a regex to find the specific class section
const classRegex = new RegExp(`export\\s+class\\s+${escapeRegex(className)}[^}]*?{([^]*?)(?=export\\s+class|$)`, 's');
const classMatch = directiveContent.match(classRegex);
if (!classMatch) {
console.warn(`Could not find class ${className}`);
return null;
}
const classContent = classMatch[1];
// Regex to find the @Input setter for this attribute within the class
const inputRegex = new RegExp(
`@Input\\(\\)\\s+set\\s+${escapeRegex(attributeName)}\\s*\\(\\s*\\w+:\\s*([^)]+)\\s*\\)`,
'g'
);
const inputMatch = inputRegex.exec(classContent);
if (!inputMatch) {
console.warn(`Could not find @Input setter for attribute: ${attributeName} in class: ${className}`);
return null;
}
let type = inputMatch[1].trim();
// Clean up the type (remove extra whitespace, etc.)
type = type.replace(/\s+/g, ' ');
return {
type: type
};
}
/**
* Escape special regex characters
*/
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Generate API directives table for components
*/
function generateApiDirectivesTable(componentFiles, directiveFiles) {
componentFiles.forEach((componentFile) => {
try {
console.log(`Processing component: ${componentFile}`);
const componentSelector = getComponentSelector(componentFile);
const readmeFilePath = componentFile.replace('.ts', '.md');
console.log(`Component selector: ${componentSelector}`);
// Initialize table with header
initializeDynamicTableContent(readmeFilePath);
const allDirectives = [];
// Extract directives from all directive files
directiveFiles.forEach((directiveFile) => {
console.log(`Checking directive file: ${directiveFile}`);
const directives = extractDirectivesForComponent(directiveFile, componentSelector);
allDirectives.push(...directives);
});
console.log(`Found ${allDirectives.length} directives for ${componentSelector}`);
// Sort directives alphabetically by attribute name
allDirectives.sort((a, b) => a.attribute.localeCompare(b.attribute));
// Add rows to table
allDirectives.forEach((directive) => {
addRowToTable(readmeFilePath, directive.attribute, directive.type, directive.className);
});
// If no directives found, add "no directives" message
if (allDirectives.length === 0) {
removeApiTableContent(readmeFilePath);
}
} catch (error) {
console.error(`Error processing component ${componentFile}:`, error.message);
}
});
}
/**
* Initialize table with header
*/
function initializeDynamicTableContent(filePath) {
replaceDynamicTableContent(filePath, apiDirectivesTable);
replaceDynamicTableContent(filePath, apiDirectivesTable);
}
/**
* Replace table content with "no directives" message
*/
function removeApiTableContent(filePath) {
const content = '_No API directives available for this component_. \n';
replaceDynamicTableContent(filePath, content);
const content = '_No API directives available for this component_. \n';
replaceDynamicTableContent(filePath, content);
}
function apiTableContentIsEmpty(filePath) {
try {
const data = fs.readFileSync(filePath, 'utf8');
const startIdx = data.indexOf(startApiLine);
const endIdx = data.indexOf(endApiLine);
if (startIdx !== -1 && endIdx !== -1) {
const capturedContent = data.substring(startIdx + startApiLine.length, endIdx).trim();
return capturedContent === apiDirectivesTable;
}
return false;
} catch (error) {
return false;
}
}
function writeApiDirectivesTable(componentFiles, directiveFiles) {
componentFiles.forEach((componentFile) => {
// const componentName = componentFile.split('/').pop()
const componentFileName = componentFile.split('/').pop().replace('.component.ts', '');
const componentName = componentFileName.replace(/(?:^|-)([a-z])/g, (_, char) => char.toUpperCase());
const readmeFilePath = componentFile.replace('.ts', '.md');
const componentContent = fs.readFileSync(componentFile, 'utf8');
const selectorMatch = componentContent.match(/@Component\({[^]*?selector: ['"]([^'"]+)['"][^]*?}\)/);
const componentSelectorName = selectorMatch[1];
initializeDynamicTableContent(readmeFilePath);
if (!componentSelectorName) {
throw new Error(`Unable to find the component name in the file ${componentFileName}`);
}
// const directiveRegex = new RegExp(`@Directive\\(\\s*{[^}]*selector:\\s*['"]${componentName}\\s*\\[([^'"]+)\\]`, 'g');
const directiveRegex = /^\s*(selector):\s*(['"])(.*?)\2\s*$/gm;
directiveFiles.forEach((directiveFile) => {
const directiveContent = fs.readFileSync(directiveFile, 'utf8');
let directiveNameMatch;
while ((directiveNameMatch = directiveRegex.exec(directiveContent)) !== null) {
if (directiveNameMatch[0].includes('@Directive({\n//')) {
// Skip directives that are commented out
continue;
}
const selectorValue = directiveNameMatch[3].split(',');
const directiveMatch = selectorValue.find((value) => value.includes(componentSelectorName));
if (directiveMatch) {
const directiveName = directiveMatch.match(/\[(.*?)\]/).pop();
const className = directiveName.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase()) + 'Directive';
const inputRegex = new RegExp(
`@Input\\(\\)\\s+set\\s+(${directiveName.replace(/\[/g, '\\[').replace(/\]/g, '\\]')})\\((\\w+):\\s+(\\w+)`
);
const inputMatch = directiveContent.match(inputRegex);
const inputType = inputMatch && inputMatch.pop();
if (inputType && className) {
let finalClassName = componentName === 'Videoconference' ? className : componentName + className;
addRowToTable(readmeFilePath, directiveName, inputType, finalClassName);
}
} else {
console.log(`The selector "${componentSelectorName}" does not match with ${selectorValue}. Skipping...`);
}
}
});
if (apiTableContentIsEmpty(readmeFilePath)) {
removeApiTableContent(readmeFilePath);
}
});
}
// Function to add a row to a Markdown table in a file
/**
* Add a row to the markdown table
*/
function addRowToTable(filePath, parameter, type, reference) {
// Read the current content of the file
try {
const data = fs.readFileSync(filePath, 'utf8');
try {
const data = fs.readFileSync(filePath, 'utf8');
const markdownRow = `| **${parameter}** | \`${type}\` | [${reference}](../directives/${reference}.html) |`;
// Define the target line and the Markdown row
const markdownRow = `| **${parameter}** | \`${type}\` | [${reference}](../directives/${reference}.html) |`;
const lines = data.split('\n');
const targetIndex = lines.findIndex((line) => line.includes(endApiLine));
// Find the line that contains the table
const lines = data.split('\n');
const targetIndex = lines.findIndex((line) => line.includes(endApiLine));
if (targetIndex !== -1) {
// Insert the new row above the target line
lines.splice(targetIndex, 0, markdownRow);
// Join the lines back together
const updatedContent = lines.join('\n');
// Write the updated content to the file
fs.writeFileSync(filePath, updatedContent, 'utf8');
console.log('Row added successfully.');
} else {
console.error('Table not found in the file.');
}
} catch (error) {
console.error('Error writing to file:', error);
}
if (targetIndex !== -1) {
lines.splice(targetIndex, 0, markdownRow);
const updatedContent = lines.join('\n');
fs.writeFileSync(filePath, updatedContent, 'utf8');
console.log(`Added directive: ${parameter} -> ${reference}`);
} else {
console.error('End marker not found in file:', filePath);
}
} catch (error) {
console.error('Error adding row to table:', error);
}
}
/**
* Replace content between start and end markers
*/
function replaceDynamicTableContent(filePath, content) {
// Read the current content of the file
try {
const data = fs.readFileSync(filePath, 'utf8');
const pattern = new RegExp(`${startApiLine}([\\s\\S]*?)${endApiLine}`, 'g');
try {
const data = fs.readFileSync(filePath, 'utf8');
const pattern = new RegExp(`${startApiLine}([\\s\\S]*?)${endApiLine}`, 'g');
// Replace the content between startLine and endLine with the replacement table
const modifiedContent = data.replace(pattern, (match, capturedContent) => {
return startApiLine + '\n' + content + '\n' + endApiLine;
});
// Write the modified content back to the file
fs.writeFileSync(filePath, modifiedContent, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') {
console.log(`${filePath} not found! Maybe it is an internal component. Skipping...`);
} else {
console.error('Error writing to file:', error);
}
}
const modifiedContent = data.replace(pattern, (match, capturedContent) => {
return startApiLine + '\n' + content + '\n' + endApiLine;
});
fs.writeFileSync(filePath, modifiedContent, 'utf8');
console.log(`Updated table content in: ${filePath}`);
} catch (error) {
if (error.code === 'ENOENT') {
console.log(`${filePath} not found! Maybe it is an internal component. Skipping...`);
} else {
console.error('Error writing to file:', error);
}
}
}
const directiveFiles = getDirectiveFiles();
const componentFiles = getComponentFiles();
const adminFiles = getAdminFiles();
writeApiDirectivesTable(componentFiles.concat(adminFiles), directiveFiles);
// Main execution
if (require.main === module) {
try {
const directiveFiles = getDirectiveFiles();
const componentFiles = getComponentFiles();
const adminFiles = getAdminFiles();
console.log('Starting directive table generation...');
generateApiDirectivesTable(componentFiles.concat(adminFiles), directiveFiles);
console.log('Directive table generation completed!');
} catch (error) {
console.error('Script execution failed:', error);
process.exit(1);
}
}
// Export functions for testing
module.exports = {
generateApiDirectivesTable,
getDirectiveFiles,
getComponentFiles,
getAdminFiles
};

View File

@ -1,12 +1,12 @@
{
"name": "openvidu-components-angular",
"version": "3.2.0",
"version": "3.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvidu-components-angular",
"version": "3.2.0",
"version": "3.3.0",
"dependencies": {
"tslib": "^2.3.0"
},

View File

@ -15,5 +15,5 @@
"livekit-client": "^2.1.0",
"@livekit/track-processors": "^0.3.2"
},
"version": "3.2.0"
"version": "3.3.0"
}

View File

@ -4,5 +4,6 @@ With the following directives you can modify the default User Interface with the
<!-- start-dynamic-api-directives-content -->
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **recordingsList** | `RecordingInfo` | [AdminDashboardRecordingsListDirective](../directives/AdminDashboardRecordingsListDirective.html) |
| **navbarTitle** | `string` | [AdminDashboardTitleDirective](../directives/AdminDashboardTitleDirective.html) |
| **recordingsList** | `RecordingInfo[]` | [AdminDashboardRecordingsListDirective](../directives/AdminDashboardRecordingsListDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

@ -6,4 +6,5 @@ With the following directives you can modify the default User Interface with the
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **error** | `any` | [AdminLoginErrorDirective](../directives/AdminLoginErrorDirective.html) |
| **navbarTitle** | `any` | [AdminLoginTitleDirective](../directives/AdminLoginTitleDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

@ -19,6 +19,11 @@
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
</div>
<!-- Render additional layout elements injected via ovAdditionalLayoutElement -->
@if (layoutAdditionalElementsTemplate) {
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
}
<div
*ngFor="let track of remoteParticipants | tracks; trackBy: trackParticipantElement"
class="remote-participant"

View File

@ -21,6 +21,6 @@ It will recognise the following directive in a child element.
With the following directives you can modify the default User Interface with the aim of fully customizing your videoconference application.
<!-- start-dynamic-api-directives-content -->
_No API directives available for this component_.
_No API directives available for this component_.
<!-- end-dynamic-api-directives-content -->

View File

@ -1,3 +1,5 @@
import { LayoutAdditionalElementsDirective } from '../../directives/template/internals.directive';
import {
AfterViewInit,
ChangeDetectionStrategy,
@ -11,7 +13,7 @@ import {
ViewChild,
ViewContainerRef
} from '@angular/core';
import { combineLatest, map, Subscription } from 'rxjs';
import { combineLatest, map, Subject, takeUntil } from 'rxjs';
import { StreamDirective } from '../../directives/template/openvidu-components-angular.directive';
import { ParticipantTrackPublication, ParticipantModel } from '../../models/participant.model';
import { LayoutService } from '../../services/layout/layout.service';
@ -20,6 +22,7 @@ import { CdkDrag } from '@angular/cdk/drag-drop';
import { PanelService } from '../../services/panel/panel.service';
import { GlobalConfigService } from '../../services/config/global-config.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LayoutTemplateConfiguration, TemplateManagerService } from '../../services/template/template-manager.service';
/**
*
@ -39,6 +42,11 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
*/
@ContentChild('stream', { read: TemplateRef }) streamTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ContentChild('layoutAdditionalElements', { read: TemplateRef }) layoutAdditionalElementsTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ -62,9 +70,27 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
// is inside of the layout component tagged with '*ovLayout' directive
if (externalStream) {
this.streamTemplate = externalStream.template;
this.updateTemplatesAndMarkForCheck();
}
}
/**
* @ignore
*/
@ContentChild(LayoutAdditionalElementsDirective) set externalAdditionalElements(
externalAdditionalElements: LayoutAdditionalElementsDirective
) {
if (externalAdditionalElements) {
this._externalLayoutAdditionalElements = externalAdditionalElements;
this.updateTemplatesAndMarkForCheck();
}
}
/**
* @ignore
*/
templateConfig: LayoutTemplateConfiguration = {};
localParticipant: ParticipantModel | undefined;
remoteParticipants: ParticipantModel[] = [];
/**
@ -72,11 +98,11 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
*/
captionsEnabled = true;
private localParticipantSubs: Subscription;
private remoteParticipantsSubs: Subscription;
private captionsSubs: Subscription;
private _externalStream?: StreamDirective;
private _externalLayoutAdditionalElements?: LayoutAdditionalElementsDirective;
private destroy$ = new Subject<void>();
private resizeObserver: ResizeObserver;
private cdkSubscription: Subscription;
private resizeTimeout: NodeJS.Timeout;
private videoIsAtRight: boolean = false;
private lastLayoutWidth: number = 0;
@ -90,10 +116,13 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
private participantService: ParticipantService,
private globalService: GlobalConfigService,
private directiveService: OpenViduComponentsConfigService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) {}
ngOnInit(): void {
this.setupTemplates();
this.subscribeToParticipants();
this.subscribeToCaptions();
}
@ -107,13 +136,11 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.localParticipant = undefined;
this.remoteParticipants = [];
this.resizeObserver?.disconnect();
this.localParticipantSubs?.unsubscribe();
this.remoteParticipantsSubs?.unsubscribe();
this.captionsSubs?.unsubscribe();
this.cdkSubscription?.unsubscribe();
this.layoutService.clear();
}
@ -126,8 +153,36 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
return track;
}
private setupTemplates() {
this.templateConfig = this.templateManagerService.setupLayoutTemplates(
this._externalStream,
this._externalLayoutAdditionalElements
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
private applyTemplateConfiguration() {
if (this.templateConfig.layoutStreamTemplate) {
this.streamTemplate = this.templateConfig.layoutStreamTemplate;
}
if (this.templateConfig.layoutAdditionalElementsTemplate) {
this.layoutAdditionalElementsTemplate = this.templateConfig.layoutAdditionalElementsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
private subscribeToCaptions() {
this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => {
this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.captionsEnabled = value;
this.cd.markForCheck();
this.layoutService.update();
@ -135,7 +190,7 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToParticipants() {
this.localParticipantSubs = this.participantService.localParticipant$.subscribe((p) => {
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p) => {
if (p) {
this.localParticipant = p;
if (!this.localParticipant?.isMinimized) {
@ -146,14 +201,12 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
}
});
this.remoteParticipantsSubs = combineLatest([
this.participantService.remoteParticipants$,
this.directiveService.layoutRemoteParticipants$
])
combineLatest([this.participantService.remoteParticipants$, this.directiveService.layoutRemoteParticipants$])
.pipe(
map(([serviceParticipants, directiveParticipants]) =>
directiveParticipants !== undefined ? directiveParticipants : serviceParticipants
)
),
takeUntil(this.destroy$)
)
.subscribe((participants) => {
this.remoteParticipants = participants;
@ -218,7 +271,8 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
this.videoIsAtRight = false;
}
};
this.cdkSubscription = this.cdkDrag.released.subscribe(handler);
this.cdkDrag.released.pipe(takeUntil(this.destroy$)).subscribe(handler);
if (this.globalService.isProduction()) return;
// Just for allow E2E testing with drag and drop

View File

@ -17,6 +17,8 @@
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
></ov-recording-activity>
<ov-broadcasting-activity
*ngIf="showBroadcastingActivity"

View File

@ -4,6 +4,6 @@ With the following directives you can modify the default User Interface with the
<!-- start-dynamic-api-directives-content -->
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **recordingActivity** | `boolean` | [ActivitiesPanelRecordingActivityDirective](../directives/ActivitiesPanelRecordingActivityDirective.html) |
| **broadcastingActivity** | `boolean` | [ActivitiesPanelBroadcastingActivityDirective](../directives/ActivitiesPanelBroadcastingActivityDirective.html) |
| **recordingActivity** | `boolean` | [ActivitiesPanelRecordingActivityDirective](../directives/ActivitiesPanelRecordingActivityDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import { PanelStatusInfo, PanelType } from '../../../models/panel.model';
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
import { PanelService } from '../../../services/panel/panel.service';
@ -54,6 +54,21 @@ export class ActivitiesPanelComponent implements OnInit {
*/
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
/**
* @internal
* Provides event notifications that fire when view recordings button has been clicked.
* This event is triggered when the user wants to view all recordings in an external page.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* @internal
* Provides event notifications that fire when view recording button has been clicked.
* This event is triggered when the user wants to view a specific recording in an external page.
* It provides the recording ID as event data.
*/
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when start broadcasting button is clicked.
* It provides the {@link BroadcastingStartRequestedEvent} payload as event data.
@ -80,9 +95,7 @@ export class ActivitiesPanelComponent implements OnInit {
* @internal
*/
showBroadcastingActivity: boolean = true;
private panelSubscription: Subscription;
private recordingActivitySub: Subscription;
private broadcastingActivitySub: Subscription;
private destroy$ = new Subject<void>();
/**
* @internal
@ -105,9 +118,8 @@ export class ActivitiesPanelComponent implements OnInit {
* @internal
*/
ngOnDestroy() {
if (this.panelSubscription) this.panelSubscription.unsubscribe();
if (this.recordingActivitySub) this.recordingActivitySub.unsubscribe();
if (this.broadcastingActivitySub) this.broadcastingActivitySub.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
/**
@ -118,7 +130,7 @@ export class ActivitiesPanelComponent implements OnInit {
}
private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
if (ev.panelType === PanelType.ACTIVITIES && !!ev.subOptionType) {
this.expandedPanel = ev.subOptionType;
}
@ -126,12 +138,12 @@ export class ActivitiesPanelComponent implements OnInit {
}
private subscribeToActivitiesPanelDirective() {
this.recordingActivitySub = this.libService.recordingActivity$.subscribe((value: boolean) => {
this.libService.recordingActivity$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showRecordingActivity = value;
this.cd.markForCheck();
});
this.broadcastingActivitySub = this.libService.broadcastingActivity$.subscribe((value: boolean) => {
this.libService.broadcastingActivity$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showBroadcastingActivity = value;
this.cd.markForCheck();
});

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import {
BroadcastingStartRequestedEvent,
BroadcastingStatus,
@ -76,7 +76,7 @@ export class BroadcastingActivityComponent implements OnInit {
*/
isPanelOpened: boolean = false;
private broadcastingSub: Subscription;
private destroy$ = new Subject<void>();
/**
* @internal
@ -99,7 +99,8 @@ export class BroadcastingActivityComponent implements OnInit {
* @internal
*/
ngOnDestroy() {
if (this.broadcastingSub) this.broadcastingSub.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
/**
@ -147,7 +148,7 @@ export class BroadcastingActivityComponent implements OnInit {
}
private subscribeToBroadcastingStatus() {
this.broadcastingSub = this.broadcastingService.broadcastingStatusObs.subscribe((event: BroadcastingStatusInfo | undefined) => {
this.broadcastingService.broadcastingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: BroadcastingStatusInfo | undefined) => {
if (!!event) {
const { status, broadcastingId, error } = event;
this.broadcastingStatus = status;

View File

@ -72,6 +72,372 @@
text-align: center;
}
.recording-placeholder {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.recording-placeholder-img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
.empty-state {
margin-bottom: 20px;
}
.recording-status-messages {
margin-top: 10px;
}
.recording-status {
display: flex;
align-items: flex-start;
gap: 12px;
border: 1px solid var(--ov-warn-color);
border-radius: 8px;
padding: 12px 16px;
margin: 16px 0;
font-size: 15px;
box-shadow: 0 2px 8px 0 rgba(255, 193, 7, 0.04);
.status-icon {
font-size: 28px;
color: var(--ov-warn-color);
flex-shrink: 0;
margin-top: 2px;
}
.status-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.status-title {
font-weight: 600;
font-size: 15px;
margin-bottom: 2px;
}
.status-message {
font-size: 14px;
opacity: 0.85;
}
}
.recording-status-starting {
background: rgba(255, 193, 7, 0.08);
border-color: var(--ov-warn-color);
}
.recording-status-stopping {
background: rgba(255, 193, 7, 0.13);
border-color: var(--ov-warn-color);
}
.recording-error-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
// Modern recording list styles
.recording-list-container {
display: flex;
flex-direction: column;
gap: 10px;
padding-top: 16px;
max-height: 500px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: var(--ov-accent-action-color);
border-radius: 2px;
opacity: 0.3;
}
&::-webkit-scrollbar-thumb:hover {
opacity: 0.6;
}
}
.recording-card {
background: var(--ov-surface-background-color);
border: 1px solid rgba(0, 102, 204, 0.1);
border-radius: var(--ov-surface-radius);
padding: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
flex-shrink: 0;
box-sizing: border-box;
&.recording-active {
background: linear-gradient(135deg, transparent 69%, var(--ov-error-color) 250%);
}
}
.recording-header {
display: flex;
align-items: flex-start;
gap: 5px;
width: 100%;
height: 60px;
flex-shrink: 0;
}
.recording-status-indicator {
flex-shrink: 0;
padding-top: 2px;
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
&.recording-live {
background: #ffffff;
box-shadow: 0 0 0 4px var(--ov-error-color);
animation: pulse-dot 2s infinite;
}
&.recording-stopping {
background: var(--ov-warn-color);
animation: pulse-dot 2s infinite;
}
&.recording-failed {
background: var(--ov-error-color);
}
&.recording-ready {
background: #4caf50;
}
}
.recording-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow: hidden;
}
.recording-name {
font-size: 14px;
font-weight: 500;
color: var(--ov-text-surface-color);
margin-bottom: 4px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 17px;
}
.recording-status-text {
font-size: 12px;
font-weight: 500;
&.recording-live-text {
color: var(--ov-primary-action-color);
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
.recording-metadata {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 4px;
height: auto;
overflow: visible;
}
.metadata-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--ov-text-surface-color);
opacity: 0.7;
white-space: nowrap;
flex-shrink: 0;
.metadata-icon {
font-size: 14px;
width: 14px;
height: 14px;
flex-shrink: 0;
}
}
.recording-actions-menu {
display: flex;
gap: 8px;
flex-shrink: 0;
opacity: 1;
align-items: center;
width: 100%;
justify-content: center;
height: 32px;
margin-top: auto;
}
.action-btn {
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
&.action-play {
color: var(--ov-accent-action-color);
&:hover {
background: rgba(0, 102, 204, 0.1);
color: var(--ov-accent-action-color);
}
}
&.action-view {
color: var(--ov-accent-action-color);
border-radius: var(--ov-surface-radius);
}
&.action-download {
color: #4caf50;
&:hover {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
}
}
&.action-delete {
color: var(--ov-error-color);
&:hover {
background: rgba(244, 67, 54, 0.1);
color: var(--ov-error-color);
}
}
}
// Animations
@keyframes pulse-dot {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.8;
}
}
@keyframes pulse-border {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.recording-actions {
display: flex;
gap: 5px;
}
.action-button {
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
// Mobile responsive design for new recording cards
@media (max-width: 768px) {
.recording-list-container {
padding-top: 12px;
gap: 12px;
}
.recording-card {
padding: 8px;
height: 100px;
gap: 8px;
}
.recording-header {
gap: 8px;
height: 50px;
}
.recording-info {
min-width: 0;
}
.recording-metadata {
gap: 8px;
margin-top: 2px;
}
.metadata-item {
font-size: 11px;
gap: 2px;
.metadata-icon {
font-size: 12px;
width: 12px;
height: 12px;
}
}
.recording-actions-menu {
opacity: 1; // Always visible on mobile
gap: 6px;
height: 28px;
}
.action-btn {
width: 28px;
height: 28px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
}
}
.recording-message {
color: var(--ov-text-surface-color);
}
@ -80,14 +446,84 @@
color: var(--ov-error-color);
font-weight: 600;
}
.recording-name {
font-size: 14px;
font-weight: bold;
.recording-error {
display: flex;
align-items: flex-start;
gap: 12px;
background: rgba(244, 67, 54, 0.08);
border: 1px solid var(--ov-error-color);
border-radius: 8px;
padding: 12px 16px;
margin: 16px 0;
color: var(--ov-error-color);
font-size: 15px;
box-shadow: 0 2px 8px 0 rgba(244, 67, 54, 0.04);
.error-icon {
font-size: 28px;
color: var(--ov-error-color);
flex-shrink: 0;
margin-top: 2px;
width: 100%;
height: 100%;
}
.error-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.error-title {
font-weight: 600;
font-size: 15px;
margin-bottom: 2px;
}
.error-message {
font-size: 14px;
opacity: 0.85;
}
}
.recording-date {
font-size: 12px !important;
font-style: italic;
.disable-recording-btn {
background-color: var(--ov-secondary-action-color) !important;
color: var(--ov-text-surface-color) !important;
cursor: not-allowed !important;
}
// Enhanced empty state
.empty-state {
text-align: center;
padding: 32px 16px;
color: var(--ov-text-surface-color);
}
.empty-state-icon {
margin-bottom: 16px;
mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
color: var(--ov-accent-action-color);
opacity: 0.6;
}
}
.empty-state-title {
font-size: 18px;
font-weight: 500;
margin: 0 0 8px 0;
color: var(--ov-text-surface-color);
}
.empty-state-subtitle {
font-size: 14px;
margin: 0;
opacity: 0.7;
line-height: 1.4;
}
.not-allowed-message {
@ -96,25 +532,44 @@
}
.recording-action-buttons {
margin-top: 20px;
margin-bottom: 20px;
margin: 5px 0px;
}
#start-recording-btn {
width: 100%;
background-color: var(--ov-primary-action-color);
color: var(--ov-secondary-action-color);
border-radius: var(--ov-surface-radius);
}
#view-recordings-btn {
width: 100%;
background-color: var(--ov-accent-action-color);
color: var(--ov-secondary-action-color);
border-radius: var(--ov-surface-radius);
margin-bottom: 10px;
mat-icon {
margin-right: 8px;
}
}
.start-recording-button-container {
width: 100%;
display: inline-block;
}
#stop-recording-btn {
width: 100%;
background-color: var(--ov-error-color);
color: var(--ov-secondary-action-color);
border-radius: var(--ov-surface-radius);
}
#reset-recording-status-btn {
width: 100%;
background-color: var(--ov-secondary-action-color);
background-color: var(--ov-accent-action-color);
border-radius: var(--ov-surface-radius);
}
.recording-item {

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, OnDestroy, Output } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import {
RecordingDeleteRequestedEvent,
RecordingDownloadClickedEvent,
@ -16,6 +16,7 @@ import { RecordingService } from '../../../../services/recording/recording.servi
import { OpenViduService } from '../../../../services/openvidu/openvidu.service';
import { ILogger } from '../../../../models/logger.model';
import { LoggerService } from '../../../../services/logger/logger.service';
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
/**
* The **RecordingActivityComponent** is the component that allows showing the recording activity.
@ -31,7 +32,7 @@ import { LoggerService } from '../../../../services/logger/logger.service';
// TODO: Allow to add more than one recording type
// TODO: Allow to choose where the recording is stored (s3, google cloud, etc)
// TODO: Allow to choose the layout of the recording
export class RecordingActivityComponent implements OnInit {
export class RecordingActivityComponent implements OnInit, OnDestroy {
/**
* @internal
*/
@ -67,6 +68,20 @@ export class RecordingActivityComponent implements OnInit {
*/
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
/**
* @internal
* Provides event notifications that fire when view recordings button has been clicked.
* This event is triggered when the user wants to view all recordings in an external page.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* @internal
* This event is fired when the user clicks on the view recording button.
* It provides the recording ID as event data.
*/
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* @internal
*/
@ -99,12 +114,53 @@ export class RecordingActivityComponent implements OnInit {
*/
recordingError: any;
/**
* @internal
*/
hasRoomTracksPublished: boolean = false;
/**
* @internal
*/
mouseHovering: boolean = false;
/**
* @internal
*/
isReadOnlyMode: boolean = false;
/**
* @internal
*/
viewButtonText: string = 'PANEL.RECORDING.VIEW';
/**
* @internal
*/
showStartStopRecordingButton: boolean = true;
/**
* @internal
*/
showViewRecordingsButton: boolean = false;
/**
* @internal
*/
showRecordingList: boolean = true; // Controls visibility of the recording list in the panel
/**
* @internal
*/
showControls: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean } = {
play: true,
download: true,
delete: true,
externalView: false
};
private log: ILogger;
private recordingStatusSubscription: Subscription;
private destroy$ = new Subject<void>();
/**
* @internal
@ -115,7 +171,8 @@ export class RecordingActivityComponent implements OnInit {
private actionService: ActionService,
private openviduService: OpenViduService,
private cd: ChangeDetectorRef,
private loggerSrv: LoggerService
private loggerSrv: LoggerService,
private libService: OpenViduComponentsConfigService
) {
this.log = this.loggerSrv.get('RecordingActivityComponent');
}
@ -125,13 +182,23 @@ export class RecordingActivityComponent implements OnInit {
*/
ngOnInit(): void {
this.subscribeToRecordingStatus();
this.subscribeToTracksChanges();
this.subscribeToConfigChanges();
}
/**
* @internal
*/
ngOnDestroy() {
if (this.recordingStatusSubscription) this.recordingStatusSubscription.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
/**
* @internal
*/
trackByRecordingId(index: number, recording: RecordingInfo): string | undefined {
return recording.id;
}
/**
@ -226,11 +293,104 @@ export class RecordingActivityComponent implements OnInit {
this.recordingService.playRecording(recording);
}
/**
* @internal
*/
viewRecording(recording: RecordingInfo) {
// This method can be overridden or emit a custom event for navigation
// For now, it uses the same behavior as play, but can be customized
if (!recording.filename) {
this.log.e('Error viewing recording. Recording filename is undefined');
return;
}
const payload: RecordingPlayClickedEvent = {
roomName: this.openviduService.getRoomName(),
recordingId: recording.id
};
this.onRecordingPlayClicked.emit(payload);
// You can customize this to navigate to a different page instead
this.recordingService.playRecording(recording);
}
/**
* @internal
*/
viewAllRecordings() {
this.onViewRecordingsClicked.emit();
}
/**
* @internal
* Format duration in seconds to a readable format (e.g., "2m 30s")
*/
formatDuration(seconds: number): string {
if (!seconds || seconds < 0) return '0s';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else if (minutes > 0) {
return `${minutes}m ${remainingSeconds}s`;
} else {
return `${remainingSeconds}s`;
}
}
/**
* @internal
* Format file size in bytes to a readable format (e.g., "2.5 MB")
*/
formatFileSize(bytes: number): string {
if (!bytes || bytes < 0) return '0 B';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const size = bytes / Math.pow(1024, i);
return `${size.toFixed(1)} ${sizes[i]}`;
}
private subscribeToConfigChanges() {
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => {
this.isReadOnlyMode = readOnly;
this.cd.markForCheck();
});
this.libService.recordingActivityShowControls$
.pipe(takeUntil(this.destroy$))
.subscribe((controls: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean }) => {
this.showControls = controls;
this.cd.markForCheck();
});
this.libService.recordingActivityStartStopRecordingButton$.pipe(takeUntil(this.destroy$)).subscribe((show: boolean) => {
this.showStartStopRecordingButton = show;
this.cd.markForCheck();
});
this.libService.recordingActivityViewRecordingsButton$.pipe(takeUntil(this.destroy$)).subscribe((show: boolean) => {
this.showViewRecordingsButton = show;
this.cd.markForCheck();
});
this.libService.recordingActivityShowRecordingsList$.pipe(takeUntil(this.destroy$)).subscribe((show: boolean) => {
this.showRecordingList = show;
this.cd.markForCheck();
});
}
private subscribeToRecordingStatus() {
this.recordingStatusSubscription = this.recordingService.recordingStatusObs.subscribe((event: RecordingStatusInfo) => {
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, recordingList, error } = event;
this.recordingStatus = status;
this.recordingList = recordingList;
if (this.showRecordingList) {
this.recordingList = recordingList;
} else {
this.recordingList = recordingList.filter((rec) => rec.status === RecordingStatus.STARTED);
}
this.recordingError = error;
this.recordingAlive = this.recordingStatus === RecordingStatus.STARTED;
if (this.recordingStatus !== RecordingStatus.FAILED) {
@ -239,4 +399,24 @@ export class RecordingActivityComponent implements OnInit {
this.cd.markForCheck();
});
}
private subscribeToTracksChanges() {
this.hasRoomTracksPublished = this.openviduService.hasRoomTracksPublished();
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe(() => {
const newValue = this.openviduService.hasRoomTracksPublished();
if (this.hasRoomTracksPublished !== newValue) {
this.hasRoomTracksPublished = newValue;
this.cd.markForCheck();
}
});
this.participantService.remoteParticipants$.pipe(takeUntil(this.destroy$)).subscribe(() => {
const newValue = this.openviduService.hasRoomTracksPublished();
if (this.hasRoomTracksPublished !== newValue) {
this.hasRoomTracksPublished = newValue;
this.cd.markForCheck();
}
});
}
}

View File

@ -1,5 +1,5 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import { ChatMessage } from '../../../models/chat.model';
import { PanelType } from '../../../models/panel.model';
import { ChatService } from '../../../services/chat/chat.service';
@ -34,7 +34,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
*/
messageList: ChatMessage[] = [];
private chatMessageSubscription: Subscription;
private destroy$ = new Subject<void>();
/**
* @ignore
@ -66,7 +66,8 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
* @ignore
*/
ngOnDestroy(): void {
if (this.chatMessageSubscription) this.chatMessageSubscription.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
/**
@ -109,7 +110,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
}
private subscribeToMessages() {
this.chatMessageSubscription = this.chatService.messagesObs.subscribe((messages: ChatMessage[]) => {
this.chatService.messagesObs.pipe(takeUntil(this.destroy$)).subscribe((messages: ChatMessage[]) => {
this.messageList = messages;
if (this.panelService.isChatPanelOpened()) {
this.scrollToBottom();

View File

@ -8,7 +8,7 @@ import {
Output,
TemplateRef
} from '@angular/core';
import { skip, Subscription } from 'rxjs';
import { skip, Subject, takeUntil } from 'rxjs';
import {
ActivitiesPanelDirective,
AdditionalPanelsDirective,
@ -25,6 +25,7 @@ import {
} from '../../models/panel.model';
import { PanelService } from '../../services/panel/panel.service';
import { BackgroundEffect } from '../../models/background-effect.model';
import { TemplateManagerService, PanelTemplateConfiguration } from '../../services/template/template-manager.service';
/**
*
@ -75,42 +76,20 @@ export class PanelComponent implements OnInit {
*/
@ContentChild(ParticipantsPanelDirective)
set externalParticipantPanel(externalParticipantsPanel: ParticipantsPanelDirective) {
// This directive will has value only when PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
// is inside of the PANEL component tagged with '*ovPanel'
this._externalParticipantPanel = externalParticipantsPanel;
if (externalParticipantsPanel) {
this.participantsPanelTemplate = externalParticipantsPanel.template;
this.updateTemplatesAndMarkForCheck();
}
}
// TODO: backgroundEffectsPanel does not provides customization
// @ContentChild(BackgroundEffectsPanelDirective)
// set externalBackgroundEffectsPanel(externalBackgroundEffectsPanel: BackgroundEffectsPanelDirective) {
// This directive will has value only when BACKGROUND EFFECTS PANEL component tagged with '*ovBackgroundEffectsPanel'
// is inside of the PANEL component tagged with '*ovPanel'
// if (externalBackgroundEffectsPanel) {
// this.backgroundEffectsPanelTemplate = externalBackgroundEffectsPanel.template;
// }
// }
// TODO: settingsPanel does not provides customization
// @ContentChild(SettingsPanelDirective)
// set externalSettingsPanel(externalSettingsPanel: SettingsPanelDirective) {
// This directive will has value only when SETTINGS PANEL component tagged with '*ovSettingsPanel'
// is inside of the PANEL component tagged with '*ovPanel'
// if (externalSettingsPanel) {
// this.settingsPanelTemplate = externalSettingsPanel.template;
// }
// }
/**
* @ignore
*/
@ContentChild(ActivitiesPanelDirective)
set externalActivitiesPanel(externalActivitiesPanel: ActivitiesPanelDirective) {
// This directive will has value only when ACTIVITIES PANEL component tagged with '*ovActivitiesPanel'
// is inside of the PANEL component tagged with '*ovPanel'
this._externalActivitiesPanel = externalActivitiesPanel;
if (externalActivitiesPanel) {
this.activitiesPanelTemplate = externalActivitiesPanel.template;
this.updateTemplatesAndMarkForCheck();
}
}
@ -119,10 +98,9 @@ export class PanelComponent implements OnInit {
*/
@ContentChild(ChatPanelDirective)
set externalChatPanel(externalChatPanel: ChatPanelDirective) {
// This directive will has value only when CHAT PANEL component tagged with '*ovChatPanel'
// is inside of the PANEL component tagged with '*ovPanel'
this._externalChatPanel = externalChatPanel;
if (externalChatPanel) {
this.chatPanelTemplate = externalChatPanel.template;
this.updateTemplatesAndMarkForCheck();
}
}
@ -131,10 +109,9 @@ export class PanelComponent implements OnInit {
*/
@ContentChild(AdditionalPanelsDirective)
set externalAdditionalPanels(externalAdditionalPanels: AdditionalPanelsDirective) {
// This directive will has value only when ADDITIONAL PANELS component tagged with '*ovPanelAdditionalPanels'
// is inside of the PANEL component tagged with '*ovPanel'
this._externalAdditionalPanels = externalAdditionalPanels;
if (externalAdditionalPanels) {
this.additionalPanelsTemplate = externalAdditionalPanels.template;
this.updateTemplatesAndMarkForCheck();
}
}
@ -195,7 +172,20 @@ export class PanelComponent implements OnInit {
* @internal
*/
isExternalPanelOpened: boolean;
private panelSubscription: Subscription;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: PanelTemplateConfiguration = {};
// Store directive references for template setup
private _externalParticipantPanel?: ParticipantsPanelDirective;
private _externalChatPanel?: ChatPanelDirective;
private _externalActivitiesPanel?: ActivitiesPanelDirective;
private _externalAdditionalPanels?: AdditionalPanelsDirective;
private destroy$ = new Subject<void>();
private panelEmitersHandler: Map<
PanelType,
@ -207,30 +197,78 @@ export class PanelComponent implements OnInit {
*/
constructor(
private panelService: PanelService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) {}
/**
* @ignore
*/
ngOnInit(): void {
this.setupTemplates();
this.subscribeToPanelToggling();
this.panelEmitersHandler.set(PanelType.CHAT, this.onChatPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.PARTICIPANTS, this.onParticipantsPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.SETTINGS, this.onSettingsPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.ACTIVITIES, this.onActivitiesPanelStatusChanged);
}
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupPanelTemplates(
this._externalParticipantPanel,
this._externalChatPanel,
this._externalActivitiesPanel,
this._externalAdditionalPanels
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
if (this.templateConfig.participantsPanelTemplate) {
this.participantsPanelTemplate = this.templateConfig.participantsPanelTemplate;
}
if (this.templateConfig.chatPanelTemplate) {
this.chatPanelTemplate = this.templateConfig.chatPanelTemplate;
}
if (this.templateConfig.activitiesPanelTemplate) {
this.activitiesPanelTemplate = this.templateConfig.activitiesPanelTemplate;
}
if (this.templateConfig.additionalPanelsTemplate) {
this.additionalPanelsTemplate = this.templateConfig.additionalPanelsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
/**
* @ignore
*/
ngOnDestroy() {
this.isChatPanelOpened = false;
this.isParticipantsPanelOpened = false;
if (this.panelSubscription) this.panelSubscription.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelStatusObs.pipe(skip(1)).subscribe((ev: PanelStatusInfo) => {
this.panelService.panelStatusObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
this.isChatPanelOpened = ev.isOpened && ev.panelType === PanelType.CHAT;
this.isParticipantsPanelOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isBackgroundEffectsPanelOpened = ev.isOpened && ev.panelType === PanelType.BACKGROUND_EFFECTS;

View File

@ -1,33 +1,71 @@
<mat-list>
<mat-list-item>
<div matListItemIcon class="participant-avatar" [style.background-color]="_participant.colorProfile">
<mat-icon>person</mat-icon>
</div>
<h3 matListItemTitle class="participant-name">{{ _participant.name }}
<span *ngIf="_participant.isLocal"> ({{ 'PANEL.PARTICIPANTS.YOU' | translate }})</span>
</h3>
<p matListItemLine class="participant-subtitle">{{ _participant | tracksPublishedTypes }}</p>
<!-- <p matListItemLine>
<span class="participant-subtitle"></span>
</p> -->
<div class="participant-action-buttons" matListItemMeta>
<button
mat-icon-button
id="mute-btn"
*ngIf="!_participant.isLocal && showMuteButton"
[class.warn-btn]="_participant.isMutedForcibly"
(click)="toggleMuteForcibly()"
[disableRipple]="true"
<!-- Main participant container with improved structure -->
<div class="participant-container" [attr.data-participant-id]="_participant?.sid">
<!-- Avatar section with dynamic color -->
<div
class="participant-avatar"
[style.background-color]="_participant?.colorProfile"
[attr.aria-label]="'Avatar for ' + participantDisplayName"
>
<mat-icon *ngIf="!_participant.isMutedForcibly">volume_up</mat-icon>
<mat-icon *ngIf="_participant.isMutedForcibly">volume_off</mat-icon>
</button>
<mat-icon>person</mat-icon>
</div>
<!-- External item elements -->
<ng-container *ngIf="participantPanelItemElementsTemplate">
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
</ng-container>
<!-- Content section with name and status -->
<div class="participant-content">
<div class="participant-name">
{{ participantDisplayName }}
<span *ngIf="isLocalParticipant" class="local-indicator">
{{ 'PANEL.PARTICIPANTS.YOU' | translate }}
</span>
<!-- Participant badges -->
<div class="participant-badges">
<ng-container *ngTemplateOutlet="participantBadgeTemplate"></ng-container>
</div>
</div>
<div class="participant-subtitle">
<span class="status-indicator">
{{ _participant | tracksPublishedTypes }}
</span>
<!-- Additional status indicators -->
<span *ngIf="_participant?.isMutedForcibly" class="status-indicator">
<mat-icon>volume_off</mat-icon>
{{ 'PANEL.PARTICIPANTS.MUTED' | translate }}
</span>
</div>
</div>
<!-- Action buttons section -->
<div class="participant-action-buttons">
<!-- Mute/Unmute button for remote participants -->
<button
mat-icon-button
id="mute-btn"
*ngIf="!isLocalParticipant && showMuteButton"
[class.warn-btn]="_participant?.isMutedForcibly"
(click)="toggleMuteForcibly()"
[disabled]="!_participant"
[disableRipple]="true"
[attr.aria-label]="
_participant?.isMutedForcibly
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
"
[matTooltip]="
_participant?.isMutedForcibly ? ('PANEL.PARTICIPANTS.UNMUTE' | translate) : ('PANEL.PARTICIPANTS.MUTE' | translate)
"
>
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
</button>
<!-- 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

@ -1,68 +1,443 @@
:host {
// Container for the participant item
.participant-container {
position: relative;
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: var(--ov-surface-radius, 8px);
background-color: var(--ov-surface-background, #ffffff);
border-bottom: 1px solid var(--ov-surface-border, #e0e0e0);
transition: all 0.2s ease-in-out;
min-height: 64px;
// &:hover {
// background-color: var(--ov-surface-hover, #f5f5f5);
// transform: translateY(-1px);
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// }
&:last-child {
border-bottom: none;
}
// Loading state
&.loading {
opacity: 0.7;
pointer-events: none;
&::after {
content: '';
position: absolute;
top: 50%;
right: 16px;
width: 16px;
height: 16px;
border: 2px solid var(--ov-primary-color, #1976d2);
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
}
// Focus state for keyboard navigation
&:focus-within {
outline: 2px solid var(--ov-primary-color, #1976d2);
outline-offset: 2px;
}
}
// Avatar styling with improved design
.participant-avatar {
display: inherit;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: var(--ov-surface-radius);
margin: auto !important;
padding: 10px;
color: #000000;
margin-right: 12px;
padding: 0;
color: #ffffff;
font-weight: 500;
flex-shrink: 0;
position: relative;
overflow: hidden;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
z-index: 1;
}
}
.participant-subtitle {
font-style: italic;
font-size: 11px !important;
margin: 0;
color: var(--ov-text-surface-color);
// Main content area
.participant-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0; // Allows text truncation
margin-right: 8px;
}
// Participant name styling
.participant-name {
font-weight: bold !important;
color: var(--ov-text-surface-color);
font-weight: 600 !important;
font-size: 14px;
line-height: 1.2;
color: var(--ov-text-primary, #212121);
margin: 0 0 4px 0;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// Local participant indicator
.local-indicator {
font-size: 10px;
font-weight: 600;
color: var(--ov-primary-color, #1976d2);
background-color: var(--ov-primary-light, #e3f2fd);
padding: 4px 8px;
border-radius: var(--ov-surface-radius);
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
border: 1px solid var(--ov-primary-color, #1976d2);
}
}
// Subtitle styling
.participant-subtitle {
font-style: normal;
font-size: 12px !important;
font-weight: 400;
margin: 0;
color: var(--ov-text-secondary, #757575);
line-height: 1.3;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// Status indicators
.status-indicator {
display: inline-flex;
align-items: center;
gap: 3px;
mat-icon {
font-size: 12px;
width: 12px;
height: 12px;
}
// Different colors for different statuses
&.camera-on {
color: var(--ov-success-color, #4caf50);
}
&.camera-off {
color: var(--ov-warning-color, #ff9800);
}
&.microphone-muted {
color: var(--ov-error-color, #d32f2f);
}
}
}
// Action buttons container
.participant-action-buttons {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
margin-left: auto;
}
::ng-deep .participant-action-buttons > *:not(#mute-btn) {
display: contents;
// Mute button styling
#mute-btn {
width: 32px;
height: 32px;
border-radius: 50%;
color: var(--ov-text-secondary, #757575);
background-color: transparent;
transition: all 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
position: relative;
&:hover {
background-color: var(--ov-surface-hover, #f5f5f5);
color: var(--ov-text-primary, #212121);
transform: scale(1.1);
}
&:focus {
outline: 2px solid var(--ov-primary-color, #1976d2);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
pointer-events: none;
}
&.warn-btn {
color: var(--ov-error-color, #d32f2f);
background-color: var(--ov-error-light, #ffebee);
&:hover {
background-color: var(--ov-error-color, #d32f2f);
color: #ffffff;
}
// Pulsing animation for muted state
animation: pulse 2s infinite;
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
::ng-deep .participant-action-buttons > *:not(#mute-btn) > * {
margin: auto;
// Participant badges container
.participant-badges {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
// Badge styling
::ng-deep .badge {
// Badge variants
&.moderator {
color: var(--ov-warning-color, #f57c00);
}
&.speaker {
color: var(--ov-primary-color, #1976d2);
}
&.host {
color: var(--ov-success-color, #4caf50);
}
}
}
// After local participant content area
.after-local-content {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--ov-surface-border, #e0e0e0);
animation: fadeIn 0.3s ease-in-out;
background-color: var(--ov-surface-alt, #fafafa);
border-radius: var(--ov-surface-radius, 8px);
padding: 12px;
}
// External item elements styling
.external-elements {
display: flex;
align-items: center;
gap: 4px;
// Custom styling for external buttons
::ng-deep button {
transition: all 0.2s ease-in-out;
&:hover {
transform: scale(1.05);
}
}
}
// Material Design overrides for better integration
mat-list {
padding: 0;
}
::ng-deep .mat-mdc-list-item {
height: max-content !important;
padding-bottom: 10px !important;
}
::ng-deep .mat-mdc-list-item:hover {
color: #000000 !important;
}
::ng-deep .mat-mdc-list-item:hover .mat-mdc-list-item-title {
color: var(--ov-text-surface-color) !important;
}
mat-list {
padding: 3px;
height: auto !important;
padding: 0 !important;
min-height: auto !important;
border-radius: var(--ov-surface-radius, 8px);
}
::ng-deep .mdc-list-item__content {
padding-left: 10px !important;
align-self: center !important;
padding: 0 !important;
align-self: stretch !important;
width: 100%;
}
::ng-deep .mat-mdc-list-base {
--mdc-list-list-item-hover-label-text-color: unset;
--mdc-list-list-item-hover-leading-icon-color: unset;
padding: 0;
}
#mute-btn {
border-radius: 50%;
color: var(--ov-text-surface-color);
::ng-deep .mat-mdc-list-item:hover {
background-color: transparent !important;
}
.warn-btn {
/* background-color: var(--ov-error-color) !important; */
color: var(--ov-error-color);
// Animations
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
// Responsive design
@media (max-width: 768px) {
.participant-container {
padding: 10px 12px;
min-height: 56px;
}
.participant-avatar {
width: 36px;
height: 36px;
margin-right: 10px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
&::after {
width: 10px;
height: 10px;
bottom: 1px;
right: 1px;
}
}
.participant-name {
font-size: 13px;
.local-indicator {
font-size: 9px;
padding: 2px 6px;
}
}
.participant-subtitle {
font-size: 11px !important;
}
#mute-btn {
width: 28px;
height: 28px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
}
.after-local-content {
margin-top: 10px;
padding-top: 10px;
padding: 10px;
}
}
// High contrast mode support
@media (prefers-contrast: high) {
.participant-container {
border: 2px solid var(--ov-text-primary, #212121);
}
.participant-avatar {
border: 2px solid var(--ov-surface-background, #ffffff);
}
.local-indicator {
border-width: 2px;
}
}
// Reduced motion support
@media (prefers-reduced-motion: reduce) {
.participant-container,
.participant-avatar,
#mute-btn,
.after-local-content,
.external-elements ::ng-deep button {
transition: none;
animation: none;
}
.participant-container:hover {
transform: none;
}
.participant-avatar:hover,
#mute-btn:hover,
.external-elements ::ng-deep button:hover {
transform: none;
}
#mute-btn.warn-btn {
animation: none;
}
}
// Dark theme support
@media (prefers-color-scheme: dark) {
.participant-container {
background-color: var(--ov-surface-background, #424242);
border-bottom-color: var(--ov-surface-border, #616161);
&:hover {
background-color: var(--ov-surface-hover, #484848);
}
}
.participant-name {
color: var(--ov-text-primary, #ffffff);
}
.participant-subtitle {
color: var(--ov-text-secondary, #cccccc);
}
.after-local-content {
background-color: var(--ov-surface-alt, #373737);
}
}
}

View File

@ -1,16 +1,17 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { ParticipantPanelItemElementsDirective } from '../../../../directives/template/openvidu-components-angular.directive';
import { ParticipantPanelParticipantBadgeDirective } from '../../../../directives/template/internals.directive';
import { ParticipantModel } from '../../../../models/participant.model';
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
import { ParticipantService } from '../../../../services/participant/participant.service';
import { TemplateManagerService, ParticipantPanelItemTemplateConfiguration } from '../../../../services/template/template-manager.service';
/**
*
* The **ParticipantPanelItemComponent** is hosted inside of the {@link ParticipantsPanelComponent}.
* It is in charge of displaying the participants information inside of the ParticipansPanelComponent.
* It displays participant information with enhanced UI/UX, including support for custom content
* injection through structural directives.
*/
@Component({
selector: 'ov-participant-panel-item',
templateUrl: './participant-panel-item.component.html',
@ -35,40 +36,69 @@ export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
*/
@ContentChild(ParticipantPanelItemElementsDirective)
set externalItemElements(externalItemElements: ParticipantPanelItemElementsDirective) {
// This directive will has value only when ITEM ELEMENTS component tagget with '*ovParticipantPanelItemElements' directive
// is inside of the P PANEL ITEM component tagged with '*ovParticipantPanelItem' directive
this._externalItemElements = externalItemElements;
if (externalItemElements) {
this.participantPanelItemElementsTemplate = externalItemElements.template;
this.updateTemplatesAndMarkForCheck();
}
}
/**
* The participant to be displayed
* @ignore
*/
@ContentChild(ParticipantPanelParticipantBadgeDirective)
set externalParticipantBadge(participantBadge: ParticipantPanelParticipantBadgeDirective) {
this._externalParticipantBadge = participantBadge;
if (participantBadge) {
this.updateTemplatesAndMarkForCheck();
}
}
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: ParticipantPanelItemTemplateConfiguration = {};
// Store directive references for template setup
private _externalItemElements?: ParticipantPanelItemElementsDirective;
private _externalParticipantBadge?: ParticipantPanelParticipantBadgeDirective;
/**
* The participant to be displayed
*/
@Input()
set participant(participant: ParticipantModel) {
this._participant = participant;
this.cd.markForCheck();
}
/**
* @ignore
* @internal
* Current participant being displayed
*/
_participant: ParticipantModel;
/**
* Whether to show the mute button for remote participants
*/
@Input()
muteButton: boolean = true;
/**
* @ignore
*/
constructor(
private libService: OpenViduComponentsConfigService,
private participantService: ParticipantService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) {}
/**
* @ignore
*/
ngOnInit(): void {
this.setupTemplates();
this.subscribeToParticipantPanelItemDirectives();
}
@ -80,14 +110,72 @@ export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
}
/**
* @ignore
* Toggles the mute state of a remote participant
*/
toggleMuteForcibly() {
if (this._participant) {
if (this._participant && !this._participant.isLocal) {
this.participantService.setRemoteMutedForcibly(this._participant.sid, !this._participant.isMutedForcibly);
}
}
/**
* Gets the template for local participant badge
*/
get participantBadgeTemplate(): TemplateRef<any> | undefined {
return this._externalParticipantBadge?.template;
}
/**
* Checks if the current participant is the local participant
*/
get isLocalParticipant(): boolean {
return this._participant?.isLocal || false;
}
/**
* Gets the participant's display name
*/
get participantDisplayName(): string {
return this._participant?.name || '';
}
/**
* Checks if external elements are available
*/
get hasExternalElements(): boolean {
return !!this.participantPanelItemElementsTemplate;
}
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupParticipantPanelItemTemplates(this._externalItemElements);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
if (this.templateConfig.participantPanelItemElementsTemplate) {
this.participantPanelItemElementsTemplate = this.templateConfig.participantPanelItemElementsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
private subscribeToParticipantPanelItemDirectives() {
this.muteButtonSub = this.libService.participantItemMuteButton$.subscribe((value: boolean) => {
this.showMuteButton = value;

View File

@ -7,14 +7,13 @@
</div>
<div class="scrollable">
<div class="local-participant-container" *ngIf="localParticipant">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: localParticipant }"></ng-container>
<mat-divider *ngIf="true"></mat-divider>
</div>
<ng-container *ngTemplateOutlet="participantPanelAfterLocalParticipantTemplate"></ng-container>
<div class="remote-participants-container" id="remote-participants-container" *ngIf="remoteParticipants.length > 0">
<div *ngFor="let participant of this.remoteParticipants" id="remote-participant-item">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: participant }"></ng-container>
</div>

View File

@ -13,8 +13,10 @@ import {
import { ParticipantService } from '../../../../services/participant/participant.service';
import { PanelService } from '../../../../services/panel/panel.service';
import { ParticipantPanelItemDirective } from '../../../../directives/template/openvidu-components-angular.directive';
import { Subscription } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import { ParticipantModel } from '../../../../models/participant.model';
import { TemplateManagerService, ParticipantsPanelTemplateConfiguration } from '../../../../services/template/template-manager.service';
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
/**
* The **ParticipantsPanelComponent** is hosted inside of the {@link PanelComponent}.
@ -48,20 +50,33 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
*/
@ContentChild('participantPanelItem', { read: TemplateRef }) participantPanelItemTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ContentChild('participantPanelAfterLocalParticipant', { read: TemplateRef })
participantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ContentChild(ParticipantPanelItemDirective)
set externalParticipantPanelItem(externalParticipantPanelItem: ParticipantPanelItemDirective) {
// This directive will has value only when PARTICIPANT PANEL ITEM component tagged with '*ovParticipantPanelItem'
// is inside of the PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
this._externalParticipantPanelItem = externalParticipantPanelItem;
if (externalParticipantPanelItem) {
this.participantPanelItemTemplate = externalParticipantPanelItem.template;
this.updateTemplatesAndMarkForCheck();
}
}
private localParticipantSubs: Subscription;
private remoteParticipantsSubs: Subscription;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: ParticipantsPanelTemplateConfiguration = {};
// Store directive references for template setup
private _externalParticipantPanelItem?: ParticipantPanelItemDirective;
private destroy$ = new Subject<void>();
/**
* @ignore
@ -69,32 +84,26 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
constructor(
private participantService: ParticipantService,
private panelService: PanelService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnInit(): void {
this.localParticipantSubs = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
if (p) {
this.localParticipant = p;
this.cd.markForCheck();
}
});
this.setupTemplates();
this.remoteParticipantsSubs = this.participantService.remoteParticipants$.subscribe((p: ParticipantModel[]) => {
this.remoteParticipants = p;
this.cd.markForCheck();
});
this.subscribeToParticipantsChanges();
}
/**
* @ignore
*/
ngOnDestroy() {
if (this.localParticipantSubs) this.localParticipantSubs.unsubscribe();
if (this.remoteParticipantsSubs) this.remoteParticipantsSubs.unsubscribe;
this.destroy$.next();
this.destroy$.complete();
}
/**
@ -109,6 +118,57 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
}
}
private subscribeToParticipantsChanges() {
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p: ParticipantModel | undefined) => {
if (p) {
this.localParticipant = p;
this.cd.markForCheck();
}
});
this.participantService.remoteParticipants$.pipe(takeUntil(this.destroy$)).subscribe((p: ParticipantModel[]) => {
this.remoteParticipants = p;
this.cd.markForCheck();
});
}
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupParticipantsPanelTemplates(
this._externalParticipantPanelItem,
this.defaultParticipantPanelItemTemplate
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
if (this.templateConfig.participantPanelItemTemplate) {
this.participantPanelItemTemplate = this.templateConfig.participantPanelItemTemplate;
}
if (this.templateConfig.participantPanelAfterLocalParticipantTemplate) {
this.participantPanelAfterLocalParticipantTemplate = this.templateConfig.participantPanelAfterLocalParticipantTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
/**
* @ignore
*/

View File

@ -22,7 +22,7 @@
[value]="settingsOptions.GENERAL"
>
<mat-icon matListItemIcon>manage_accounts</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showCameraButton"
@ -32,7 +32,7 @@
[value]="settingsOptions.VIDEO"
>
<mat-icon matListItemIcon>videocam</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showMicrophoneButton"
@ -42,7 +42,7 @@
[value]="settingsOptions.AUDIO"
>
<mat-icon matListItemIcon>mic</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
</mat-list-option>
<!-- <mat-list-option
*ngIf="showCaptions"

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model';
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
import { PanelService } from '../../../services/panel/panel.service';
@ -27,11 +27,8 @@ export class SettingsPanelComponent implements OnInit {
showCameraButton: boolean = true;
showMicrophoneButton: boolean = true;
showCaptions: boolean = true;
panelSubscription: Subscription;
isMobile: boolean = false;
private cameraButtonSub: Subscription;
private microphoneButtonSub: Subscription;
private captionsSubs: Subscription;
private destroy$ = new Subject<void>();
constructor(
private panelService: PanelService,
private platformService: PlatformService,
@ -44,10 +41,8 @@ export class SettingsPanelComponent implements OnInit {
}
ngOnDestroy() {
if (this.panelSubscription) this.panelSubscription.unsubscribe();
if (this.cameraButtonSub) this.cameraButtonSub.unsubscribe();
if (this.microphoneButtonSub) this.microphoneButtonSub.unsubscribe();
if (this.captionsSubs) this.captionsSubs.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
close() {
@ -58,13 +53,13 @@ export class SettingsPanelComponent implements OnInit {
}
private subscribeToDirectives() {
this.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => (this.showCameraButton = value));
this.microphoneButtonSub = this.libService.microphoneButton$.subscribe((value: boolean) => (this.showMicrophoneButton = value));
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => (this.showCaptions = value));
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCameraButton = value));
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showMicrophoneButton = value));
this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCaptions = value));
}
private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
if (ev.panelType === PanelType.SETTINGS && !!ev.subOptionType) {
this.selectedOption = ev.subOptionType as PanelSettingsOptions;
}

View File

@ -54,7 +54,7 @@
</div>
<div class="join-btn-container">
<button mat-flat-button (click)="joinSession()" id="join-button">
<button mat-flat-button (click)="join()" id="join-button">
{{ 'PREJOIN.JOIN' | translate }}
</button>
</div>

View File

@ -9,7 +9,7 @@ import {
OnInit,
Output
} from '@angular/core';
import { Subscription } from 'rxjs';
import { filter, Subject, takeUntil, tap } from 'rxjs';
import { ILogger } from '../../models/logger.model';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
@ -19,7 +19,6 @@ import { TranslateService } from '../../services/translate/translate.service';
import { LocalTrack } from 'livekit-client';
import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model';
import { StorageService } from '../../services/storage/storage.service';
/**
* @internal
@ -61,11 +60,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
audioTrack: LocalTrack | undefined;
private tracks: LocalTrack[];
private log: ILogger;
private cameraButtonSub: Subscription;
private microphoneButtonSub: Subscription;
private minimalSub: Subscription;
private displayLogoSub: Subscription;
private displayParticipantNameSub: Subscription;
private destroy$ = new Subject<void>();
private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
@HostListener('window:resize')
@ -78,7 +73,6 @@ export class PreJoinComponent implements OnInit, OnDestroy {
private libService: OpenViduComponentsConfigService,
private cdkSrv: CdkOverlayService,
private openviduService: OpenViduService,
private storageService: StorageService,
private translateService: TranslateService,
private changeDetector: ChangeDetectorRef
) {
@ -99,15 +93,12 @@ export class PreJoinComponent implements OnInit, OnDestroy {
// }
async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body');
if (this.minimalSub) this.minimalSub.unsubscribe();
if (this.cameraButtonSub) this.cameraButtonSub.unsubscribe();
if (this.microphoneButtonSub) this.microphoneButtonSub.unsubscribe();
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
if (this.displayParticipantNameSub) this.displayParticipantNameSub.unsubscribe();
if (this.shouldRemoveTracksWhenComponentIsDestroyed) {
this.tracks.forEach((track) => {
this.tracks?.forEach((track) => {
track.stop();
});
}
@ -130,7 +121,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.cdkSrv.setSelector('#prejoin-container');
}
joinSession() {
join() {
if (this.showParticipantName && !this.participantName) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return;
@ -140,9 +131,22 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
// Assign participant name to the observable if it is defined
if(this.participantName) this.libService.setParticipantName(this.participantName);
if (this.participantName) {
this.libService.updateGeneralConfig({ participantName: this.participantName });
this.onReadyToJoin.emit();
// Wait for the next tick to ensure the participant name propagates
// through the observable before emitting onReadyToJoin
this.libService.participantName$
.pipe(
takeUntil(this.destroy$),
filter((name) => name === this.participantName),
tap(() => this.onReadyToJoin.emit())
)
.subscribe();
} else {
// No participant name to set, emit immediately
this.onReadyToJoin.emit();
}
}
onParticipantNameChanged(name: string) {
@ -150,33 +154,38 @@ export class PreJoinComponent implements OnInit, OnDestroy {
}
onEnterPressed() {
this.joinSession();
this.join();
}
private subscribeToPrejoinDirectives() {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.isMinimal = value;
this.changeDetector.markForCheck();
});
this.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => {
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value;
this.changeDetector.markForCheck();
});
this.microphoneButtonSub = this.libService.microphoneButton$.subscribe((value: boolean) => {
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showMicrophoneButton = value;
this.changeDetector.markForCheck();
});
this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showLogo = value;
this.changeDetector.markForCheck();
});
this.libService.participantName$.subscribe((value: string) => {
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
if (value) {
this.participantName = value;
this.changeDetector.markForCheck();
}
});
this.displayParticipantNameSub = this.libService.prejoinDisplayParticipantName$.subscribe((value: boolean) => {
this.libService.prejoinDisplayParticipantName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showParticipantName = value;
this.changeDetector.markForCheck();
});

View File

@ -16,7 +16,7 @@ import {
import { ILogger } from '../../models/logger.model';
import { animate, style, transition, trigger } from '@angular/animations';
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav';
import { skip, Subscription } from 'rxjs';
import { skip, Subject, takeUntil } from 'rxjs';
import { SidenavMode } from '../../models/layout.model';
import { PanelStatusInfo, PanelType } from '../../models/panel.model';
import { DataTopic } from '../../models/data-topic.model';
@ -48,6 +48,7 @@ import {
} from 'livekit-client';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { RecordingStatus } from '../../models/recording.model';
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
/**
* @internal
@ -82,7 +83,7 @@ export class SessionComponent implements OnInit, OnDestroy {
/**
* Provides event notifications that fire when participant is disconnected from Room.
* @deprecated Use {@link onParticipantLeft} instead.
* @deprecated Use {@link SessionComponent.onParticipantLeft} instead.
*/
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -103,12 +104,16 @@ export class SessionComponent implements OnInit, OnDestroy {
drawer: MatDrawerContainer;
loading: boolean = true;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: SessionTemplateConfiguration = {};
private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true;
private readonly SIDENAV_WIDTH_LIMIT_MODE = 790;
private menuSubscription: Subscription;
private layoutWidthSubscription: Subscription;
private destroy$ = new Subject<void>();
private updateLayoutInterval: NodeJS.Timeout;
private captionLanguageSubscription: Subscription;
private log: ILogger;
constructor(
@ -125,9 +130,11 @@ export class SessionComponent implements OnInit, OnDestroy {
private translateService: TranslateService,
// private captionService: CaptionService,
private backgroundService: VirtualBackgroundService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) {
this.log = this.loggerSrv.get('SessionComponent');
this.setupTemplates();
}
@HostListener('window:beforeunload')
@ -181,15 +188,39 @@ export class SessionComponent implements OnInit, OnDestroy {
set layoutContainer(container: ElementRef) {
setTimeout(async () => {
if (container) {
// Apply background from storage when layout container is in DOM
await this.backgroundService.applyBackgroundFromStorage();
if (this.libService.showBackgroundEffectsButton()) {
// Apply background from storage when layout container is in DOM only when background effects button is enabled
await this.backgroundService.applyBackgroundFromStorage();
}
}
}, 0);
}
async ngOnInit() {
this.shouldDisconnectRoomWhenComponentIsDestroyed = true;
this.room = this.openviduService.getRoom();
// Check if room is available before proceeding
if (!this.openviduService.isRoomInitialized()) {
this.log.e('Room is not initialized when SessionComponent starts. This indicates a timing issue.');
this.actionService.openDialog(
this.translateService.translate('ERRORS.SESSION'),
'Room is not ready. Please ensure the token is properly configured.'
);
return;
}
// Get room instance
try {
this.room = this.openviduService.getRoom();
this.log.d('Room successfully obtained for SessionComponent');
} catch (error) {
this.log.e('Unexpected error getting room:', error);
this.actionService.openDialog(
this.translateService.translate('ERRORS.SESSION'),
'Failed to get room instance: ' + (error?.message || error)
);
return;
}
// this.subscribeToCaptionLanguage();
this.subcribeToActiveSpeakersChanged();
@ -202,14 +233,15 @@ export class SessionComponent implements OnInit, OnDestroy {
// this.subscribeToParticipantNameChanged();
this.subscribeToDataMessage();
this.subscribeToReconnection();
this.subscribeToVirtualBackground();
if (this.libService.isRecordingEnabled()) {
// if (this.libService.isRecordingEnabled()) {
// this.subscribeToRecordingEvents();
}
// }
if (this.libService.isBroadcastingEnabled()) {
// if (this.libService.isBroadcastingEnabled()) {
// this.subscribeToBroadcastingEvents();
}
// }
try {
await this.participantService.connect();
// Send room created after participant connect for avoiding to send incomplete room payload
@ -228,6 +260,18 @@ export class SessionComponent implements OnInit, OnDestroy {
});
}
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupSessionTemplates(
this.toolbarTemplate,
this.panelTemplate,
this.layoutTemplate
);
}
async ngOnDestroy() {
if (this.shouldDisconnectRoomWhenComponentIsDestroyed) {
await this.disconnectRoom(ParticipantLeftReason.LEAVE);
@ -235,8 +279,8 @@ export class SessionComponent implements OnInit, OnDestroy {
if (this.room) this.room.removeAllListeners();
this.participantService.clear();
// this.room = undefined;
if (this.menuSubscription) this.menuSubscription.unsubscribe();
if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
// if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe();
}
@ -266,7 +310,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.startUpdateLayoutInterval();
});
this.menuSubscription = this.panelService.panelStatusObs.pipe(skip(1)).subscribe((ev: PanelStatusInfo) => {
this.panelService.panelStatusObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
if (this.sideMenu) {
this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS;
@ -285,7 +329,7 @@ export class SessionComponent implements OnInit, OnDestroy {
}
private subscribeToLayoutWidth() {
this.layoutWidthSubscription = this.layoutService.layoutWidthObs.subscribe((width) => {
this.layoutService.layoutWidthObs.pipe(takeUntil(this.destroy$)).subscribe((width) => {
this.sidenavMode = width <= this.SIDENAV_WIDTH_LIMIT_MODE ? SidenavMode.OVER : SidenavMode.SIDE;
});
}
@ -455,7 +499,9 @@ export class SessionComponent implements OnInit, OnDestroy {
case DataTopic.ROOM_STATUS:
const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData;
this.recordingService.setRecordingList(recordingList);
if (this.libService.showRecordingActivityRecordingsList()) {
this.recordingService.setRecordingList(recordingList);
}
if (isRecordingStarted) {
const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED);
this.recordingService.setRecordingStarted(recordingActive);
@ -488,6 +534,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => {
this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
this.actionService.closeConnectionDialog();
const participantLeftEvent: ParticipantLeftEvent = {
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '',
@ -530,7 +577,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.log.d('Participant disconnected', participantLeftEvent);
this.onParticipantLeft.emit(participantLeftEvent);
this.onRoomDisconnected.emit();
if (descriptionErrorKey) {
if (this.libService.getShowDisconnectionDialog() && descriptionErrorKey) {
this.actionService.openDialog(
this.translateService.translate(messageErrorKey),
this.translateService.translate(descriptionErrorKey)
@ -539,6 +586,17 @@ export class SessionComponent implements OnInit, OnDestroy {
});
}
private subscribeToVirtualBackground() {
this.libService.backgroundEffectsButton$.subscribe(async (enable) => {
if (!enable && this.backgroundService.isBackgroundApplied()) {
await this.backgroundService.removeBackground();
if (this.panelService.isBackgroundEffectsPanelOpened()) {
this.panelService.closePanel();
}
}
});
}
private startUpdateLayoutInterval() {
this.updateLayoutInterval = setInterval(() => {
this.layoutService.update();

View File

@ -17,7 +17,7 @@ With the following directives you can modify the default User Interface with the
<!-- start-dynamic-api-directives-content -->
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **displayParticipantName** | `boolean` | [StreamDisplayParticipantNameDirective](../directives/StreamDisplayParticipantNameDirective.html) |
| **displayAudioDetection** | `boolean` | [StreamDisplayAudioDetectionDirective](../directives/StreamDisplayAudioDetectionDirective.html) |
| **displayParticipantName** | `boolean` | [StreamDisplayParticipantNameDirective](../directives/StreamDisplayParticipantNameDirective.html) |
| **videoControls** | `boolean` | [StreamVideoControlsDirective](../directives/StreamVideoControlsDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

@ -1,6 +1,6 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
import { Subscription } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LayoutService } from '../../services/layout/layout.service';
@ -92,10 +92,7 @@ export class StreamComponent implements OnInit, OnDestroy {
}
private _streamContainer: ElementRef;
private minimalSub: Subscription;
private displayParticipantNameSub: Subscription;
private displayAudioDetectionSub: Subscription;
private videoControlsSub: Subscription;
private destroy$ = new Subject<void>();
private readonly HOVER_TIMEOUT = 3000;
/**
@ -113,11 +110,9 @@ export class StreamComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body');
if (this.videoControlsSub) this.videoControlsSub.unsubscribe();
if (this.displayAudioDetectionSub) this.displayAudioDetectionSub.unsubscribe();
if (this.displayParticipantNameSub) this.displayParticipantNameSub.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
}
/**
@ -183,20 +178,31 @@ export class StreamComponent implements OnInit, OnDestroy {
}
private subscribeToStreamDirectives() {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.isMinimal = value;
});
this.displayParticipantNameSub = this.libService.displayParticipantName$.subscribe((value: boolean) => {
this.showParticipantName = value;
// this.cd.markForCheck();
});
this.displayAudioDetectionSub = this.libService.displayAudioDetection$.subscribe((value: boolean) => {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.videoControlsSub = this.libService.streamVideoControls$.subscribe((value: boolean) => {
this.showVideoControls = value;
// this.cd.markForCheck();
});
this.libService.minimal$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.isMinimal = value;
});
this.libService.displayParticipantName$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showParticipantName = value;
// this.cd.markForCheck();
});
this.libService.displayAudioDetection$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.libService.streamVideoControls$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showVideoControls = value;
// this.cd.markForCheck();
});
}
}

View File

@ -6,20 +6,26 @@
id="session-info-container"
[class.collapsed]="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
>
<span id="session-name" *ngIf="!isMinimal && room && room.name && showSessionName">{{ room.name }}</span>
<span id="session-name" *ngIf="!isMinimal && showRoomName">{{ roomName }}</span>
<div
id="activities-tag"
*ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
>
<div *ngIf="recordingStatus === _recordingStatus.STARTED" id="recording-tag" class="recording-tag">
<mat-icon class="blink">radio_button_checked</mat-icon>
<span class="blink">REC</span>
<span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span>
</div>
<div *ngIf="broadcastingStatus === _broadcastingStatus.STARTED" id="broadcasting-tag" class="broadcasting-tag">
<mat-icon class="blink">sensors</mat-icon>
<span class="blink">LIVE</span>
</div>
@if (recordingStatus === _recordingStatus.STARTED) {
<div id="recording-tag" class="recording-tag" (click)="openRecordingActivityPanel()">
<mat-icon class="blink">radio_button_checked</mat-icon>
<span class="blink">REC</span>
<span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span>
</div>
}
@if (broadcastingStatus === _broadcastingStatus.STARTED) {
<!-- Broadcasting tag -->
<div id="broadcasting-tag" class="broadcasting-tag">
<mat-icon class="blink">sensors</mat-icon>
<span class="blink">LIVE</span>
</div>
}
</div>
</div>
</div>
@ -120,18 +126,36 @@
*ngIf="!isMinimal && showRecordingButton"
mat-menu-item
id="recording-btn"
[disabled]="recordingStatus === _recordingStatus.STARTING || recordingStatus === _recordingStatus.STOPPING"
[disabled]="
recordingStatus === _recordingStatus.STARTING ||
recordingStatus === _recordingStatus.STOPPING ||
!hasRoomTracksPublished
"
[matTooltip]="!hasRoomTracksPublished ? ('TOOLBAR.NO_TRACKS_PUBLISHED' | translate) : ''"
(click)="toggleRecording()"
>
<mat-icon color="warn">radio_button_checked</mat-icon>
<span *ngIf="recordingStatus === _recordingStatus.STOPPED || recordingStatus === _recordingStatus.STOPPING">
{{ 'TOOLBAR.START_RECORDING' | translate }}
</span>
<span *ngIf="recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING">
{{ 'TOOLBAR.STOP_RECORDING' | translate }}
</span>
@if (
recordingStatus === _recordingStatus.STOPPED ||
recordingStatus === _recordingStatus.STOPPING ||
recordingStatus === _recordingStatus.FAILED
) {
<span class="blink">
{{ 'TOOLBAR.START_RECORDING' | translate }}
</span>
} @else if (recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING) {
<span>{{ 'TOOLBAR.STOP_RECORDING' | translate }}</span>
}
</button>
<!-- View recordings button -->
@if (!isMinimal && showViewRecordingsButton) {
<button mat-menu-item id="view-recordings-btn" (click)="onViewRecordingsClicked.emit()">
<mat-icon>video_library</mat-icon>
<span>{{ 'TOOLBAR.VIEW_RECORDINGS' | translate }}</span>
</button>
}
<!-- Broadcasting button -->
<button
*ngIf="!isMinimal && showBroadcastingButton"

View File

@ -25,16 +25,18 @@ With the following directives you can modify the default User Interface with the
<!-- start-dynamic-api-directives-content -->
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **screenshareButton** | `boolean` | [ToolbarScreenshareButtonDirective](../directives/ToolbarScreenshareButtonDirective.html) |
| **recordingButton** | `boolean` | [ToolbarRecordingButtonDirective](../directives/ToolbarRecordingButtonDirective.html) |
| **broadcastingButton** | `boolean` | [ToolbarBroadcastingButtonDirective](../directives/ToolbarBroadcastingButtonDirective.html) |
| **fullscreenButton** | `boolean` | [ToolbarFullscreenButtonDirective](../directives/ToolbarFullscreenButtonDirective.html) |
| **backgroundEffectsButton** | `boolean` | [ToolbarBackgroundEffectsButtonDirective](../directives/ToolbarBackgroundEffectsButtonDirective.html) |
| **settingsButton** | `boolean` | [ToolbarSettingsButtonDirective](../directives/ToolbarSettingsButtonDirective.html) |
| **leaveButton** | `boolean` | [ToolbarLeaveButtonDirective](../directives/ToolbarLeaveButtonDirective.html) |
| **participantsPanelButton** | `boolean` | [ToolbarParticipantsPanelButtonDirective](../directives/ToolbarParticipantsPanelButtonDirective.html) |
| **chatPanelButton** | `boolean` | [ToolbarChatPanelButtonDirective](../directives/ToolbarChatPanelButtonDirective.html) |
| **activitiesPanelButton** | `boolean` | [ToolbarActivitiesPanelButtonDirective](../directives/ToolbarActivitiesPanelButtonDirective.html) |
| **displayRoomName** | `boolean` | [ToolbarDisplayRoomNameDirective](../directives/ToolbarDisplayRoomNameDirective.html) |
| **backgroundEffectsButton** | `boolean` | [ToolbarBackgroundEffectsButtonDirective](../directives/ToolbarBackgroundEffectsButtonDirective.html) |
| **broadcastingButton** | `boolean` | [ToolbarBroadcastingButtonDirective](../directives/ToolbarBroadcastingButtonDirective.html) |
| **cameraButton** | `boolean` | [ToolbarCameraButtonDirective](../directives/ToolbarCameraButtonDirective.html) |
| **chatPanelButton** | `boolean` | [ToolbarChatPanelButtonDirective](../directives/ToolbarChatPanelButtonDirective.html) |
| **displayLogo** | `boolean` | [ToolbarDisplayLogoDirective](../directives/ToolbarDisplayLogoDirective.html) |
| **displayRoomName** | `boolean` | [ToolbarDisplayRoomNameDirective](../directives/ToolbarDisplayRoomNameDirective.html) |
| **fullscreenButton** | `boolean` | [ToolbarFullscreenButtonDirective](../directives/ToolbarFullscreenButtonDirective.html) |
| **leaveButton** | `boolean` | [ToolbarLeaveButtonDirective](../directives/ToolbarLeaveButtonDirective.html) |
| **microphoneButton** | `boolean` | [ToolbarMicrophoneButtonDirective](../directives/ToolbarMicrophoneButtonDirective.html) |
| **participantsPanelButton** | `boolean` | [ToolbarParticipantsPanelButtonDirective](../directives/ToolbarParticipantsPanelButtonDirective.html) |
| **recordingButton** | `boolean` | [ToolbarRecordingButtonDirective](../directives/ToolbarRecordingButtonDirective.html) |
| **screenshareButton** | `boolean` | [ToolbarScreenshareButtonDirective](../directives/ToolbarScreenshareButtonDirective.html) |
| **settingsButton** | `boolean` | [ToolbarSettingsButtonDirective](../directives/ToolbarSettingsButtonDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

@ -126,6 +126,7 @@ $ov-recording-blinking-color: #eb5144;
text-align: center;
line-height: 20px;
margin: auto;
cursor: pointer;
}
.recording-tag {

View File

@ -12,7 +12,7 @@ import {
TemplateRef,
ViewChild
} from '@angular/core';
import { fromEvent, skip, Subscription } from 'rxjs';
import { fromEvent, skip, Subject, takeUntil } from 'rxjs';
import { ChatService } from '../../services/chat/chat.service';
import { DocumentService } from '../../services/document/document.service';
import { PanelService } from '../../services/panel/panel.service';
@ -44,6 +44,7 @@ import { ParticipantService } from '../../services/participant/participant.servi
import { PlatformService } from '../../services/platform/platform.service';
import { RecordingService } from '../../services/recording/recording.service';
import { StorageService } from '../../services/storage/storage.service';
import { TemplateManagerService, ToolbarTemplateConfiguration } from '../../services/template/template-manager.service';
import { TranslateService } from '../../services/translate/translate.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
@ -77,10 +78,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
@ContentChild(ToolbarAdditionalButtonsDirective)
set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) {
// This directive will has value only when ADDITIONAL BUTTONS component (tagged with '*ovToolbarAdditionalButtons' directive)
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
this._externalAdditionalButtons = externalAdditionalButtons;
if (externalAdditionalButtons) {
this.toolbarAdditionalButtonsTemplate = externalAdditionalButtons.template;
this.updateTemplatesAndMarkForCheck();
}
}
@ -89,16 +89,15 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
@ContentChild(ToolbarAdditionalPanelButtonsDirective)
set externalAdditionalPanelButtons(externalAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective) {
// This directive will has value only when ADDITIONAL PANEL BUTTONS component tagged with '*ovToolbarAdditionalPanelButtons' directive
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
this._externalAdditionalPanelButtons = externalAdditionalPanelButtons;
if (externalAdditionalPanelButtons) {
this.toolbarAdditionalPanelButtonsTemplate = externalAdditionalPanelButtons.template;
this.updateTemplatesAndMarkForCheck();
}
}
/**
* This event is emitted when the room has been disconnected.
* @deprecated Use {@link onParticipantLeft} instead.
* @deprecated Use {@link ToolbarComponent.onParticipantLeft} instead.
*/
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -145,6 +144,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new EventEmitter<BroadcastingStopRequestedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* @ignore
*/
@ -240,6 +245,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
showRecordingButton: boolean = true;
/**
* @ignore
*/
showViewRecordingsButton: boolean = false;
/**
* @ignore
*/
@ -280,7 +290,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* @ignore
*/
showSessionName: boolean = true;
showRoomName: boolean = true;
/**
* @ignore
*/
roomName: string = '';
/**
* @ignore
@ -312,6 +327,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
isRecordingReadOnlyMode: boolean = false;
/**
* @ignore
*/
@ -339,31 +359,18 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
recordingTime: Date;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: ToolbarTemplateConfiguration = {};
// Store directive references for template setup
private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective;
private _externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
private log: ILogger;
private minimalSub: Subscription;
private panelTogglingSubscription: Subscription;
private chatMessagesSubscription: Subscription;
private localParticipantSubscription: Subscription;
private cameraButtonSub: Subscription;
private microphoneButtonSub: Subscription;
private screenshareButtonSub: Subscription;
private fullscreenButtonSub: Subscription;
private backgroundEffectsButtonSub: Subscription;
private leaveButtonSub: Subscription;
private recordingButtonSub: Subscription;
private broadcastingButtonSub: Subscription;
private recordingSubscription: Subscription;
private broadcastingSubscription: Subscription;
private activitiesPanelButtonSub: Subscription;
private participantsPanelButtonSub: Subscription;
private chatPanelButtonSub: Subscription;
private displayLogoSub: Subscription;
private brandingLogoSub: Subscription;
private displayRoomNameSub: Subscription;
private settingsButtonSub: Subscription;
private captionsSubs: Subscription;
private additionalButtonsPositionSub: Subscription;
private fullscreenChangeSubscription: Subscription;
private destroy$ = new Subject<void>();
private currentWindowHeight = window.innerHeight;
/**
@ -386,7 +393,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private broadcastingService: BroadcastingService,
private translateService: TranslateService,
private storageSrv: StorageService,
private cdkOverlayService: CdkOverlayService
private cdkOverlayService: CdkOverlayService,
private templateManagerService: TemplateManagerService
) {
this.log = this.loggerSrv.get('ToolbarComponent');
}
@ -416,10 +424,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
async ngOnInit() {
this.room = this.openviduService.getRoom();
this.evalAndSetRoomName(this.libService.getRoomName());
this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable();
this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable();
this.setupTemplates();
this.subscribeToToolbarDirectives();
this.subscribeToUserMediaProperties();
@ -437,34 +447,55 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnDestroy(): void {
this.panelService.clear();
if (this.panelTogglingSubscription) this.panelTogglingSubscription.unsubscribe();
if (this.chatMessagesSubscription) this.chatMessagesSubscription.unsubscribe();
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
if (this.cameraButtonSub) this.cameraButtonSub.unsubscribe();
if (this.microphoneButtonSub) this.microphoneButtonSub.unsubscribe();
if (this.screenshareButtonSub) this.screenshareButtonSub.unsubscribe();
if (this.fullscreenButtonSub) this.fullscreenButtonSub.unsubscribe();
if (this.backgroundEffectsButtonSub) this.backgroundEffectsButtonSub.unsubscribe();
if (this.leaveButtonSub) this.leaveButtonSub.unsubscribe();
if (this.recordingButtonSub) this.recordingButtonSub.unsubscribe();
if (this.broadcastingButtonSub) this.broadcastingButtonSub.unsubscribe();
if (this.participantsPanelButtonSub) this.participantsPanelButtonSub.unsubscribe();
if (this.chatPanelButtonSub) this.chatPanelButtonSub.unsubscribe();
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
if (this.brandingLogoSub) this.brandingLogoSub.unsubscribe();
if (this.displayRoomNameSub) this.displayRoomNameSub.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
if (this.activitiesPanelButtonSub) this.activitiesPanelButtonSub.unsubscribe();
if (this.recordingSubscription) this.recordingSubscription.unsubscribe();
if (this.broadcastingSubscription) this.broadcastingSubscription.unsubscribe();
if (this.settingsButtonSub) this.settingsButtonSub.unsubscribe();
if (this.captionsSubs) this.captionsSubs.unsubscribe();
if (this.fullscreenChangeSubscription) this.fullscreenChangeSubscription.unsubscribe();
if (this.additionalButtonsPositionSub) this.additionalButtonsPositionSub.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
this.isFullscreenActive = false;
this.cdkOverlayService.setSelector('body');
}
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
this._externalAdditionalButtons,
this._externalAdditionalPanelButtons
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
if (this.templateConfig.toolbarAdditionalButtonsTemplate) {
this.toolbarAdditionalButtonsTemplate = this.templateConfig.toolbarAdditionalButtonsTemplate;
}
if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) {
this.toolbarAdditionalPanelButtonsTemplate = this.templateConfig.toolbarAdditionalPanelButtonsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
/**
* @internal
*/
get hasRoomTracksPublished(): boolean {
return this.openviduService.hasRoomTracksPublished();
}
/**
* @ignore
*/
@ -538,10 +569,33 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
}
/**
* @ignore
*/
openRecordingActivityPanel() {
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.panelService.togglePanel(PanelType.ACTIVITIES, 'recording');
}
}
/**
* @ignore
*/
openBroadcastingActivityPanel() {
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.panelService.togglePanel(PanelType.ACTIVITIES, 'broadcasting');
}
}
/**
* @ignore
*/
toggleRecording() {
if (this.recordingStatus === RecordingStatus.FAILED) {
this.openRecordingActivityPanel();
return;
}
const payload: RecordingStartRequestedEvent = {
roomName: this.openviduService.getRoomName()
};
@ -551,9 +605,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onRecordingStopRequested.emit(payload);
} else if (this.recordingStatus === RecordingStatus.STOPPED) {
this.onRecordingStartRequested.emit(payload);
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('recording');
}
this.openRecordingActivityPanel();
}
}
@ -570,9 +622,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onBroadcastingStopRequested.emit(payload);
this.broadcastingService.setBroadcastingStopped();
} else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) {
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('broadcasting');
}
this.openBroadcastingActivityPanel();
}
}
@ -625,7 +675,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.documentService.toggleFullscreen('session-container');
}
private toggleActivitiesPanel(expandPanel: string) {
/**
* @internal
* @param expandPanel
*/
toggleActivitiesPanel(expandPanel: string) {
this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel);
}
@ -640,21 +694,23 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToFullscreenChanged() {
this.fullscreenChangeSubscription = fromEvent(document, 'fullscreenchange').subscribe(() => {
const isFullscreen = Boolean(document.fullscreenElement);
if (isFullscreen) {
this.cdkOverlayService.setSelector('#session-container');
} else {
this.cdkOverlayService.setSelector('body');
}
this.isFullscreenActive = isFullscreen;
this.onFullscreenEnabledChanged.emit(this.isFullscreenActive);
this.cd.detectChanges();
});
fromEvent(document, 'fullscreenchange')
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
const isFullscreen = Boolean(document.fullscreenElement);
if (isFullscreen) {
this.cdkOverlayService.setSelector('#session-container');
} else {
this.cdkOverlayService.setSelector('body');
}
this.isFullscreenActive = isFullscreen;
this.onFullscreenEnabledChanged.emit(this.isFullscreenActive);
this.cd.detectChanges();
});
}
private subscribeToMenuToggling() {
this.panelTogglingSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
this.isChatOpened = ev.isOpened && ev.panelType === PanelType.CHAT;
this.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES;
@ -666,7 +722,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToChatMessages() {
this.chatMessagesSubscription = this.chatService.messagesObs.pipe(skip(1)).subscribe((messages) => {
this.chatService.messagesObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((messages) => {
if (!this.panelService.isChatPanelOpened()) {
this.unreadMessages++;
}
@ -675,7 +731,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
});
}
private subscribeToUserMediaProperties() {
this.localParticipantSubscription = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p: ParticipantModel | undefined) => {
if (p) {
if (this.isCameraEnabled !== p.isCameraEnabled) {
this.onVideoEnabledChanged.emit(p.isCameraEnabled);
@ -699,8 +755,13 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToRecordingStatus() {
this.recordingSubscription = this.recordingService.recordingStatusObs.subscribe((event: RecordingStatusInfo) => {
const { status, recordingElapsedTime } = event;
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => {
this.isRecordingReadOnlyMode = readOnly;
this.cd.markForCheck();
});
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, startedAt } = event;
this.recordingStatus = status;
if (status === RecordingStatus.STARTED) {
this.startedRecording = event.recordingList.find((rec) => rec.status === RecordingStatus.STARTED);
@ -708,15 +769,15 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.startedRecording = undefined;
}
if (recordingElapsedTime) {
this.recordingTime = recordingElapsedTime;
if (startedAt) {
this.recordingTime = startedAt;
}
this.cd.markForCheck();
});
}
private subscribeToBroadcastingStatus() {
this.broadcastingSubscription = this.broadcastingService.broadcastingStatusObs.subscribe((ev: BroadcastingStatusInfo) => {
this.broadcastingService.broadcastingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: BroadcastingStatusInfo) => {
if (!!ev) {
this.broadcastingStatus = ev.status;
this.broadcastingId = ev.broadcastingId;
@ -726,86 +787,97 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToToolbarDirectives() {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.isMinimal = value;
this.cd.markForCheck();
});
this.brandingLogoSub = this.libService.brandingLogo$.subscribe((value: string) => {
this.libService.brandingLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
this.brandingLogo = value;
this.cd.markForCheck();
});
this.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => {
this.libService.toolbarViewRecordingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showViewRecordingsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value;
this.cd.markForCheck();
});
this.microphoneButtonSub = this.libService.microphoneButton$.subscribe((value: boolean) => {
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showMicrophoneButton = value;
this.cd.markForCheck();
});
this.screenshareButtonSub = this.libService.screenshareButton$.subscribe((value: boolean) => {
this.libService.screenshareButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showScreenshareButton = value && !this.platformService.isMobile();
this.cd.markForCheck();
});
this.fullscreenButtonSub = this.libService.fullscreenButton$.subscribe((value: boolean) => {
this.libService.fullscreenButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showFullscreenButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.leaveButtonSub = this.libService.leaveButton$.subscribe((value: boolean) => {
this.libService.leaveButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showLeaveButton = value;
this.cd.markForCheck();
});
this.recordingButtonSub = this.libService.recordingButton$.subscribe((value: boolean) => {
this.libService.recordingButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showRecordingButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.broadcastingButtonSub = this.libService.broadcastingButton$.subscribe((value: boolean) => {
this.libService.broadcastingButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showBroadcastingButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.settingsButtonSub = this.libService.toolbarSettingsButton$.subscribe((value: boolean) => {
this.libService.toolbarSettingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showSettingsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.chatPanelButtonSub = this.libService.chatPanelButton$.subscribe((value: boolean) => {
this.libService.chatPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showChatPanelButton = value;
this.cd.markForCheck();
});
this.participantsPanelButtonSub = this.libService.participantsPanelButton$.subscribe((value: boolean) => {
this.libService.participantsPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showParticipantsPanelButton = value;
this.cd.markForCheck();
});
this.activitiesPanelButtonSub = this.libService.activitiesPanelButton$.subscribe((value: boolean) => {
this.libService.activitiesPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showActivitiesPanelButton = value;
this.cd.markForCheck();
});
this.backgroundEffectsButtonSub = this.libService.backgroundEffectsButton$.subscribe((value: boolean) => {
this.libService.backgroundEffectsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showBackgroundEffectsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showLogo = value;
this.cd.markForCheck();
});
this.displayRoomNameSub = this.libService.displayRoomName$.subscribe((value: boolean) => {
this.showSessionName = value;
this.cd.markForCheck();
});
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => {
this.showCaptionsButton = value;
this.libService.displayRoomName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showRoomName = value;
this.cd.markForCheck();
});
this.additionalButtonsPositionSub = this.libService.toolbarAdditionalButtonsPosition$.subscribe(
(value: ToolbarAdditionalButtonsPosition) => {
this.libService.roomName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
this.evalAndSetRoomName(value);
this.cd.markForCheck();
});
// this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
// this.showCaptionsButton = value;
// this.cd.markForCheck();
// });
this.libService.toolbarAdditionalButtonsPosition$
.pipe(takeUntil(this.destroy$))
.subscribe((value: ToolbarAdditionalButtonsPosition) => {
// Using Promise.resolve() to defer change detection until the next microtask.
// This ensures that Angular's change detection has the latest value before updating the view.
// Without this, Angular's OnPush strategy might not immediately reflect the change,
@ -815,12 +887,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.additionalButtonsPosition = value;
this.cd.markForCheck();
});
}
);
});
}
private subscribeToCaptionsToggling() {
this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => {
this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.captionsEnabled = value;
this.cd.markForCheck();
});
@ -834,4 +905,14 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.showBroadcastingButton ||
this.showSettingsButton;
}
private evalAndSetRoomName(value: string) {
if (!!value) {
this.roomName = value;
} else if (!!this.room && this.room.name) {
this.roomName = this.room.name;
} else {
this.roomName = '';
}
}
}

View File

@ -1,27 +1,36 @@
<div id="call-container">
<div id="spinner" *ngIf="loading">
<mat-spinner [diameter]="50"></mat-spinner>
<div id="spinner" *ngIf="componentState.isLoading">
<mat-spinner [diameter]="spinnerDiameter"></mat-spinner>
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
</div>
<div [@inOutAnimation] id="pre-join-container" *ngIf="showPrejoin && !loading">
<ov-pre-join
[error]="_tokenError"
(onReadyToJoin)="_onReadyToJoin()"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
(onLangChanged)="onLangChanged.emit($event)"
></ov-pre-join>
<div [@inOutAnimation] id="pre-join-container" *ngIf="componentState.showPrejoin && !componentState.isLoading">
<ng-container *ngIf="openviduAngularPreJoinTemplate; else defaultPreJoin">
<ng-container *ngTemplateOutlet="openviduAngularPreJoinTemplate"></ng-container>
</ng-container>
<ng-template #defaultPreJoin>
<ov-pre-join
[error]="componentState.error?.tokenError"
(onReadyToJoin)="_onReadyToJoin()"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
(onLangChanged)="onLangChanged.emit($event)"
></ov-pre-join>
</ng-template>
</div>
<div id="spinner" *ngIf="!loading && error">
<div id="spinner" *ngIf="!componentState.isLoading && componentState.error?.hasError">
<mat-icon class="error-icon">error</mat-icon>
<span>{{ errorMessage }}</span>
<span>{{ componentState.error?.message }}</span>
</div>
<div [@inOutAnimation] id="vc-container" *ngIf="isRoomReady && !showPrejoin && !loading && !error">
<div
[@inOutAnimation]
id="vc-container"
*ngIf="componentState.isRoomReady && !componentState.showPrejoin && !componentState.isLoading && !componentState.error?.hasError"
>
<ov-session
(onRoomCreated)="onRoomCreated.emit($event)"
(onRoomReconnecting)="onRoomDisconnected.emit()"
@ -64,6 +73,7 @@
(onRecordingStartRequested)="onRecordingStartRequested.emit($event)"
(onRecordingStopRequested)="onRecordingStopRequested.emit($event)"
(onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
>
<ng-template #toolbarAdditionalButtons>
<ng-container *ngTemplateOutlet="openviduAngularToolbarAdditionalButtonsTemplate"></ng-container>
@ -128,6 +138,8 @@
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
(onBroadcastingStartRequested)="onBroadcastingStartRequested.emit($event)"
(onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)"
></ov-activities-panel>
@ -140,6 +152,9 @@
*ngTemplateOutlet="openviduAngularParticipantPanelItemTemplate; context: { $implicit: participant }"
></ng-container>
</ng-template>
<ng-template #participantPanelAfterLocalParticipant>
<ng-container *ngTemplateOutlet="openviduAngularParticipantPanelAfterLocalParticipantTemplate"></ng-container>
</ng-template>
</ov-participants-panel>
</ng-template>
@ -158,6 +173,10 @@
<ng-template #stream let-track>
<ng-container *ngTemplateOutlet="openviduAngularStreamTemplate; context: { $implicit: track }"></ng-container>
</ng-template>
<ng-template #layoutAdditionalElements>
<ng-container *ngTemplateOutlet="ovLayoutAdditionalElementsTemplate"></ng-container>
</ng-template>
</ov-layout>
</ng-template>

View File

@ -23,32 +23,35 @@ With the following directives you can modify the default User Interface with the
<!-- start-dynamic-api-directives-content -->
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **activitiesPanelBroadcastingActivity** | `boolean` | [ActivitiesPanelBroadcastingActivityDirective](../directives/ActivitiesPanelBroadcastingActivityDirective.html) |
| **activitiesPanelRecordingActivity** | `boolean` | [ActivitiesPanelRecordingActivityDirective](../directives/ActivitiesPanelRecordingActivityDirective.html) |
| **audioEnabled** | `boolean` | [AudioEnabledDirective](../directives/AudioEnabledDirective.html) |
| **lang** | `AvailableLangs` | [LangDirective](../directives/LangDirective.html) |
| **langOptions** | `LangOption[]` | [LangOptionsDirective](../directives/LangOptionsDirective.html) |
| **livekitUrl** | `string` | [LivekitUrlDirective](../directives/LivekitUrlDirective.html) |
| **minimal** | `boolean` | [MinimalDirective](../directives/MinimalDirective.html) |
| **participantName** | `string` | [ParticipantNameDirective](../directives/ParticipantNameDirective.html) |
| **participantPanelItemMuteButton** | `boolean` | [ParticipantPanelItemMuteButtonDirective](../directives/ParticipantPanelItemMuteButtonDirective.html) |
| **prejoin** | `boolean` | [PrejoinDirective](../directives/PrejoinDirective.html) |
| **recordingStreamBaseUrl** | `string` | [RecordingStreamBaseUrlDirective](../directives/RecordingStreamBaseUrlDirective.html) |
| **streamDisplayAudioDetection** | `boolean` | [StreamDisplayAudioDetectionDirective](../directives/StreamDisplayAudioDetectionDirective.html) |
| **streamDisplayParticipantName** | `boolean` | [StreamDisplayParticipantNameDirective](../directives/StreamDisplayParticipantNameDirective.html) |
| **streamVideoControls** | `boolean` | [StreamVideoControlsDirective](../directives/StreamVideoControlsDirective.html) |
| **token** | `string` | [TokenDirective](../directives/TokenDirective.html) |
| **tokenError** | `any` | [TokenErrorDirective](../directives/TokenErrorDirective.html) |
| **minimal** | `boolean` | [MinimalDirective](../directives/MinimalDirective.html) |
| **lang** | `string` | [LangDirective](../directives/LangDirective.html) |
| **langOptions** | `LangOption` | [LangOptionsDirective](../directives/LangOptionsDirective.html) |
| **participantName** | `string` | [ParticipantNameDirective](../directives/ParticipantNameDirective.html) |
| **prejoin** | `boolean` | [PrejoinDirective](../directives/PrejoinDirective.html) |
| **videoEnabled** | `boolean` | [VideoEnabledDirective](../directives/VideoEnabledDirective.html) |
| **audioEnabled** | `boolean` | [AudioEnabledDirective](../directives/AudioEnabledDirective.html) |
| **toolbarScreenshareButton** | `boolean` | [ToolbarScreenshareButtonDirective](../directives/ToolbarScreenshareButtonDirective.html) |
| **toolbarRecordingButton** | `boolean` | [ToolbarRecordingButtonDirective](../directives/ToolbarRecordingButtonDirective.html) |
| **toolbarBroadcastingButton** | `boolean` | [ToolbarBroadcastingButtonDirective](../directives/ToolbarBroadcastingButtonDirective.html) |
| **toolbarFullscreenButton** | `boolean` | [ToolbarFullscreenButtonDirective](../directives/ToolbarFullscreenButtonDirective.html) |
| **toolbarBackgroundEffectsButton** | `boolean` | [ToolbarBackgroundEffectsButtonDirective](../directives/ToolbarBackgroundEffectsButtonDirective.html) |
| **toolbarSettingsButton** | `boolean` | [ToolbarSettingsButtonDirective](../directives/ToolbarSettingsButtonDirective.html) |
| **toolbarLeaveButton** | `boolean` | [ToolbarLeaveButtonDirective](../directives/ToolbarLeaveButtonDirective.html) |
| **toolbarParticipantsPanelButton** | `boolean` | [ToolbarParticipantsPanelButtonDirective](../directives/ToolbarParticipantsPanelButtonDirective.html) |
| **toolbarChatPanelButton** | `boolean` | [ToolbarChatPanelButtonDirective](../directives/ToolbarChatPanelButtonDirective.html) |
| **toolbarActivitiesPanelButton** | `boolean` | [ToolbarActivitiesPanelButtonDirective](../directives/ToolbarActivitiesPanelButtonDirective.html) |
| **toolbarDisplayRoomName** | `boolean` | [ToolbarDisplayRoomNameDirective](../directives/ToolbarDisplayRoomNameDirective.html) |
| **toolbarBackgroundEffectsButton** | `boolean` | [ToolbarBackgroundEffectsButtonDirective](../directives/ToolbarBackgroundEffectsButtonDirective.html) |
| **toolbarBroadcastingButton** | `boolean` | [ToolbarBroadcastingButtonDirective](../directives/ToolbarBroadcastingButtonDirective.html) |
| **toolbarCameraButton** | `boolean` | [ToolbarCameraButtonDirective](../directives/ToolbarCameraButtonDirective.html) |
| **toolbarChatPanelButton** | `boolean` | [ToolbarChatPanelButtonDirective](../directives/ToolbarChatPanelButtonDirective.html) |
| **toolbarDisplayLogo** | `boolean` | [ToolbarDisplayLogoDirective](../directives/ToolbarDisplayLogoDirective.html) |
| **streamDisplayParticipantName** | `boolean` | [StreamDisplayParticipantNameDirective](../directives/StreamDisplayParticipantNameDirective.html) |
| **streamDisplayAudioDetection** | `boolean` | [StreamDisplayAudioDetectionDirective](../directives/StreamDisplayAudioDetectionDirective.html) |
| **streamVideoControls** | `boolean` | [StreamVideoControlsDirective](../directives/StreamVideoControlsDirective.html) |
| **participantPanelItemMuteButton** | `boolean` | [ParticipantPanelItemMuteButtonDirective](../directives/ParticipantPanelItemMuteButtonDirective.html) |
| **activitiesPanelRecordingActivity** | `boolean` | [ActivitiesPanelRecordingActivityDirective](../directives/ActivitiesPanelRecordingActivityDirective.html) |
| **activitiesPanelBroadcastingActivity** | `boolean` | [ActivitiesPanelBroadcastingActivityDirective](../directives/ActivitiesPanelBroadcastingActivityDirective.html) |
| **toolbarDisplayRoomName** | `boolean` | [ToolbarDisplayRoomNameDirective](../directives/ToolbarDisplayRoomNameDirective.html) |
| **toolbarFullscreenButton** | `boolean` | [ToolbarFullscreenButtonDirective](../directives/ToolbarFullscreenButtonDirective.html) |
| **toolbarLeaveButton** | `boolean` | [ToolbarLeaveButtonDirective](../directives/ToolbarLeaveButtonDirective.html) |
| **toolbarMicrophoneButton** | `boolean` | [ToolbarMicrophoneButtonDirective](../directives/ToolbarMicrophoneButtonDirective.html) |
| **toolbarParticipantsPanelButton** | `boolean` | [ToolbarParticipantsPanelButtonDirective](../directives/ToolbarParticipantsPanelButtonDirective.html) |
| **toolbarRecordingButton** | `boolean` | [ToolbarRecordingButtonDirective](../directives/ToolbarRecordingButtonDirective.html) |
| **toolbarScreenshareButton** | `boolean` | [ToolbarScreenshareButtonDirective](../directives/ToolbarScreenshareButtonDirective.html) |
| **toolbarSettingsButton** | `boolean` | [ToolbarSettingsButtonDirective](../directives/ToolbarSettingsButtonDirective.html) |
| **videoEnabled** | `boolean` | [VideoEnabledDirective](../directives/VideoEnabledDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

@ -1,6 +1,17 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { AfterViewInit, Component, ContentChild, EventEmitter, OnDestroy, Output, TemplateRef, ViewChild } from '@angular/core';
import { Subscription, filter, skip, take } from 'rxjs';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
EventEmitter,
OnDestroy,
Output,
TemplateRef,
ViewChild
} from '@angular/core';
import { Subject, filter, skip, take, takeUntil } from 'rxjs';
import {
ActivitiesPanelDirective,
AdditionalPanelsDirective,
@ -16,12 +27,19 @@ import {
ToolbarDirective
} from '../../directives/template/openvidu-components-angular.directive';
import { ILogger } from '../../models/logger.model';
import { VideoconferenceState, VideoconferenceStateInfo } from '../../models/videoconference-state.model';
import { ActionService } from '../../services/action/action.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { DeviceService } from '../../services/device/device.service';
import { LoggerService } from '../../services/logger/logger.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { StorageService } from '../../services/storage/storage.service';
import {
TemplateManagerService,
TemplateConfiguration,
ExternalDirectives,
DefaultTemplates
} from '../../services/template/template-manager.service';
import { Room } from 'livekit-client';
import { ParticipantLeftEvent, ParticipantModel } from '../../models/participant.model';
import { CustomDevice } from '../../models/device.model';
@ -40,6 +58,11 @@ import {
} from '../../models/recording.model';
import { BroadcastingStartRequestedEvent, BroadcastingStopRequestedEvent } from '../../models/broadcasting.model';
import { LangOption } from '../../models/lang.model';
import {
LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective,
PreJoinDirective
} from '../../directives/template/internals.directive';
/**
* The **VideoconferenceComponent** is the parent of all OpenVidu components.
@ -51,68 +74,286 @@ import { LangOption } from '../../models/lang.model';
styleUrls: ['./videoconference.component.scss'],
animations: [
trigger('inOutAnimation', [
transition(':enter', [style({ opacity: 0 }), animate('300ms ease-out', style({ opacity: 1 }))])
transition(':enter', [
style({ opacity: 0 }),
animate(`${VideoconferenceComponent.ANIMATION_DURATION_MS}ms ease-out`, style({ opacity: 1 }))
])
// transition(':leave', [style({ opacity: 1 }), animate('50ms ease-in', style({ opacity: 0.9 }))])
])
],
standalone: false
})
export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
// Constants
private static readonly PARTICIPANT_NAME_TIMEOUT_MS = 1000;
private static readonly ANIMATION_DURATION_MS = 300;
private static readonly MATERIAL_ICONS_URL =
'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=background_replace,keep_off';
private static readonly MATERIAL_ICONS_SELECTOR = 'link[href*="Material+Symbols+Outlined"]';
private static readonly SPINNER_DIAMETER = 50;
// *** Toolbar ***
private _externalToolbar?: ToolbarDirective;
/**
* @internal
*/
@ContentChild(ToolbarDirective) externalToolbar: ToolbarDirective;
@ContentChild(ToolbarDirective)
set externalToolbar(value: ToolbarDirective) {
this._externalToolbar = value;
this.setupTemplates();
}
get externalToolbar(): ToolbarDirective | undefined {
return this._externalToolbar;
}
private _externalToolbarAdditionalButtons?: ToolbarAdditionalButtonsDirective;
/**
* @internal
*/
@ContentChild(ToolbarAdditionalButtonsDirective) externalToolbarAdditionalButtons: ToolbarAdditionalButtonsDirective;
@ContentChild(ToolbarAdditionalButtonsDirective)
set externalToolbarAdditionalButtons(value: ToolbarAdditionalButtonsDirective) {
this._externalToolbarAdditionalButtons = value;
this.setupTemplates();
}
/**
* @internal
*/
@ContentChild(ToolbarAdditionalPanelButtonsDirective) externalToolbarAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective;
get externalToolbarAdditionalButtons(): ToolbarAdditionalButtonsDirective | undefined {
return this._externalToolbarAdditionalButtons;
}
private _externalToolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
/**
* @internal
*/
@ContentChild(AdditionalPanelsDirective) externalAdditionalPanels: AdditionalPanelsDirective;
@ContentChild(ToolbarAdditionalPanelButtonsDirective)
set externalToolbarAdditionalPanelButtons(value: ToolbarAdditionalPanelButtonsDirective) {
this._externalToolbarAdditionalPanelButtons = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalToolbarAdditionalPanelButtons(): ToolbarAdditionalPanelButtonsDirective | undefined {
return this._externalToolbarAdditionalPanelButtons;
}
private _externalAdditionalPanels?: AdditionalPanelsDirective;
/**
* @internal
*/
@ContentChild(AdditionalPanelsDirective)
set externalAdditionalPanels(value: AdditionalPanelsDirective) {
this._externalAdditionalPanels = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalAdditionalPanels(): AdditionalPanelsDirective | undefined {
return this._externalAdditionalPanels;
}
// *** Panels ***
/**
* @internal
*/
@ContentChild(PanelDirective) externalPanel: PanelDirective;
/**
* @internal
*/
@ContentChild(ChatPanelDirective) externalChatPanel: ChatPanelDirective;
/**
* @internal
*/
@ContentChild(ActivitiesPanelDirective) externalActivitiesPanel: ActivitiesPanelDirective;
private _externalPanel?: PanelDirective;
/**
* @internal
*/
@ContentChild(ParticipantsPanelDirective) externalParticipantsPanel: ParticipantsPanelDirective;
@ContentChild(PanelDirective)
set externalPanel(value: PanelDirective) {
this._externalPanel = value;
this.setupTemplates();
}
/**
* @internal
*/
@ContentChild(ParticipantPanelItemDirective) externalParticipantPanelItem: ParticipantPanelItemDirective;
get externalPanel(): PanelDirective | undefined {
return this._externalPanel;
}
private _externalChatPanel?: ChatPanelDirective;
/**
* @internal
*/
@ContentChild(ParticipantPanelItemElementsDirective) externalParticipantPanelItemElements: ParticipantPanelItemElementsDirective;
@ContentChild(ChatPanelDirective)
set externalChatPanel(value: ChatPanelDirective) {
this._externalChatPanel = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalChatPanel(): ChatPanelDirective | undefined {
return this._externalChatPanel;
}
private _externalActivitiesPanel?: ActivitiesPanelDirective;
/**
* @internal
*/
@ContentChild(ActivitiesPanelDirective)
set externalActivitiesPanel(value: ActivitiesPanelDirective) {
this._externalActivitiesPanel = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalActivitiesPanel(): ActivitiesPanelDirective | undefined {
return this._externalActivitiesPanel;
}
private _externalParticipantsPanel?: ParticipantsPanelDirective;
/**
* @internal
*/
@ContentChild(ParticipantsPanelDirective)
set externalParticipantsPanel(value: ParticipantsPanelDirective) {
this._externalParticipantsPanel = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalParticipantsPanel(): ParticipantsPanelDirective | undefined {
return this._externalParticipantsPanel;
}
private _externalParticipantPanelItem?: ParticipantPanelItemDirective;
/**
* @internal
*/
@ContentChild(ParticipantPanelItemDirective)
set externalParticipantPanelItem(value: ParticipantPanelItemDirective) {
this._externalParticipantPanelItem = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalParticipantPanelItem(): ParticipantPanelItemDirective | undefined {
return this._externalParticipantPanelItem;
}
private _externalParticipantPanelItemElements?: ParticipantPanelItemElementsDirective;
/**
* @internal
*/
@ContentChild(ParticipantPanelItemElementsDirective)
set externalParticipantPanelItemElements(value: ParticipantPanelItemElementsDirective) {
this._externalParticipantPanelItemElements = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalParticipantPanelItemElements(): ParticipantPanelItemElementsDirective | undefined {
return this._externalParticipantPanelItemElements;
}
// *** Layout ***
private _externalLayout?: LayoutDirective;
/**
* @internal
*/
@ContentChild(LayoutDirective) externalLayout: LayoutDirective;
@ContentChild(LayoutDirective)
set externalLayout(value: LayoutDirective) {
this._externalLayout = value;
this.setupTemplates();
}
/**
* @internal
*/
@ContentChild(StreamDirective) externalStream: StreamDirective;
get externalLayout(): LayoutDirective | undefined {
return this._externalLayout;
}
private _externalStream?: StreamDirective;
/**
* @internal
*/
@ContentChild(StreamDirective)
set externalStream(value: StreamDirective) {
this._externalStream = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalStream(): StreamDirective | undefined {
return this._externalStream;
}
// *** PreJoin ***
private _externalPreJoin?: PreJoinDirective;
/**
* @internal
*/
@ContentChild(PreJoinDirective)
set externalPreJoin(value: PreJoinDirective) {
this._externalPreJoin = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalPreJoin(): PreJoinDirective | undefined {
return this._externalPreJoin;
}
private _externalParticipantPanelAfterLocalParticipant?: ParticipantPanelAfterLocalParticipantDirective;
/**
* @internal
*/
@ContentChild(ParticipantPanelAfterLocalParticipantDirective)
set externalParticipantPanelAfterLocalParticipant(value: ParticipantPanelAfterLocalParticipantDirective) {
this._externalParticipantPanelAfterLocalParticipant = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalParticipantPanelAfterLocalParticipant(): ParticipantPanelAfterLocalParticipantDirective | undefined {
return this._externalParticipantPanelAfterLocalParticipant;
}
private _externalLayoutAdditionalElements?: LayoutAdditionalElementsDirective;
/**
* @internal
*/
@ContentChild(LayoutAdditionalElementsDirective)
set externalLayoutAdditionalElements(value: LayoutAdditionalElementsDirective) {
this._externalLayoutAdditionalElements = value;
this.setupTemplates();
}
/**
* @internal
*/
get externalLayoutAdditionalElements(): LayoutAdditionalElementsDirective | undefined {
return this._externalLayoutAdditionalElements;
}
/**
* @internal
@ -182,6 +423,10 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal
*/
openviduAngularAdditionalPanelsTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularParticipantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
/**
* @internal
*/
@ -198,6 +443,20 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal
*/
openviduAngularStreamTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularPreJoinTemplate: TemplateRef<any>;
/**
* @internal
*/
ovLayoutAdditionalElementsTemplate: TemplateRef<any>;
/**
* @internal
* Template configuration managed by TemplateManagerService
*/
private templateConfig: TemplateConfiguration;
/**
* Provides event notifications that fire when the local participant is ready to join to the room.
@ -213,7 +472,7 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
/**
* Provides event notifications that fire when Room is disconnected for the local participant.
* @deprecated Use {@link onParticipantLeft} instead
* @deprecated Use {@link VideoconferenceComponent.onParticipantLeft} instead
*/
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -315,6 +574,13 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
*/
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recording button.
* It provides the recording ID as event data.
*/
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when download recording button is clicked from {@link ActivitiesPanelComponent}.
* It provides the {@link RecordingDownloadClickedEvent} payload as event data.
@ -335,6 +601,12 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new EventEmitter<BroadcastingStopRequestedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when Room is created for the local participant.
* It provides the {@link https://openvidu.io/latest/docs/getting-started/#room Room} payload as event data.
@ -355,37 +627,55 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
/**
* @internal
* Centralized state management for the videoconference component
*/
error: boolean = false;
/**
* @internal
*/
errorMessage: string = '';
/**
* @internal
*/
showPrejoin: boolean = true;
componentState: VideoconferenceStateInfo = {
state: VideoconferenceState.INITIALIZING,
showPrejoin: true,
isRoomReady: false,
isConnected: false,
hasAudioDevices: false,
hasVideoDevices: false,
hasUserInitiatedJoin: false,
wasPrejoinShown: false,
isLoading: true,
error: {
hasError: false,
message: '',
tokenError: null
}
};
/**
* @internal
*/
isRoomReady: boolean = false;
/**
* @internal
*/
loading = true;
/**
* @internal
*/
_tokenError: any;
private prejoinSub: Subscription;
private tokenSub: Subscription;
private tokenErrorSub: Subscription;
private participantNameSub: Subscription;
private destroy$ = new Subject<void>();
private log: ILogger;
private latestParticipantName: string | undefined;
// Expose constants to template
get spinnerDiameter(): number {
return VideoconferenceComponent.SPINNER_DIAMETER;
}
/**
* @internal
* Updates the component state
*/
private updateComponentState(newState: Partial<VideoconferenceStateInfo>): void {
this.componentState = { ...this.componentState, ...newState };
this.log.d(`State updated to: ${this.componentState.state}`, this.componentState);
}
/**
* @internal
* Checks if user has initiated the join process
*/
private hasUserInitiatedJoin(): boolean {
return (
this.componentState.state === VideoconferenceState.JOINING ||
this.componentState.state === VideoconferenceState.READY_TO_CONNECT ||
this.componentState.state === VideoconferenceState.CONNECTED
);
}
/**
* @internal
*/
@ -395,17 +685,27 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
private deviceSrv: DeviceService,
private openviduService: OpenViduService,
private actionService: ActionService,
private libService: OpenViduComponentsConfigService
private libService: OpenViduComponentsConfigService,
private templateManagerService: TemplateManagerService
) {
this.log = this.loggerSrv.get('VideoconferenceComponent');
// Initialize state
this.updateComponentState({
state: VideoconferenceState.INITIALIZING,
showPrejoin: true,
isRoomReady: false,
wasPrejoinShown: false,
isLoading: true,
error: { hasError: false }
});
this.subscribeToVideconferenceDirectives();
}
ngOnDestroy() {
if (this.prejoinSub) this.prejoinSub.unsubscribe();
if (this.participantNameSub) this.participantNameSub.unsubscribe();
if (this.tokenSub) this.tokenSub.unsubscribe();
if (this.tokenErrorSub) this.tokenErrorSub.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
this.deviceSrv.clear();
}
@ -413,113 +713,204 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal
*/
ngAfterViewInit() {
//Add material icons to the page
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=background_replace,keep_off';
link.rel = 'stylesheet';
document.head.appendChild(link);
if (this.externalToolbar) {
this.log.d('Setting EXTERNAL TOOLBAR');
this.openviduAngularToolbarTemplate = this.externalToolbar.template;
} else {
this.log.d('Setting DEFAULT TOOLBAR');
if (this.externalToolbarAdditionalButtons) {
this.log.d('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS');
this.openviduAngularToolbarAdditionalButtonsTemplate = this.externalToolbarAdditionalButtons.template;
}
if (this.externalToolbarAdditionalPanelButtons) {
this.log.d('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS');
this.openviduAngularToolbarAdditionalPanelButtonsTemplate = this.externalToolbarAdditionalPanelButtons.template;
}
this.openviduAngularToolbarTemplate = this.defaultToolbarTemplate;
}
if (this.externalPanel) {
this.log.d('Setting EXTERNAL PANEL');
this.openviduAngularPanelTemplate = this.externalPanel.template;
} else {
this.log.d('Setting DEFAULT PANEL');
if (this.externalParticipantsPanel) {
this.openviduAngularParticipantsPanelTemplate = this.externalParticipantsPanel.template;
this.log.d('Setting EXTERNAL PARTICIPANTS PANEL');
} else {
this.log.d('Setting DEFAULT PARTICIPANTS PANEL');
if (this.externalParticipantPanelItem) {
this.log.d('Setting EXTERNAL P ITEM');
this.openviduAngularParticipantPanelItemTemplate = this.externalParticipantPanelItem.template;
} else {
if (this.externalParticipantPanelItemElements) {
this.log.d('Setting EXTERNAL PARTICIPANT PANEL ITEM ELEMENT');
this.openviduAngularParticipantPanelItemElementsTemplate = this.externalParticipantPanelItemElements.template;
}
this.openviduAngularParticipantPanelItemTemplate = this.defaultParticipantPanelItemTemplate;
this.log.d('Setting DEFAULT P ITEM');
}
this.openviduAngularParticipantsPanelTemplate = this.defaultParticipantsPanelTemplate;
}
if (this.externalChatPanel) {
this.log.d('Setting EXTERNAL CHAT PANEL');
this.openviduAngularChatPanelTemplate = this.externalChatPanel.template;
} else {
this.log.d('Setting DEFAULT CHAT PANEL');
this.openviduAngularChatPanelTemplate = this.defaultChatPanelTemplate;
}
if (this.externalActivitiesPanel) {
this.log.d('Setting EXTERNAL ACTIVITIES PANEL');
this.openviduAngularActivitiesPanelTemplate = this.externalActivitiesPanel.template;
} else {
this.log.d('Setting DEFAULT ACTIVITIES PANEL');
this.openviduAngularActivitiesPanelTemplate = this.defaultActivitiesPanelTemplate;
}
if (this.externalAdditionalPanels) {
this.log.d('Setting EXTERNAL ADDITIONAL PANELS');
this.openviduAngularAdditionalPanelsTemplate = this.externalAdditionalPanels.template;
}
this.openviduAngularPanelTemplate = this.defaultPanelTemplate;
}
if (this.externalLayout) {
this.log.d('Setting EXTERNAL LAYOUT');
this.openviduAngularLayoutTemplate = this.externalLayout.template;
} else {
this.log.d('Setting DEAFULT LAYOUT');
if (this.externalStream) {
this.log.d('Setting EXTERNAL STREAM');
this.openviduAngularStreamTemplate = this.externalStream.template;
} else {
this.log.d('Setting DEFAULT STREAM');
this.openviduAngularStreamTemplate = this.defaultStreamTemplate;
}
this.openviduAngularLayoutTemplate = this.defaultLayoutTemplate;
}
this.deviceSrv.initializeDevices().then(() => (this.loading = false));
this.addMaterialIconsIfNeeded();
this.setupTemplates();
this.deviceSrv.initializeDevices().then(() => {
this.updateComponentState({
isLoading: false
});
});
}
/**
* @internal
*/
_onReadyToJoin() {
this.openviduService.initRoom();
const participantName = this.latestParticipantName;
if (participantName) this.onTokenRequested.emit(participantName);
// Emits onReadyToJoin event only if prejoin page has been shown
if (this.showPrejoin) this.onReadyToJoin.emit();
private addMaterialIconsIfNeeded(): void {
//Add material icons to the page if not already present
const existingLink = document.querySelector(VideoconferenceComponent.MATERIAL_ICONS_SELECTOR);
if (!existingLink) {
const link = document.createElement('link');
link.href = VideoconferenceComponent.MATERIAL_ICONS_URL;
link.rel = 'stylesheet';
document.head.appendChild(link);
}
}
/**
* @internal
*/
private setupTemplates(): void {
const externalDirectives: ExternalDirectives = {
toolbar: this.externalToolbar,
toolbarAdditionalButtons: this.externalToolbarAdditionalButtons,
toolbarAdditionalPanelButtons: this.externalToolbarAdditionalPanelButtons,
additionalPanels: this.externalAdditionalPanels,
panel: this.externalPanel,
chatPanel: this.externalChatPanel,
activitiesPanel: this.externalActivitiesPanel,
participantsPanel: this.externalParticipantsPanel,
participantPanelAfterLocalParticipant: this.externalParticipantPanelAfterLocalParticipant,
participantPanelItem: this.externalParticipantPanelItem,
participantPanelItemElements: this.externalParticipantPanelItemElements,
layout: this.externalLayout,
stream: this.externalStream,
preJoin: this.externalPreJoin,
layoutAdditionalElements: this.externalLayoutAdditionalElements
};
const defaultTemplates: DefaultTemplates = {
toolbar: this.defaultToolbarTemplate,
panel: this.defaultPanelTemplate,
chatPanel: this.defaultChatPanelTemplate,
participantsPanel: this.defaultParticipantsPanelTemplate,
activitiesPanel: this.defaultActivitiesPanelTemplate,
participantPanelItem: this.defaultParticipantPanelItemTemplate,
layout: this.defaultLayoutTemplate,
stream: this.defaultStreamTemplate
};
// Use the template manager service to set up all templates
this.templateConfig = this.templateManagerService.setupTemplates(externalDirectives, defaultTemplates);
// Apply the configuration to the component properties
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
const assignIfChanged = <K extends keyof this>(prop: K, value: this[K]) => {
if (this[prop] !== value) {
this[prop] = value;
}
};
assignIfChanged('openviduAngularToolbarTemplate', this.templateConfig.toolbarTemplate);
assignIfChanged('openviduAngularPanelTemplate', this.templateConfig.panelTemplate);
assignIfChanged('openviduAngularChatPanelTemplate', this.templateConfig.chatPanelTemplate);
assignIfChanged('openviduAngularParticipantsPanelTemplate', this.templateConfig.participantsPanelTemplate);
assignIfChanged('openviduAngularActivitiesPanelTemplate', this.templateConfig.activitiesPanelTemplate);
assignIfChanged('openviduAngularParticipantPanelItemTemplate', this.templateConfig.participantPanelItemTemplate);
assignIfChanged('openviduAngularLayoutTemplate', this.templateConfig.layoutTemplate);
assignIfChanged('openviduAngularStreamTemplate', this.templateConfig.streamTemplate);
// Optional templates
if (this.templateConfig.toolbarAdditionalButtonsTemplate) {
assignIfChanged('openviduAngularToolbarAdditionalButtonsTemplate', this.templateConfig.toolbarAdditionalButtonsTemplate);
}
if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) {
assignIfChanged(
'openviduAngularToolbarAdditionalPanelButtonsTemplate',
this.templateConfig.toolbarAdditionalPanelButtonsTemplate
);
}
if (this.templateConfig.additionalPanelsTemplate) {
assignIfChanged('openviduAngularAdditionalPanelsTemplate', this.templateConfig.additionalPanelsTemplate);
}
if (this.templateConfig.participantPanelAfterLocalParticipantTemplate) {
assignIfChanged(
'openviduAngularParticipantPanelAfterLocalParticipantTemplate',
this.templateConfig.participantPanelAfterLocalParticipantTemplate
);
}
if (this.templateConfig.participantPanelItemElementsTemplate) {
assignIfChanged(
'openviduAngularParticipantPanelItemElementsTemplate',
this.templateConfig.participantPanelItemElementsTemplate
);
}
if (this.templateConfig.preJoinTemplate) {
assignIfChanged('openviduAngularPreJoinTemplate', this.templateConfig.preJoinTemplate);
}
if (this.templateConfig.layoutAdditionalElementsTemplate) {
assignIfChanged('ovLayoutAdditionalElementsTemplate', this.templateConfig.layoutAdditionalElementsTemplate);
}
}
/**
* @internal
* Handles the ready-to-join event, initializing the room and managing the prejoin flow.
* This method coordinates the transition from prejoin state to actual room joining.
*/
_onReadyToJoin(): void {
this.log.d('Ready to join - initializing room and handling prejoin flow');
try {
// Mark that user has initiated the join process
this.updateComponentState({
state: VideoconferenceState.JOINING,
wasPrejoinShown: this.componentState.showPrejoin
});
// Always initialize the room when ready to join
this.openviduService.initRoom();
// Get the most current participant name from the service
// This ensures we have the latest value after any batch updates
const participantName = this.libService.getCurrentParticipantName() || this.latestParticipantName;
if (this.componentState.isRoomReady) {
// Room is ready, hide prejoin and proceed
this.log.d('Room is ready, proceeding to join');
this.updateComponentState({
state: VideoconferenceState.READY_TO_CONNECT,
showPrejoin: false
});
} else {
// Room not ready, request token if we have a participant name
if (participantName) {
this.log.d(`Requesting token for participant: ${participantName}`);
this.onTokenRequested.emit(participantName);
} else {
this.log.w('No participant name available when requesting token');
// Wait a bit and try again in case name is still propagating
setTimeout(() => {
const retryName = this.libService.getCurrentParticipantName() || this.latestParticipantName;
if (retryName) {
this.log.d(`Retrying token request for participant: ${retryName}`);
this.onTokenRequested.emit(retryName);
} else {
this.log.e('Still no participant name available after retry');
}
}, 10);
}
}
// Emit onReadyToJoin event only if prejoin page was actually shown
// This ensures the event semantics are correct
if (this.componentState.wasPrejoinShown) {
this.log.d('Emitting onReadyToJoin event (prejoin was shown)');
this.onReadyToJoin.emit();
}
} catch (error) {
this.log.e('Error during ready to join process', error);
this.updateComponentState({
state: VideoconferenceState.ERROR,
error: {
hasError: true,
message: 'Error during ready to join process'
}
});
}
}
/**
* @internal
*/
_onParticipantLeft(event: ParticipantLeftEvent) {
this.isRoomReady = false;
// Reset to disconnected state to allow prejoin to show again if needed
this.updateComponentState({
state: VideoconferenceState.DISCONNECTED,
isRoomReady: false,
showPrejoin: this.libService.showPrejoin()
});
this.onParticipantLeft.emit(event);
}
private subscribeToVideconferenceDirectives() {
this.tokenSub = this.libService.token$.pipe(skip(1)).subscribe((token: string) => {
this.libService.token$.pipe(skip(1), takeUntil(this.destroy$)).subscribe((token: string) => {
try {
if (!token) {
this.log.e('Token is empty');
@ -529,27 +920,61 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
const livekitUrl = this.libService.getLivekitUrl();
this.openviduService.initializeAndSetToken(token, livekitUrl);
this.log.d('Token has been successfully set. Room is ready to join');
this.isRoomReady = true;
this.showPrejoin = false;
// Only update showPrejoin if user hasn't initiated join process yet
// This prevents prejoin from showing again after user clicked join
if (!this.hasUserInitiatedJoin()) {
this.updateComponentState({
state: VideoconferenceState.PREJOIN_SHOWN,
isRoomReady: true,
showPrejoin: this.libService.showPrejoin()
});
} else {
// User has initiated join, proceed to hide prejoin and continue
this.log.d('User has initiated join, hiding prejoin and proceeding');
this.updateComponentState({
state: VideoconferenceState.READY_TO_CONNECT,
isRoomReady: true,
showPrejoin: false
});
}
} catch (error) {
this.log.e('Error trying to set token', error);
this._tokenError = error;
this.updateComponentState({
state: VideoconferenceState.ERROR,
error: {
hasError: true,
message: 'Error setting token',
tokenError: error
}
});
}
});
this.tokenErrorSub = this.libService.tokenError$.subscribe((error: any) => {
this.libService.tokenError$.pipe(takeUntil(this.destroy$)).subscribe((error: any) => {
if (!error) return;
this.log.e('Token error received', error);
this._tokenError = error;
this.updateComponentState({
state: VideoconferenceState.ERROR,
error: {
hasError: true,
message: 'Token error',
tokenError: error
}
});
if (!this.showPrejoin) {
if (!this.componentState.showPrejoin) {
this.actionService.openDialog(error.name, error.message, false);
}
});
this.prejoinSub = this.libService.prejoin$.subscribe((value: boolean) => {
this.showPrejoin = value;
if (!this.showPrejoin) {
this.libService.prejoin$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.updateComponentState({
showPrejoin: value
});
if (!value) {
// Emit token ready if the prejoin page won't be shown
// Ensure we have a participant name before proceeding with the join
@ -560,10 +985,11 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
this._onReadyToJoin();
} else {
// No name yet - set up a one-time subscription to wait for it
const waitForNameSub = this.libService.participantName$
this.libService.participantName$
.pipe(
filter((name) => !!name),
take(1)
take(1),
takeUntil(this.destroy$)
)
.subscribe(() => {
// Now we have the name in latestParticipantName
@ -573,24 +999,35 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
setTimeout(() => {
if (!this.latestParticipantName) {
this.log.w('No participant name received after timeout, proceeding anyway');
waitForNameSub.unsubscribe();
const storedName = this.storageSrv.getParticipantName();
if (storedName) {
this.latestParticipantName = storedName;
this.libService.setParticipantName(storedName);
this.libService.updateGeneralConfig({ participantName: storedName });
}
this._onReadyToJoin();
}
}, 1000);
}, VideoconferenceComponent.PARTICIPANT_NAME_TIMEOUT_MS);
}
}
// this.cd.markForCheck();
});
this.participantNameSub = this.libService.participantName$.subscribe((name: string) => {
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
if (name) {
this.latestParticipantName = name;
this.storageSrv.setParticipantName(name);
// If we're waiting for a participant name to proceed with joining, do it now
if (
this.componentState.state === VideoconferenceState.JOINING &&
this.componentState.isRoomReady &&
!this.componentState.showPrejoin
) {
this.log.d('Participant name received, proceeding to join');
this.updateComponentState({
state: VideoconferenceState.READY_TO_CONNECT,
showPrejoin: false
});
}
}
});
}

View File

@ -49,9 +49,7 @@ export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit,
}
update(value: boolean) {
if (this.libService.showRecordingActivity() !== value) {
this.libService.setRecordingActivity(value);
}
this.libService.updateRecordingActivityConfig({ enabled: value });
}
}
@ -103,8 +101,6 @@ export class ActivitiesPanelBroadcastingActivityDirective implements AfterViewIn
}
update(value: boolean) {
if (this.libService.showBroadcastingActivity() !== value) {
this.libService.setBroadcastingActivity(value);
}
this.libService.setBroadcastingActivity(value);
}
}

View File

@ -16,15 +16,17 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
standalone: false
})
export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingsList(value: RecordingInfo[]) {
this.recordingsValue = value;
this.update(this.recordingsValue);
}
recordingsValue: RecordingInfo [] = [];
recordingsValue: RecordingInfo[] = [];
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.recordingsValue);
@ -38,9 +40,7 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
}
update(value: RecordingInfo[]) {
if (this.libService.getAdminRecordingsList() !== value) {
this.libService.setAdminRecordingsList(value);
}
this.libService.updateAdminConfig({ recordingsList: value });
}
}
@ -58,15 +58,17 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
standalone: false
})
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: any) {
@Input() set navbarTitle(value: string) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
}
navbarTitleValue: any = null;
navbarTitleValue: string = 'OpenVidu Dashboard';
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
@ -75,18 +77,15 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
this.clear();
}
clear() {
this.navbarTitleValue = null;
this.navbarTitleValue = 'OpenVidu Dashboard';
this.update(null);
}
update(value: any) {
if (this.libService.getAdminDashboardTitle() !== value) {
this.libService.setAdminDashboardTitle(value);
}
this.libService.updateAdminConfig({ dashboardTitle: value });
}
}
/**
* The **navbarTitle** directive allows customize the title of the navbar in {@link AdminLoginComponent}.
*
@ -101,7 +100,6 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
standalone: false
})
export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: any) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
@ -109,7 +107,10 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
navbarTitleValue: any = null;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
@ -123,14 +124,10 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
}
update(value: any) {
if (this.libService.getAdminLoginTitle() !== value) {
this.libService.setAdminLoginTitle(value);
}
this.libService.updateAdminConfig({ loginTitle: value });
}
}
/**
* The **error** directive allows show the authentication error in {@link AdminLoginComponent}.
*
@ -140,12 +137,11 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
* <ov-admin-login [error]="error"></ov-admin-login>
*
*/
@Directive({
@Directive({
selector: 'ov-admin-login[error]',
standalone: false
})
export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
@Input() set error(value: any) {
this.errorValue = value;
this.update(this.errorValue);
@ -153,7 +149,10 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
errorValue: any = null;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.errorValue);
@ -167,9 +166,6 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
}
update(value: any) {
if (this.libService.getAdminLoginError() !== value) {
this.libService.setAdminLoginError(value);
}
this.libService.updateAdminConfig({ loginError: value });
}
}

View File

@ -1,16 +1,23 @@
import { NgModule } from '@angular/core';
import { ActivitiesPanelBroadcastingActivityDirective, ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive';
import {
AdminLoginErrorDirective,
AdminDashboardRecordingsListDirective,
AdminLoginTitleDirective,
AdminDashboardTitleDirective
AdminDashboardTitleDirective,
AdminLoginErrorDirective,
AdminLoginTitleDirective
} from './admin.directive';
import {
LayoutRemoteParticipantsDirective,
FallbackLogoDirective,
LayoutRemoteParticipantsDirective,
PrejoinDisplayParticipantName,
ToolbarBrandingLogoDirective,
PrejoinDisplayParticipantName
ToolbarViewRecordingsButtonDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
} from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import {
@ -20,21 +27,21 @@ import {
} from './stream.directive';
import {
ToolbarActivitiesPanelButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarBackgroundEffectsButtonDirective,
ToolbarBroadcastingButtonDirective,
ToolbarCameraButtonDirective,
// ToolbarCaptionsButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarDisplayLogoDirective,
ToolbarDisplayRoomNameDirective,
ToolbarFullscreenButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarMicrophoneButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarScreenshareButtonDirective,
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarCameraButtonDirective,
ToolbarMicrophoneButtonDirective
ToolbarSettingsButtonDirective
} from './toolbar.directive';
import {
AudioEnabledDirective,
@ -47,6 +54,7 @@ import {
ParticipantNameDirective,
PrejoinDirective,
RecordingStreamBaseUrlDirective,
ShowDisconnectionDialogDirective,
TokenDirective,
TokenErrorDirective,
VideoEnabledDirective
@ -64,7 +72,10 @@ const directives = [
PrejoinDirective,
PrejoinDisplayParticipantName,
VideoEnabledDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
AudioEnabledDirective,
ShowDisconnectionDialogDirective,
RecordingStreamBaseUrlDirective,
ToolbarCameraButtonDirective,
ToolbarMicrophoneButtonDirective,
@ -82,6 +93,7 @@ const directives = [
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarViewRecordingsButtonDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective,
@ -95,7 +107,11 @@ const directives = [
AdminLoginTitleDirective,
AdminLoginErrorDirective,
AdminDashboardTitleDirective,
LayoutRemoteParticipantsDirective
LayoutRemoteParticipantsDirective,
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
];
@NgModule({

View File

@ -122,7 +122,7 @@ export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy {
}
private update(value: string) {
this.libService.setBrandingLogo(value);
this.libService.updateToolbarConfig({ brandingLogo: value });
}
}
@ -158,6 +158,369 @@ export class PrejoinDisplayParticipantName implements OnDestroy {
}
private update(value: boolean) {
this.libService.setPrejoinDisplayParticipantName(value);
this.libService.updateGeneralConfig({ prejoinDisplayParticipantName: value });
}
}
/**
* @internal
*
* The **recordingActivityReadOnly** directive sets the recording activity panel to read-only mode.
* In this mode, users can only view recordings without the ability to start, stop, or delete them.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `false`
*
* @example
* <ov-videoconference [recordingActivityReadOnly]="true"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityReadOnly]',
standalone: false
})
export class RecordingActivityReadOnlyDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityReadOnly(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update(false);
}
/**
* @ignore
*/
update(value: boolean) {
this.libService.updateRecordingActivityConfig({ readOnly: value });
}
}
/**
*
* @internal
*
* The **recordingActivityShowControls** directive allows to show/hide specific recording controls (play, download, delete, externalView).
* You can pass an object with boolean properties to control which buttons are shown.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `{ play: true, download: true, delete: true, externalView: false }`
*
* @example
* <ov-videoconference [recordingActivityShowControls]="{ play: false, download: true, delete: false }"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityShowControls]',
standalone: false
})
export class RecordingActivityShowControlsDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityShowControls(value: { play: boolean; download: boolean; delete: boolean; externalView: boolean }) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update({ play: true, download: true, delete: true, externalView: false });
}
/**
* @ignore
*/
update(value: { play: boolean; download: boolean; delete: boolean; externalView: boolean }) {
this.libService.updateRecordingActivityConfig({ showControls: value });
}
}
/**
* @internal
* The **viewRecordingsButton** directive allows show/hide the view recordings toolbar button.
*
* Default: `false`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarViewRecordingsButton]="true"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [viewRecordingsButton]="true"></ov-toolbar>
*
* When the button is clicked, it will fire the `onViewRecordingsClicked` event.
*/
@Directive({
selector: 'ov-videoconference[toolbarViewRecordingsButton], ov-toolbar[viewRecordingsButton]',
standalone: false
})
export class ToolbarViewRecordingsButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarViewRecordingsButton(value: boolean) {
this.viewRecordingsValue = value;
this.update(this.viewRecordingsValue);
}
/**
* @ignore
*/
@Input() set viewRecordingsButton(value: boolean) {
this.viewRecordingsValue = value;
this.update(this.viewRecordingsValue);
}
private viewRecordingsValue: boolean = false;
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.viewRecordingsValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.viewRecordingsValue = false;
this.update(true);
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ viewRecordings: value });
}
}
/**
* @internal
*
* The **recordingActivityStartStopRecordingButton** directive allows to show or hide the start/stop recording buttons in recording activity.
*
* Default: `true`
*
* It is only available for {@link VideoconferenceComponent}.
*
* @example
* <ov-videoconference [recordingActivityStartStopRecordingButton]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityStartStopRecordingButton]',
standalone: false
})
export class StartStopRecordingButtonsDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityStartStopRecordingButton(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.update(true);
}
private update(value: boolean) {
this.libService.updateRecordingActivityConfig({ startStopButton: value });
}
}
/**
* @internal
* The **recordingActivityViewRecordingsButton** directive allows to show/hide the view recordings button in the recording activity panel.
*
* Default: `false`
*
* Can be used in {@link VideoconferenceComponent}.
*
* @example
* <ov-videoconference [recordingActivityViewRecordingsButton]="true"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityViewRecordingsButton]',
standalone: false
})
export class RecordingActivityViewRecordingsButtonDirective implements AfterViewInit, OnDestroy {
@Input() set recordingActivityViewRecordingsButton(value: boolean) {
this._value = value;
this.update(this._value);
}
private _value: boolean = false;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this._value);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this._value = false;
this.update(this._value);
}
private update(value: boolean) {
this.libService.updateRecordingActivityConfig({ viewRecordingsButton: value });
}
}
/**
* @internal
* The **recordingActivityShowRecordingsList** directive allows to show or hide the recordings list in the recording activity panel.
*
* Default: `true`
*
* Can be used in {@link VideoconferenceComponent}.
*
* @example
* <ov-videoconference [recordingActivityShowRecordingsList]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityShowRecordingsList]',
standalone: false
})
export class RecordingActivityShowRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingActivityShowRecordingsList(value: boolean) {
this._value = value;
this.update(this._value);
}
private _value: boolean = true;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this._value);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this._value = true;
this.update(this._value);
}
private update(value: boolean) {
this.libService.updateRecordingActivityConfig({ showRecordingsList: value });
}
}
/**
* @internal
* The **toolbarRoomName** directive allows to display a specific room name in the toolbar.
* If the room name is not set, it will display the room ID instead.
*
* Can be used in {@link ToolbarComponent}.
*
* @example
* <ov-videoconference [toolbarRoomName]="roomName"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[toolbarRoomName], ov-toolbar[roomName]',
standalone: false
})
export class ToolbarRoomNameDirective implements AfterViewInit, OnDestroy {
@Input() set toolbarRoomName(value: string | undefined) {
this._roomName = value;
this.updateRoomName();
}
@Input() set roomName(value: string | undefined) {
this._roomName = value;
this.updateRoomName();
}
private _roomName?: string;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.updateRoomName();
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this._roomName = undefined;
this.updateRoomName();
}
private updateRoomName() {
this.libService.updateToolbarConfig({ roomName: this._roomName || '' });
}
}

View File

@ -32,7 +32,10 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
muteValue: boolean = true;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.muteValue);
@ -46,8 +49,6 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
}
update(value: boolean) {
if (this.libService.showParticipantItemMuteButton() !== value) {
this.libService.setParticipantItemMuteButton(value);
}
this.libService.updateStreamConfig({ participantItemMuteButton: value });
}
}
}

View File

@ -46,9 +46,7 @@ export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnD
}
update(value: boolean) {
if (this.libService.isParticipantNameDisplayed() !== value) {
this.libService.setDisplayParticipantName(value);
}
this.libService.updateStreamConfig({ displayParticipantName: value });
}
clear() {
@ -100,9 +98,7 @@ export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDe
}
update(value: boolean) {
if (this.libService.isAudioDetectionDisplayed() !== value) {
this.libService.setDisplayAudioDetection(value);
}
this.libService.updateStreamConfig({ displayAudioDetection: value });
}
clear() {
this.update(true);
@ -154,9 +150,7 @@ export class StreamVideoControlsDirective implements AfterViewInit, OnDestroy {
}
update(value: boolean) {
if (this.libService.showStreamVideoControls() !== value) {
this.libService.setStreamVideoControls(value);
}
this.libService.updateStreamConfig({ videoControls: value });
}
clear() {

View File

@ -62,9 +62,7 @@ export class ToolbarCameraButtonDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
if (this.libService.showCameraButton() !== value) {
this.libService.setCameraButton(value);
}
this.libService.updateToolbarConfig({ camera: value });
}
}
@ -128,9 +126,7 @@ export class ToolbarMicrophoneButtonDirective implements AfterViewInit, OnDestro
}
private update(value: boolean) {
if (this.libService.showMicrophoneButton() !== value) {
this.libService.setMicrophoneButton(value);
}
this.libService.updateToolbarConfig({ microphone: value });
}
}
@ -194,9 +190,7 @@ export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestr
}
private update(value: boolean) {
if (this.libService.showScreenshareButton() !== value) {
this.libService.setScreenshareButton(value);
}
this.libService.updateToolbarConfig({ screenshare: value });
}
}
@ -257,9 +251,7 @@ export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
if (this.libService.showRecordingButton() !== value) {
this.libService.setRecordingButton(value);
}
this.libService.updateToolbarConfig({ recording: value });
}
}
@ -321,9 +313,7 @@ export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDest
}
private update(value: boolean) {
if (this.libService.showBroadcastingButton() !== value) {
this.libService.setBroadcastingButton(value);
}
this.libService.setBroadcastingButton(value);
}
}
@ -384,9 +374,7 @@ export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestro
}
private update(value: boolean) {
if (this.libService.showFullscreenButton() !== value) {
this.libService.setFullscreenButton(value);
}
this.libService.updateToolbarConfig({ fullscreen: value });
}
}
@ -447,9 +435,7 @@ export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, O
}
private update(value: boolean) {
if (this.libService.showBackgroundEffectsButton() !== value) {
this.libService.setBackgroundEffectsButton(value);
}
this.libService.updateToolbarConfig({ backgroundEffects: value });
}
}
@ -569,9 +555,7 @@ export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
if (this.libService.showToolbarSettingsButton() !== value) {
this.libService.setToolbarSettingsButton(value);
}
this.libService.updateToolbarConfig({ settings: value });
}
}
@ -633,9 +617,7 @@ export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
if (this.libService.showLeaveButton() !== value) {
this.libService.setLeaveButton(value);
}
this.libService.updateToolbarConfig({ leave: value });
}
}
@ -698,9 +680,7 @@ export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, O
}
private update(value: boolean) {
if (this.libService.showParticipantsPanelButton() !== value) {
this.libService.setParticipantsPanelButton(value);
}
this.libService.updateToolbarConfig({ participantsPanel: value });
}
}
@ -761,9 +741,7 @@ export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
if (this.libService.showChatPanelButton() !== value) {
this.libService.setChatPanelButton(value);
}
this.libService.updateToolbarConfig({ chatPanel: value });
}
}
@ -824,9 +802,7 @@ export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnD
}
private update(value: boolean) {
if (this.libService.showActivitiesPanelButton() !== value) {
this.libService.setActivitiesPanelButton(value);
}
this.libService.updateToolbarConfig({ activitiesPanel: value });
}
}
@ -888,9 +864,7 @@ export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
if (this.libService.showRoomName() !== value) {
this.libService.setDisplayRoomName(value);
}
this.libService.updateToolbarConfig({ displayRoomName: value });
}
}
@ -952,9 +926,7 @@ export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
if (this.libService.showLogo() !== value) {
this.libService.setDisplayLogo(value);
}
this.libService.updateToolbarConfig({ displayLogo: value });
}
}
@ -1009,8 +981,6 @@ export class ToolbarAdditionalButtonsPossitionDirective implements AfterViewInit
}
private update(value: ToolbarAdditionalButtonsPosition) {
if (this.libService.getToolbarAdditionalButtonsPosition() !== value) {
this.libService.setToolbarAdditionalButtonsPosition(value);
}
this.libService.updateToolbarConfig({ additionalButtonsPosition: value });
}
}

View File

@ -55,7 +55,7 @@ export class LivekitUrlDirective implements OnDestroy {
* @ignore
*/
update(value: string) {
this.libService.setLivekitUrl(value);
this.libService.updateGeneralConfig({ livekitUrl: value });
}
}
@ -108,7 +108,7 @@ export class TokenDirective implements OnDestroy {
* @ignore
*/
update(value: string) {
this.libService.setToken(value);
this.libService.updateGeneralConfig({ token: value });
}
}
@ -160,7 +160,7 @@ export class TokenErrorDirective implements OnDestroy {
* @ignore
*/
update(value: any) {
this.libService.setTokenError(value);
this.libService.updateGeneralConfig({ tokenError: value });
}
}
@ -212,9 +212,7 @@ export class MinimalDirective implements OnDestroy {
* @ignore
*/
update(value: boolean) {
if (this.libService.isMinimal() !== value) {
this.libService.setMinimal(value);
}
this.libService.updateGeneralConfig({ minimal: value });
}
}
@ -225,7 +223,7 @@ export class MinimalDirective implements OnDestroy {
*
* **Default:** English `en`
*
* **Available:**
* **Available Langs:**
*
* * English: `en`
* * Spanish: `es`
@ -538,7 +536,7 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
* @ignore
*/
update(value: string) {
if (value) this.libService.setParticipantName(value);
if (value) this.libService.updateGeneralConfig({ participantName: value });
}
}
@ -590,9 +588,7 @@ export class PrejoinDirective implements OnDestroy {
* @ignore
*/
update(value: boolean) {
if (this.libService.isPrejoin() !== value) {
this.libService.setPrejoin(value);
}
this.libService.updateGeneralConfig({ prejoin: value });
}
}
@ -663,7 +659,7 @@ export class VideoEnabledDirective implements OnDestroy {
// Ensure libService state is consistent with the final enabled state
if (this.libService.isVideoEnabled() !== finalEnabledState) {
this.libService.setVideoEnabled(finalEnabledState);
this.libService.updateStreamConfig({ videoEnabled: finalEnabledState });
}
}
}
@ -731,7 +727,61 @@ export class AudioEnabledDirective implements OnDestroy {
this.storageService.setMicrophoneEnabled(finalEnabledState);
if (this.libService.isAudioEnabled() !== enabled) {
this.libService.setAudioEnabled(enabled);
this.libService.updateStreamConfig({ audioEnabled: enabled });
}
}
}
/**
* The **showDisconnectionDialog** directive allows to show/hide the disconnection dialog when the local participant is disconnected from the room.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `true`
*
* @example
* <ov-videoconference [showDisconnectionDialog]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[showDisconnectionDialog]',
standalone: false
})
export class ShowDisconnectionDialogDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set showDisconnectionDialog(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update(true);
}
/**
* @ignore
*/
update(value: boolean) {
if (this.libService.getShowDisconnectionDialog() !== value) {
this.libService.updateGeneralConfig({ showDisconnectionDialog: value });
}
}
}
@ -803,6 +853,6 @@ export class RecordingStreamBaseUrlDirective implements AfterViewInit, OnDestroy
* @ignore
*/
update(value: string) {
if (value) this.libService.setRecordingStreamBaseUrl(value);
if (value) this.libService.updateGeneralConfig({ recordingStreamBaseUrl: value });
}
}

View File

@ -0,0 +1,284 @@
/**
* The ***ovPreJoin** directive empowers you to substitute the default pre-join component template with a custom one.
* This directive allows you to create a completely custom pre-join experience while maintaining the core functionality.
*
* In the example below, we demonstrate how to replace the pre-join template with a custom one that includes
* device selection and a custom join button.
*
* <!--ovPreJoin-start-tutorial-->
* ```typescript
* import { HttpClient } from '@angular/common/http';
* import { Component } from '@angular/core';
* import { lastValueFrom } from 'rxjs';
* import { FormsModule } from '@angular/forms';
*
* import {
* DeviceService,
* ParticipantService,
* OpenViduComponentsModule,
* } from 'openvidu-components-angular';
*
* @Component({
* selector: 'app-root',
* template: `
* <ov-videoconference
* [token]="token"
* [livekitUrl]="LIVEKIT_URL"
* (onTokenRequested)="onTokenRequested($event)"
* (onReadyToJoin)="onReadyToJoin()"
* >
* <!-- Custom Pre-Join Component -->
* <div *ovPreJoin class="custom-prejoin">
* <h2>Join Meeting</h2>
* <div class="prejoin-form">
* <input
* type="text"
* placeholder="Enter your name"
* [(ngModel)]="participantName"
* class="name-input"
* />
* <button
* (click)="joinMeeting()"
* [disabled]="!participantName"
* class="join-button"
* >
* Join Meeting
* </button>
* </div>
* </div>
* </ov-videoconference>
* `,
* styles: `
* .custom-prejoin {
* display: flex;
* flex-direction: column;
* align-items: center;
* justify-content: center;
* height: 100vh;
* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
* color: white;
* }
* .prejoin-form {
* display: flex;
* flex-direction: column;
* gap: 20px;
* align-items: center;
* }
* .name-input {
* padding: 12px;
* border: none;
* border-radius: 8px;
* font-size: 16px;
* min-width: 250px;
* }
* .join-button {
* padding: 12px 24px;
* background: #4CAF50;
* color: white;
* border: none;
* border-radius: 8px;
* font-size: 16px;
* cursor: pointer;
* transition: background 0.3s;
* }
* .join-button:hover:not(:disabled) {
* background: #45a049;
* }
* .join-button:disabled {
* background: #cccccc;
* cursor: not-allowed;
* }
* `,
* standalone: true,
* imports: [OpenViduComponentsModule, FormsModule],
* })
* export class AppComponent {
* // For local development, leave these variables empty
* // For production, configure them with correct URLs depending on your deployment
* APPLICATION_SERVER_URL = '';
* LIVEKIT_URL = '';
*
* // Define the name of the room and initialize the token variable
* roomName = 'custom-prejoin';
* token!: string;
* participantName: string = '';
*
* constructor(
* private httpClient: HttpClient,
* private deviceService: DeviceService,
* private participantService: ParticipantService
* ) {
* this.configureUrls();
* }
*
* private configureUrls() {
* // If APPLICATION_SERVER_URL is not configured, use default value from local development
* if (!this.APPLICATION_SERVER_URL) {
* if (window.location.hostname === 'localhost') {
* this.APPLICATION_SERVER_URL = 'http://localhost:6080/';
* } else {
* this.APPLICATION_SERVER_URL =
* 'https://' + window.location.hostname + ':6443/';
* }
* }
*
* // If LIVEKIT_URL is not configured, use default value from local development
* if (!this.LIVEKIT_URL) {
* if (window.location.hostname === 'localhost') {
* this.LIVEKIT_URL = 'ws://localhost:7880/';
* } else {
* this.LIVEKIT_URL = 'wss://' + window.location.hostname + ':7443/';
* }
* }
* }
*
* // Function to request a token when a participant joins the room
* async onTokenRequested(participantName: string) {
* const { token } = await this.getToken(this.roomName, participantName);
* this.token = token;
* }
*
* // Function called when ready to join
* onReadyToJoin() {
* console.log('Ready to join the meeting');
* }
*
* // Function to join the meeting
* async joinMeeting() {
* if (this.participantName.trim()) {
* // Request token with the participant name
* await this.onTokenRequested(this.participantName);
* }
* }
*
* // Function to get a token from the server
* getToken(roomName: string, participantName: string): Promise<any> {
* try {
* // Send a POST request to the server to obtain a token
* return lastValueFrom(
* this.httpClient.post<any>(this.APPLICATION_SERVER_URL + 'token', {
* roomName,
* participantName,
* })
* );
* } catch (error: any) {
* // Handle errors, e.g., if the server is not reachable
* if (error.status === 404) {
* throw {
* status: error.status,
* message:
* 'Cannot connect with the backend. ' + error.url + ' not found',
* };
* }
* throw error;
* }
* }
* }
*
* ```
* <!--ovPreJoin-end-tutorial-->
*
* For a detailed tutorial on customizing the pre-join component, please visit [this link](https://openvidu.io/latest/docs/tutorials/angular-components/openvidu-custom-prejoin/).
*/
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[ovPreJoin]',
standalone: false
})
export class PreJoinDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}
/**
* The ***ovParticipantPanelAfterLocalParticipant** directive allows you to inject custom HTML or Angular templates
* immediately after the local participant item in the participant panel.
* This enables you to extend the participant panel with additional controls, information, or UI elements.
*
* Usage example:
* ```html
* <ov-participant-panel>
* <ng-container *ovParticipantPanelAfterLocalParticipant>
* <div class="custom-content">
* <!-- Your custom HTML here -->
* <span>Custom content after local participant</span>
* </div>
* </ng-container>
* </ov-participant-panel>
* ```
*/
@Directive({
selector: '[ovParticipantPanelAfterLocalParticipant]',
standalone: false
})
export class ParticipantPanelAfterLocalParticipantDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}
/**
* The ***ovLayoutAdditionalElements** directive allows you to inject custom HTML or Angular templates
* as additional layout elements within the videoconference UI.
* This enables you to extend the layout with extra controls, banners, or any custom UI.
*
* Usage example:
* ```html
* <ov-videoconference>
* <ng-container *ovLayoutAdditionalElements>
* <div class="my-custom-layout-element">
* <!-- Your custom HTML here -->
* <span>Extra layout element</span>
* </div>
* </ng-container>
* </ov-videoconference>
* ```
*/
@Directive({
selector: '[ovLayoutAdditionalElements]',
standalone: false
})
export class LayoutAdditionalElementsDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}
/**
* The ***ovParticipantPanelParticipantBadge** directive allows you to inject custom badges or indicators
* in the participant panel.
* This enables you to add role indicators, status badges, or other visual elements.
*
* Usage example:
* ```html
* <ov-participants-panel>
* <div *ovParticipantPanelItem="let participant">
* <ov-participant-panel-item [participant]="participant">
* <!-- Custom badge for local participant only -->
* <ng-container *ovParticipantPanelParticipantBadge>
* <span class="moderator-badge">
* <mat-icon>admin_panel_settings</mat-icon>
* Moderator
* </span>
* </ng-container>
* </ov-participant-panel-item>
* </div>
* </ov-participants-panel>
* ```
*/
@Directive({
selector: '[ovParticipantPanelParticipantBadge]',
standalone: false
})
export class ParticipantPanelParticipantBadgeDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}

View File

@ -14,6 +14,12 @@ import {
ActivitiesPanelDirective,
BackgroundEffectsPanelDirective
} from './openvidu-components-angular.directive';
import {
LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective,
ParticipantPanelParticipantBadgeDirective,
PreJoinDirective
} from './internals.directive';
@NgModule({
declarations: [
@ -29,6 +35,10 @@ import {
ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective,
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective
],
exports: [
@ -44,6 +54,10 @@ import {
ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective,
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective
]
})

View File

@ -1814,3 +1814,4 @@ export class StreamDirective {
public container: ViewContainerRef
) {}
}

View File

@ -33,7 +33,7 @@
"AUDIO_DEVICE": "音频设备",
"NO_VIDEO_DEVICE": "未找到视频设备",
"NO_AUDIO_DEVICE": "未找到音频设备",
"JOIN": "加入会话",
"JOIN": "加入房间",
"PREPARING": "筹备会议"
},
"TOOLBAR": {
@ -55,7 +55,9 @@
"LEAVE": "离开会议",
"PARTICIPANTS": "参与者",
"CHAT": "聊天",
"ACTIVITIES": "活动"
"ACTIVITIES": "活动",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"VIEW_RECORDINGS": "查看录像"
},
"STREAM": {
"SETTINGS": "设置",
@ -89,7 +91,9 @@
"MICROPHONE": "麦克风",
"SCREEN": "屏幕",
"NO_STREAMS": "无",
"YOU": "你"
"YOU": "你",
"MUTE": "静音",
"UNMUTE": "取消静音"
},
"SETTINGS": {
"TITLE": "设置",
@ -100,7 +104,7 @@
"CAPTIONS": "字幕",
"DISABLED_AUDIO": "没有音频设备",
"DISABLED_VIDEO": "没有视频设备",
"CAPTIONS_LANG_TEXT": "选择会话参与者将使用的语言。字幕将以该语言显示。"
"CAPTIONS_LANG_TEXT": "选择房间参与者将使用的语言。字幕将以该语言显示。"
},
"BACKGROUND": {
"TITLE": "背景效果",
@ -114,6 +118,10 @@
"SUBTITLE": "为后人记录你的会议",
"CONTENT_TITLE": "记录你的视频通话",
"CONTENT_SUBTITLE": "当录音完成后,你将可以轻松地下载它",
"VIEW_ONLY_SUBTITLE": "查看和访问房间录音",
"VIEW_ONLY_CONTENT_TITLE": "视频通话录音",
"VIEW_ONLY_CONTENT_SUBTITLE": "在这里您可以访问所有可用的录音",
"WATCH": "观看",
"STARTING": "开始录音",
"STOPPING": "停止录制",
"IN_PROGRESS": "录音中",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "您确定要删除录音吗",
"DOWNLOAD": "下载",
"RECORDINGS": "录制",
"NO_MODERATOR": "只有主持人可以开始录音"
"NO_MODERATOR": "只有主持人可以开始录音",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"NO_RECORDINGS_AVAILABLE": "目前没有可用的录音",
"ERROR_STARTING": "开始录音时出错"
},
"STREAMING": {
"TITLE": "直播",
@ -139,9 +150,9 @@
}
},
"ERRORS": {
"SESSION": "连接到会话时有错误",
"SESSION": "连接到房间时有错误",
"CONNECTION": "连接丢失",
"RECONNECT": "试图重新连接到会话",
"RECONNECT": "试图重新连接到房间",
"DISCONNECT": "您已断开连接",
"NETWORK_DISCONNECT": "由于网络连接问题,您已断开连接",
"SIGNAL_CLOSE": "与服务器的连接意外关闭",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Audiogerät",
"NO_VIDEO_DEVICE": "Video-Gerät nicht gefunden",
"NO_AUDIO_DEVICE": "Audio-Gerät nicht gefunden",
"JOIN": "Sitzung beitreten",
"PREPARING": "Sitzung vorbereiten..."
"JOIN": "Raum beitreten",
"PREPARING": "Raum vorbereiten..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Stummschalten des Audios",
@ -52,10 +52,11 @@
"START_RECORDING": "Aufzeichnung starten",
"STOP_RECORDING": "Aufzeichnung stoppen",
"SETTINGS": "Einstellungen",
"LEAVE": "Die Sitzung verlassen",
"LEAVE": "Die Raum verlassen",
"PARTICIPANTS": "Teilnehmer",
"CHAT": "Chat",
"ACTIVITIES": "Aktivitäten"
"ACTIVITIES": "Aktivitäten",
"NO_TRACKS_PUBLISHED": "Teile Audio oder Video, um mit der Aufnahme zu beginnen."
},
"STREAM": {
"SETTINGS": "Einstellungen",
@ -77,7 +78,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Sie",
"SUBTITLE": "Nachrichten werden am Ende der Sitzung entfernt",
"SUBTITLE": "Nachrichten werden am Ende der Raum entfernt",
"PLACEHOLDER": "Eine Nachricht senden...",
"SEND": "Senden",
"MESSAGE_SENT_NOTIFICATION": "Nachricht gesendet",
@ -89,7 +90,9 @@
"MICROPHONE": "MIKROFON",
"SCREEN": "BILDSCHIRM",
"NO_STREAMS": "KEINE",
"YOU": "Sie"
"YOU": "Sie",
"MUTE": "Stummschalten",
"UNMUTE": "Stummschaltung aufheben"
},
"SETTINGS": {
"TITLE": "Einstellungen",
@ -100,7 +103,7 @@
"CAPTIONS": "Untertitel",
"DISABLED_AUDIO": "Audio deaktiviert",
"DISABLED_VIDEO": "Video deaktiviert",
"CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Sitzung verwenden. Die Untertitel werden in dieser Sprache angezeigt."
"CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Raum verwenden. Die Untertitel werden in dieser Sprache angezeigt."
},
"BACKGROUND": {
"TITLE": "Hintergrund-Effekte",
@ -124,7 +127,9 @@
"DELETE_QUESTION": "Möchten Sie die Aufzeichnung wirklich löschen?",
"DOWNLOAD": "Download",
"RECORDINGS": "AUFZEICHNUNGEN",
"NO_MODERATOR": "Nur der MODERATOR kann die Aufzeichnung starten"
"NO_MODERATOR": "Nur der MODERATOR kann die Aufzeichnung starten",
"NO_TRACKS_PUBLISHED": "Teile Audio oder Video, um mit der Aufnahme zu beginnen.",
"ERROR_STARTING": "Fehler beim Starten der Aufnahme"
},
"STREAMING": {
"TITLE": "Streaming",
@ -139,9 +144,9 @@
}
},
"ERRORS": {
"SESSION": "Es ist ein Fehler beim Verbinden mit der Sitzung aufgetreten",
"SESSION": "Es ist ein Fehler beim Verbinden mit der Raum aufgetreten",
"CONNECTION": "Verbindung verloren",
"RECONNECT": "Ich versuche, die Verbindung zur Sitzung wiederherzustellen...",
"RECONNECT": "Ich versuche, die Verbindung zur Raum wiederherzustellen...",
"DISCONNECT": "Sie wurden getrennt",
"NETWORK_DISCONNECT": "Sie wurden aufgrund eines Netzwerkproblems getrennt",
"SIGNAL_CLOSE": "Die Verbindung zum Server wurde unerwartet geschlossen",

View File

@ -30,8 +30,8 @@
"AUDIO_DEVICE": "Audio device",
"NO_VIDEO_DEVICE": "Video device not found",
"NO_AUDIO_DEVICE": "Audio device not found",
"JOIN": "Join session",
"PREPARING": "Preparing session..."
"JOIN": "Join room",
"PREPARING": "Preparing room..."
},
"ROOM": {
"JOINING": "Joining room..."
@ -52,10 +52,12 @@
"START_RECORDING": "Start recording",
"STOP_RECORDING": "Stop recording",
"SETTINGS": "Settings",
"LEAVE": "Leave the session",
"LEAVE": "Leave the room",
"PARTICIPANTS": "Participants",
"CHAT": "Chat",
"ACTIVITIES": "Activities"
"ACTIVITIES": "Activities",
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"VIEW_RECORDINGS": "View recordings"
},
"STREAM": {
"SETTINGS": "Settings",
@ -77,7 +79,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "You",
"SUBTITLE": "Messages will be removed at the end of the session",
"SUBTITLE": "Messages will be removed at the end of the room",
"PLACEHOLDER": "Send a message...",
"SEND": "Send",
"MESSAGE_SENT_NOTIFICATION": "message sent",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROPHONE",
"SCREEN": "SCREEN",
"NO_STREAMS": "NONE",
"YOU": "You"
"YOU": "You",
"MUTE": "Mute",
"UNMUTE": "Unmute"
},
"SETTINGS": {
"TITLE": "Settings",
@ -100,7 +104,7 @@
"CAPTIONS": "Captions",
"DISABLED_AUDIO": "Audio disabled",
"DISABLED_VIDEO": "Video disabled",
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the session will use. The captions will appear in that language."
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the room will use. The captions will appear in that language."
},
"BACKGROUND": {
"TITLE": "Background effects",
@ -114,6 +118,13 @@
"SUBTITLE": "Record your meeting for posterity",
"CONTENT_TITLE": "Record your video call",
"CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease",
"VIEW_ONLY_TITLE": "Available recordings",
"VIEW_ONLY_SUBTITLE": "View and access room recordings",
"VIEW_ONLY_CONTENT_TITLE": "Video call recordings",
"VIEW_ONLY_CONTENT_SUBTITLE": "Here you can access all available recordings",
"VIEW": "View",
"WATCH": "Watch",
"ACCESS": "Access",
"STARTING": "Starting recording",
"STOPPING": "Stopping recording",
"IN_PROGRESS": "Recording in progress ...",
@ -124,7 +135,11 @@
"DELETE_QUESTION": "Are you sure you want to delete the recording?",
"DOWNLOAD": "Download",
"RECORDINGS": "RECORDINGS",
"NO_MODERATOR": "Only the MODERATOR can start the recording"
"NO_MODERATOR": "Only the MODERATOR can start the recording",
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"NO_RECORDINGS_AVAILABLE": "No recordings available at this time",
"BROWSE_RECORDINGS": "Browse saved recordings",
"ERROR_STARTING": "Error starting recording"
},
"STREAMING": {
"TITLE": "Streaming",

View File

@ -33,7 +33,7 @@
"AUDIO_DEVICE": "Dispositivo de audio",
"NO_VIDEO_DEVICE": "Dispositivo de vídeo no encontrado",
"NO_AUDIO_DEVICE": "Dispositivo de audio no encontrado",
"PREPARING": "Preparando la session ...",
"PREPARING": "Preparando la sala ...",
"JOIN": "Unirme ahora"
},
"TOOLBAR": {
@ -55,7 +55,9 @@
"LEAVE": "Salir de la sala",
"PARTICIPANTS": "Participantes",
"CHAT": "Chat",
"ACTIVITIES": "Actividades"
"ACTIVITIES": "Actividades",
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"VIEW_RECORDINGS": "Ver grabaciones"
},
"STREAM": {
"SETTINGS": "Ajustes",
@ -89,7 +91,9 @@
"MICROPHONE": "MICRÓFONO",
"SCREEN": "PANTALLA",
"NO_STREAMS": "NINGUNO",
"YOU": "Tú"
"YOU": "Tú",
"MUTE": "Silenciar",
"UNMUTE": "Activar audio"
},
"SETTINGS": {
"TITLE": "Configuración",
@ -114,6 +118,10 @@
"SUBTITLE": "Graba tus llamadas para la posteridad",
"CONTENT_TITLE": "Graba tu video conferencia",
"CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad",
"VIEW_ONLY_SUBTITLE": "Visualiza y accede a las grabaciones de la sala",
"VIEW_ONLY_CONTENT_TITLE": "Grabaciones de la video conferencia",
"VIEW_ONLY_CONTENT_SUBTITLE": "Aquí puedes acceder a todas las grabaciones disponibles",
"WATCH": "Visualizar",
"STARTING": "Iniciando grabación...",
"STOPPING": "Parando grabación",
"IN_PROGRESS": "Grabación en curso",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "¿Estás seguro/a de que deseas borrar la grabación?",
"DOWNLOAD": "Descargar",
"RECORDINGS": "GRABACIONES",
"NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación"
"NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación",
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"NO_RECORDINGS_AVAILABLE": "No hay grabaciones disponibles en este momento",
"ERROR_STARTING": "Error iniciando la grabación"
},
"STREAMING": {
"TITLE": "Streaming",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Périphérique audio",
"NO_VIDEO_DEVICE": "Appareil vidéo introuvable",
"NO_AUDIO_DEVICE": "Appareil audio introuvable",
"JOIN": "Joindre une session",
"PREPARING": "Préparation de la session ..."
"JOIN": "Joindre une salle",
"PREPARING": "Préparation de la salle ..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Mettez votre audio en sourdine",
@ -52,10 +52,12 @@
"START_RECORDING": "démarrer l'enregistrement",
"STOP_RECORDING": "Arrêter l'enregistrement",
"SETTINGS": "Paramètres",
"LEAVE": "Quitter la session",
"LEAVE": "Quitter la salle",
"PARTICIPANTS": "Participants",
"CHAT": "Chat",
"ACTIVITES": "Activités"
"ACTIVITES": "Activités",
"NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement.",
"VIEW_RECORDINGS": "Voir les enregistrements"
},
"STREAM": {
"SETTINGS": "Paramètres",
@ -77,7 +79,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Vous",
"SUBTITLE": "Les messages seront supprimés à la fin de la session",
"SUBTITLE": "Les messages seront supprimés à la fin de la salle",
"PLACEHOLDER": "Envoyer un message...",
"SEND": "Envoyer",
"MESSAGE_SENT_NOTIFICATION": "message envoyé",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROPHONE",
"SCREEN": "ÉCRAN",
"NO_STREAMS": "PAS_DE_FLUX",
"YOU": "Vous"
"YOU": "Vous",
"MUTE": "Couper le son",
"UNMUTE": "Désactiver le son"
},
"SETTINGS": {
"TITLE": "Paramètres",
@ -100,7 +104,7 @@
"CAPTIONS": "Les sous-titres",
"DISABLED_AUDIO": "Désactiver l'audio",
"DISABLED_VIDEO": "Désactiver la vidéo",
"CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la session utiliseront. Les sous-titres apparaîtront dans cette langue."
"CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la salle utiliseront. Les sous-titres apparaîtront dans cette langue."
},
"BACKGROUND": {
"TITLE": "Effets de fond",
@ -114,6 +118,10 @@
"SUBTITLE": "Enregistrez votre réunion pour la postérité",
"CONTENT_TITLE": "Enregistrez votre appel vidéo",
"CONTENT_SUBTITLE": "Une fois l'enregistrement terminé, vous pourrez le télécharger facilement",
"VIEW_ONLY_SUBTITLE": "Visualisez et accédez aux enregistrements de la salle",
"VIEW_ONLY_CONTENT_TITLE": "Enregistrements d'appel vidéo",
"VIEW_ONLY_CONTENT_SUBTITLE": "Ici vous pouvez accéder à tous les enregistrements disponibles",
"WATCH": "Regarder",
"STARTING": "Début de l'enregistrement",
"STOPPING": "Arrêt de l'enregistrement",
"IN_PROGRESS": "Enregistrement en cours",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Voulez-vous vraiment supprimer l'enregistrement ?",
"DOWNLOAD": "Télécharger",
"RECORDINGS": "ENREGISTREMENTS",
"NO_MODERATOR": "Seul le MODERATEUR peut lancer l'enregistrement"
"NO_MODERATOR": "Seul le MODERATEUR peut lancer l'enregistrement",
"NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement.",
"NO_RECORDINGS_AVAILABLE": "Aucun enregistrement disponible pour le moment",
"ERROR_STARTING": "Erreur de démarrage"
},
"STREAMING": {
"TITLE": "Streaming",
@ -139,9 +150,9 @@
}
},
"ERRORS": {
"SESSION": "There was an error connecting to the session",
"SESSION": "There was an error connecting to the salle",
"CONNECTION": "Connexion perdue",
"RECONNECT": "Oups ! Tentative de reconnexion à la session...",
"RECONNECT": "Oups ! Tentative de reconnexion à la salle...",
"DISCONNECT": "Vous avez été déconnecté",
"NETWORK_DISCONNECT": "Vous avez été déconnecté en raison d'un problème de connexion réseau",
"SIGNAL_CLOSE": "La connexion au serveur a été interrompue de manière inattendue",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "ऑडियो डिवाइस",
"NO_VIDEO_DEVICE": "वीडियो डिवाइस नहीं मिला",
"NO_AUDIO_DEVICE": "ऑडियो डिवाइस नहीं मिला",
"JOIN": "सत्र में शामिल हों",
"PREPARING": "सत्र तैयार कर रहा है ..."
"JOIN": "कमरा में शामिल हों",
"PREPARING": "कमरा तैयार कर रहा है ..."
},
"TOOLBAR": {
"MUTE_AUDIO": "अपनी ऑडियो को मौन करें",
@ -52,10 +52,12 @@
"START_RECORDING": "रिकॉर्डिंग प्रारंभ करें",
"STOP_RECORDING": "रिकॉर्डिंग रोकें",
"SETTINGS": "सेटिंग्स",
"LEAVE": "सत्र छोड़ें",
"LEAVE": "कमरा छोड़ें",
"PARTICIPANTS": "सदस्य",
"CHAT": "बातचीत",
"ACTIVITIES": "गतिविधियाँ"
"ACTIVITIES": "गतिविधियाँ",
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"VIEW_RECORDINGS": "रिकॉर्डिंग देखें"
},
"STREAM": {
"SETTINGS": "सेटिंग्स",
@ -77,7 +79,7 @@
"CHAT": {
"TITLE": "बातचीत",
"YOU": "आप",
"SUBTITLE": "सत्र समाप्त होने पर संदेश हटा दिए जाएंगे",
"SUBTITLE": "कमरा समाप्त होने पर संदेश हटा दिए जाएंगे",
"PLACEHOLDER": "एक संदेश भेजें ...",
"SEND": "भेजें",
"MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया",
@ -89,7 +91,9 @@
"MICROPHONE": "माइक्रोफ़ोन",
"SCREEN": "स्क्रीन",
"NO_STREAMS": "कोई_स्ट्रीम_नहीं",
"YOU": "आप"
"YOU": "आप",
"MUTE": "मौन",
"UNMUTE": "अनमौन"
},
"SETTINGS": {
"TITLE": "सेटिंग्स",
@ -100,7 +104,7 @@
"CAPTIONS": "उपशीर्षक",
"DISABLED_AUDIO": "ऑडियो अक्षम",
"DISABLED_VIDEO": "वीडियो अक्षम",
"CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग सत्र के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।"
"CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग कमरा के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।"
},
"BACKGROUND": {
"TITLE": "पृष्ठभूमि प्रभाव",
@ -114,6 +118,10 @@
"SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें",
"CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें",
"CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे",
"VIEW_ONLY_SUBTITLE": "कमरे की रिकॉर्डिंग देखें और एक्सेस करें",
"VIEW_ONLY_CONTENT_TITLE": "वीडियो कॉल रिकॉर्डिंग",
"VIEW_ONLY_CONTENT_SUBTITLE": "यहाँ आप सभी उपलब्ध रिकॉर्डिंग तक पहुँच सकते हैं",
"WATCH": "देखना",
"STARTING": "रिकॉर्डिंग शुरू कर रहा है",
"STOPPING": "रिकॉर्डिंग बंद करना",
"IN_PROGRESS": "रिकॉर्डिंग चल रही है",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "क्या आप वाकई रिकॉर्डिंग हटाना चाहते हैं",
"DOWNLOAD": "डाउनलोड",
"RECORDINGS": "रिकॉर्डिंग",
"NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है"
"NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है",
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"NO_RECORDINGS_AVAILABLE": "इस समय कोई रिकॉर्डिंग उपलब्ध नहीं है",
"ERROR_STARTING": "रिकॉर्डिंग शुरू करने में त्रुटि"
},
"STREAMING": {
"TITLE": "स्ट्रीमिंग",
@ -139,9 +150,9 @@
}
},
"ERRORS": {
"SESSION": "सत्र से जुड़ने में त्रुटि हुई",
"SESSION": "कमरा से जुड़ने में त्रुटि हुई",
"CONNECTION": "कनेक्शन खो गया",
"RECONNECT": "ओह! सत्र से फिर से कनेक्ट करने का प्रयास कर रहा है",
"RECONNECT": "ओह! कमरा से फिर से कनेक्ट करने का प्रयास कर रहा है",
"DISCONNECT": "आपको डिस्कनेक्ट कर दिया गया है",
"NETWORK_DISCONNECT": "नेटवर्क कनेक्टिविटी समस्या के कारण आपका कनेक्शन टूट गया",
"SIGNAL_CLOSE": "सर्वर से कनेक्शन अप्रत्याशित रूप से बंद हो गया",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Dispositivo audio",
"NO_VIDEO_DEVICE": "Dispositivo video non trovato",
"NO_AUDIO_DEVICE": "Dispositivo audio non trovato",
"JOIN": "Unisciti alla sessione",
"PREPARING": "Preparazione della sessione in corso..."
"JOIN": "Unisciti alla stanza",
"PREPARING": "Preparazione della stanza in corso..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Disattiva l'audio",
@ -52,10 +52,12 @@
"START_RECORDING": "Avvia registrazione",
"STOP_RECORDING": "Interrompi registrazione",
"SETTINGS": "Impostazioni",
"LEAVE": "Abbandona la sessione",
"LEAVE": "Abbandona la stanza",
"PARTICIPANTS": "Partecipanti",
"CHAT": "Chat",
"ACTIVITIES": "Attività"
"ACTIVITIES": "Attività",
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"VIEW_RECORDINGS": "Visualizza registrazioni"
},
"STREAM": {
"SETTINGS": "Impostazioni",
@ -77,7 +79,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Tu",
"SUBTITLE": "I messaggi verranno rimossi alla fine della sessione",
"SUBTITLE": "I messaggi verranno rimossi alla fine della stanza",
"PLACEHOLDER": "Invia un messaggio...",
"SEND": "Invia",
"MESSAGE_SENT_NOTIFICATION": "messaggio inviato",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROFONO",
"SCREEN": "SCREEN",
"NO_STREAMS": "NESSUNO",
"YOU": "Tu"
"YOU": "Tu",
"MUTE": "Disattiva l'audio",
"UNMUTE": "Attiva l'audio"
},
"SETTINGS": {
"TITLE": "Impostazioni",
@ -100,7 +104,7 @@
"CAPTIONS": "Sottotitoli",
"DISABLED_AUDIO": "Disattiva l'audio",
"DISABLED_VIDEO": "Disattiva il video",
"CAPTIONS_LANG_TEXT": "Seleziona la lingua che i partecipanti della sessione useranno. I sottotitoli appariranno in quella lingua."
"CAPTIONS_LANG_TEXT": "Seleziona la lingua che i partecipanti della stanza useranno. I sottotitoli appariranno in quella lingua."
},
"BACKGROUND": {
"TITLE": "Effetti di sfondo",
@ -114,6 +118,10 @@
"SUBTITLE": "Registra la tua riunione per i posteri",
"CONTENT_TITLE": "Registra la tua videochiamata",
"CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità",
"VIEW_ONLY_SUBTITLE": "Visualizza e accedi alle registrazioni della sala",
"VIEW_ONLY_CONTENT_TITLE": "Registrazioni di videochiamate",
"VIEW_ONLY_CONTENT_SUBTITLE": "Qui puoi accedere a tutte le registrazioni disponibili",
"WATCH": "Guardare",
"STARTING": "Avvio della registrazione",
"STOPPING": "Interruzione della registrazione",
"IN_PROGRESS": "Registrazione in corso",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?",
"DOWNLOAD": "Scarica",
"RECORDINGS": "REGISTRAZIONI",
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione"
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione",
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"NO_RECORDINGS_AVAILABLE": "Nessuna registrazione disponibile al momento",
"ERROR_STARTING": "Errore di avvio"
},
"STREAMING": {
"TITLE": "Streaming",
@ -139,9 +150,9 @@
}
},
"ERRORS": {
"SESSION": "Si è verificato un errore di connessione alla sessione",
"SESSION": "Si è verificato un errore di connessione alla stanza",
"CONNECTION": "Connessione persa",
"RECONNECT": "Oops! Si sta cercando di riconnettersi alla sessione...",
"RECONNECT": "Oops! Si sta cercando di riconnettersi alla stanza...",
"DISCONNECT": "Sei stato disconnesso",
"NETWORK_DISCONNECT": "Sei stato disconnesso a causa di un problema di connettività di rete",
"SIGNAL_CLOSE": "La connessione al server è stata chiusa inaspettatamente",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "オーディオデバイス",
"NO_VIDEO_DEVICE": "ビデオデバイスが見つかりません",
"NO_AUDIO_DEVICE": "オーディオデバイスが見つかりません",
"JOIN": "セッションに参加する",
"PREPARING": "セッションの準備中..."
"JOIN": "ルームに参加する",
"PREPARING": "ルームの準備中..."
},
"TOOLBAR": {
"MUTE_AUDIO": "オーディオをミュートする",
@ -52,10 +52,12 @@
"START_RECORDING": "録画開始",
"STOP_RECORDING": "録画の停止",
"SETTINGS": "設定",
"LEAVE": "セッションを終了する",
"LEAVE": "ルームを終了する",
"PARTICIPANTS": "参加者",
"CHAT": "チャット",
"ACTIVITIES": "アクティビティ"
"ACTIVITIES": "アクティビティ",
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"VIEW_RECORDINGS": "録画を表示"
},
"STREAM": {
"SETTINGS": "設定",
@ -77,7 +79,7 @@
"CHAT": {
"TITLE": "チャット",
"YOU": "あなた",
"SUBTITLE": "メッセージはセッション終了時に削除されます",
"SUBTITLE": "メッセージはルーム終了時に削除されます",
"PLACEHOLDER": "メッセージを送信...",
"SEND": "送信する",
"MESSAGE_SENT_NOTIFICATION": "メッセージを送信しました",
@ -89,7 +91,9 @@
"MICROPHONE": "マイクロフォン",
"SCREEN": "スクリーン",
"NO_STREAMS": "ストリームなし",
"YOU": "あなた"
"YOU": "あなた",
"MUTE": "ミュート",
"UNMUTE": "ミュート解除"
},
"SETTINGS": {
"TITLE": "設定",
@ -100,7 +104,7 @@
"CAPTIONS": "字幕",
"DISABLED_AUDIO": "オーディオを無効にする",
"DISABLED_VIDEO": "ビデオを無効にする",
"CAPTIONS_LANG_TEXT": "セッションの参加者が使用する言語を選択します。キャプションはその言語で表示されます。"
"CAPTIONS_LANG_TEXT": "ルームの参加者が使用する言語を選択します。キャプションはその言語で表示されます。"
},
"BACKGROUND": {
"TITLE": "背景効果",
@ -114,6 +118,10 @@
"SUBTITLE": "会議を録画して保存する",
"CONTENT_TITLE": "ビデオ通話を録音する",
"CONTENT_SUBTITLE": "録画が完了したら、簡単にダウンロードできます",
"VIEW_ONLY_SUBTITLE": "ルームの録画を表示してアクセスする",
"VIEW_ONLY_CONTENT_TITLE": "ビデオ通話の録画",
"VIEW_ONLY_CONTENT_SUBTITLE": "ここで利用可能なすべての録画にアクセスできます",
"WATCH": "視聴する",
"STARTING": "録画開始",
"STOPPING": "録音停止",
"IN_PROGRESS": "録画中",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "録画を削除してもよろしいですか",
"DOWNLOAD": "保存",
"RECORDINGS": "録画",
"NO_MODERATOR": "録画を開始できるのは、モデレーターのみです"
"NO_MODERATOR": "録画を開始できるのは、モデレーターのみです",
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"NO_RECORDINGS_AVAILABLE": "現在利用可能な録画はありません",
"ERROR_STARTING": "録画開始エラー"
},
"STREAMING": {
"TITLE": "ストリーミング",
@ -139,9 +150,9 @@
}
},
"ERRORS": {
"SESSION": "セッションへの接続にエラーが発生しました",
"SESSION": "ルームへの接続にエラーが発生しました",
"CONNECTION": "接続が失われました",
"RECONNECT": "セッションへの再接続を試みています",
"RECONNECT": "ルームへの再接続を試みています",
"DISCONNECT": "接続が切断されました",
"NETWORK_DISCONNECT": "ネットワーク接続の問題により切断されました",
"SIGNAL_CLOSE": "サーバーへの接続が予期せず切断されました",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Audiospeler",
"NO_VIDEO_DEVICE": "Videoapparaat niet gevonden",
"NO_AUDIO_DEVICE": "Audioapparaat niet gevonden",
"JOIN": "Deelnemen aan sessie",
"PREPARING": "Sessie voorbereiden ..."
"JOIN": "Deelnemen aan kamer",
"PREPARING": "Kamer voorbereiden ..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Audio dempen",
@ -52,10 +52,12 @@
"START_RECORDING": "Start opname",
"STOP_RECORDING": "Stop opname",
"SETTINGS": "Instellingen",
"LEAVE": "Verlaat de sessie",
"LEAVE": "Verlaat de kamer",
"PARTICIPANTS": "Deelnemers",
"CHAT": "Chat",
"ACTIVITIES": "Activiteiten"
"ACTIVITIES": "Activiteiten",
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"VIEW_RECORDINGS": "Opnames bekijken"
},
"STREAM": {
"SETTINGS": "Instellingen",
@ -77,7 +79,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Jij",
"SUBTITLE": "Berichten worden aan het einde van de sessie verwijderd",
"SUBTITLE": "Berichten worden aan het einde van de kamer verwijderd",
"PLACEHOLDER": "Stuur een bericht ...",
"SEND": "Versturen",
"MESSAGE_SENT_NOTIFICATION": "bericht verzonden",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROFOON",
"SCREEN": "SCHERM",
"NO_STREAMS": "GEEN",
"YOU": "Jij"
"YOU": "Jij",
"MUTE": "Dempen",
"UNMUTE": "Dempen opheffen"
},
"SETTINGS": {
"TITLE": "Instellingen",
@ -100,7 +104,7 @@
"CAPTIONS": "Ondertitels",
"DISABLED_AUDIO": "Geen audio",
"DISABLED_VIDEO": "Geen video",
"CAPTIONS_LANG_TEXT": "Selecteer de taal die de deelnemers van de sessie zullen gebruiken. De ondertiteling zal in die taal verschijnen."
"CAPTIONS_LANG_TEXT": "Selecteer de taal die de deelnemers van de kamer zullen gebruiken. De ondertiteling zal in die taal verschijnen."
},
"BACKGROUND": {
"TITLE": "Achtergrondeffecten",
@ -114,6 +118,10 @@
"SUBTITLE": "Neem uw vergadering op voor het nageslacht",
"CONTENT_TITLE": "Neem uw videogesprek op",
"CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden",
"VIEW_ONLY_SUBTITLE": "Bekijk en toegang tot kameropnames",
"VIEW_ONLY_CONTENT_TITLE": "Videogesprek opnames",
"VIEW_ONLY_CONTENT_SUBTITLE": "Hier heeft u toegang tot alle beschikbare opnames",
"WATCH": "Bekijken",
"STARTING": "Beginnen met opnemen",
"STOPPING": "Opname stoppen",
"IN_PROGRESS": "Opname in uitvoering",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?",
"DOWNLOAD": "Downloaden",
"RECORDINGS": "OPNAME",
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten"
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten",
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"NO_RECORDINGS_AVAILABLE": "Momenteel zijn er geen opnames beschikbaar",
"ERROR_STARTING": "Fout bij starten opname"
},
"STREAMING": {
"TITLE": "Streaming",
@ -139,9 +150,9 @@
}
},
"ERRORS": {
"SESSION": "Er is een fout opgetreden bij het verbinden met de sessie",
"SESSION": "Er is een fout opgetreden bij het verbinden met de kamer",
"CONNECTION": "Verbinding verloren",
"RECONNECT": "Proberen opnieuw verbinding te maken met de sessie...",
"RECONNECT": "Proberen opnieuw verbinding te maken met de kamer...",
"DISCONNECT": "Je bent losgekoppeld",
"NETWORK_DISCONNECT": "Je bent losgekoppeld vanwege een netwerkprobleem",
"SIGNAL_CLOSE": "De verbinding met de server werd onverwacht verbroken",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Dispositivo de áudio",
"NO_VIDEO_DEVICE": "Dispositivo de vídeo não encontrado",
"NO_AUDIO_DEVICE": "Dispositivo de áudio não encontrado",
"JOIN": "Entrar na sessão",
"PREPARING": "Preparando sessão..."
"JOIN": "Entrar na sala",
"PREPARING": "Preparando sala..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Mute seu áudio",
@ -52,10 +52,12 @@
"START_RECORDING": "Iniciar_gravação",
"STOP_RECORDING": "Parar de gravar",
"SETTINGS": "Configurações",
"LEAVE": "Sair da sessão",
"LEAVE": "Sair da sala",
"PARTICIPANTS": "Participantes",
"CHAT": "Chat",
"ACTIVITIES": "Actividades"
"ACTIVITIES": "Actividades",
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
"VIEW_RECORDINGS": "Ver gravações"
},
"STREAM": {
"SETTINGS": "Configurações",
@ -77,7 +79,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Você",
"SUBTITLE": "As mensagens serão removidas no final da sessão",
"SUBTITLE": "As mensagens serão removidas no final da sala",
"PLACEHOLDER": "Enviar uma mensagem...",
"SEND": "Enviar",
"MESSAGE_SENT_NOTIFICATION": "mensagem enviada",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROFONE",
"SCREEN": "TELA",
"NO_STREAMS": "NENHUM",
"YOU": "Você (eu)"
"YOU": "Você (eu)",
"MUTE": "Silenciar",
"UNMUTE": "Ativar som"
},
"SETTINGS": {
"TITLE": "Configurações",
@ -100,7 +104,7 @@
"CAPTIONS": "Legendas",
"DISABLED_AUDIO": "Áudio desativado",
"DISABLED_VIDEO": "Vídeo desativado",
"CAPTIONS_LANG_TEXT": "Selecione o idioma que os participantes da sessão utilizarão. Os legendas aparecerão nesse idioma."
"CAPTIONS_LANG_TEXT": "Selecione o idioma que os participantes da sala utilizarão. Os legendas aparecerão nesse idioma."
},
"BACKGROUND": {
"TITLE": "Efeitos de fundo",
@ -114,6 +118,10 @@
"SUBTITLE": "Grave a sua reunião para a posteridade",
"CONTENT_TITLE": "Grave a sua videochamada",
"CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade",
"VIEW_ONLY_SUBTITLE": "Visualize e acesse gravações da sala",
"VIEW_ONLY_CONTENT_TITLE": "Gravações de videochamada",
"VIEW_ONLY_CONTENT_SUBTITLE": "Aqui você pode acessar todas as gravações disponíveis",
"WATCH": "Assistir",
"STARTING": "Começar a gravação",
"STOPPING": "Parando a gravação",
"IN_PROGRESS": "Gravação em andamento",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?",
"DOWNLOAD": "Download",
"RECORDINGS": "GRAVAÇÕES",
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação"
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação",
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
"NO_RECORDINGS_AVAILABLE": "Nenhuma gravação disponível no momento",
"ERROR_STARTING": "Erro ao iniciar gravação"
},
"STREAMING": {
"TITLE": "Streaming",
@ -139,9 +150,9 @@
}
},
"ERRORS": {
"SESSION": "Houve um erro de ligação à sessão",
"SESSION": "Houve um erro de ligação à sala",
"CONNECTION": "Ligação perdida",
"RECONNECT": "A tentar restabelecer a ligação à sessão...",
"RECONNECT": "A tentar restabelecer a ligação à sala...",
"DISCONNECT": "Você foi desconectado",
"NETWORK_DISCONNECT": "Você foi desconectado devido a um problema de conectividade de rede",
"SIGNAL_CLOSE": "A conexão com o servidor foi encerrada inesperadamente",

View File

@ -21,7 +21,7 @@ export enum RecordingOutputMode {
export interface RecordingStatusInfo {
status: RecordingStatus;
recordingList: RecordingInfo[];
recordingElapsedTime?: Date;
startedAt?: Date;
error?: string;
}

View File

@ -9,7 +9,36 @@ export enum StorageKeys {
CAMERA_ENABLED = 'cameraEnabled',
LANG = 'lang',
CAPTION_LANG = 'captionLang',
BACKGROUND = "virtualBg"
BACKGROUND = 'virtualBg',
TAB_ID = 'tabId',
ACTIVE_TABS = 'activeTabs'
}
export const PERSISTENT_KEYS: StorageKeys[] = [
StorageKeys.VIDEO_DEVICE,
StorageKeys.AUDIO_DEVICE,
StorageKeys.LANG,
StorageKeys.CAPTION_LANG,
StorageKeys.BACKGROUND
];
export const SESSION_KEYS: StorageKeys[] = [StorageKeys.TAB_ID];
export const TAB_MANAGEMENT_KEYS: StorageKeys[] = [StorageKeys.TAB_ID, StorageKeys.ACTIVE_TABS];
// Data that should be unique per tab (stored in localStorage with tabId prefix)
export const TAB_SPECIFIC_KEYS: StorageKeys[] = [
StorageKeys.PARTICIPANT_NAME,
StorageKeys.MICROPHONE_ENABLED,
StorageKeys.CAMERA_ENABLED,
StorageKeys.LANG,
StorageKeys.CAPTION_LANG,
StorageKeys.BACKGROUND,
StorageKeys.VIDEO_DEVICE,
StorageKeys.AUDIO_DEVICE
];
// Data that should be truly persistent and shared between tabs
export const SHARED_PERSISTENT_KEYS: StorageKeys[] = [];
export const STORAGE_PREFIX = 'ovComponents-';

View File

@ -0,0 +1,98 @@
/**
* Enum representing the possible states of the videoconference component
*/
export enum VideoconferenceState {
/**
* Initial state when the component is loading
*/
INITIALIZING = 'INITIALIZING',
/**
* Prejoin page is being shown to the user
*/
PREJOIN_SHOWN = 'PREJOIN_SHOWN',
/**
* User has initiated the join process, waiting for token
*/
JOINING = 'JOINING',
/**
* Token received and room is ready to connect
*/
READY_TO_CONNECT = 'READY_TO_CONNECT',
/**
* Successfully connected to the room
*/
CONNECTED = 'CONNECTED',
/**
* Disconnected from the room
*/
DISCONNECTED = 'DISCONNECTED',
/**
* Error state
*/
ERROR = 'ERROR'
}
/**
* Interface representing the state information of the videoconference component
*/
export interface VideoconferenceStateInfo {
/**
* Current state of the videoconference
*/
state: VideoconferenceState;
/**
* Whether prejoin page should be visible
*/
showPrejoin: boolean;
/**
* Whether room is ready for connection
*/
isRoomReady: boolean;
/**
* Whether user is connected to the room
*/
isConnected: boolean;
/**
* Whether audio devices are available
*/
hasAudioDevices: boolean;
/**
* Whether video devices are available
*/
hasVideoDevices: boolean;
/**
* Whether user has initiated the join process
*/
hasUserInitiatedJoin: boolean;
/**
* Whether prejoin was shown to the user at least once
*/
wasPrejoinShown: boolean;
/**
* Whether the component is in loading state
*/
isLoading: boolean;
/**
* Error information if any
*/
error?: {
hasError: boolean;
message?: string;
tokenError?: any;
};
}

View File

@ -1,9 +1,102 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, shareReplay, map } from 'rxjs/operators';
import { RecordingInfo } from '../../models/recording.model';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { ParticipantModel } from '../../models/participant.model';
/**
* Configuration item for the service
*/
interface ConfigItem<T> {
subject: BehaviorSubject<T>;
observable$: Observable<T>;
}
/**
* Recording activity controls configuration
*/
interface RecordingControls {
play: boolean;
download: boolean;
delete: boolean;
externalView: boolean;
}
/**
* Toolbar configuration grouped by domain
*/
interface ToolbarConfig {
camera: boolean;
microphone: boolean;
screenshare: boolean;
fullscreen: boolean;
captions: boolean;
settings: boolean;
leave: boolean;
participantsPanel: boolean;
chatPanel: boolean;
activitiesPanel: boolean;
displayRoomName: boolean;
roomName: string;
displayLogo: boolean;
backgroundEffects: boolean;
recording: boolean;
viewRecordings: boolean;
broadcasting: boolean;
brandingLogo: string;
additionalButtonsPosition: ToolbarAdditionalButtonsPosition;
}
/**
* Stream/Video configuration
*/
interface StreamConfig {
videoEnabled: boolean;
audioEnabled: boolean;
displayParticipantName: boolean;
displayAudioDetection: boolean;
videoControls: boolean;
participantItemMuteButton: boolean;
}
/**
* Recording activity configuration
*/
interface RecordingActivityConfig {
enabled: boolean;
readOnly: boolean;
showControls: RecordingControls;
startStopButton: boolean;
viewRecordingsButton: boolean;
showRecordingsList: boolean;
}
/**
* Admin dashboard configuration
*/
interface AdminConfig {
recordingsList: RecordingInfo[];
loginError: any;
loginTitle: string;
dashboardTitle: string;
}
/**
* General application configuration
*/
interface GeneralConfig {
token: string;
livekitUrl: string;
tokenError: any;
minimal: boolean;
participantName: string;
prejoin: boolean;
prejoinDisplayParticipantName: boolean;
showDisconnectionDialog: boolean;
recordingStreamBaseUrl: string;
}
/**
* @internal
*/
@ -11,450 +104,483 @@ import { ParticipantModel } from '../../models/participant.model';
providedIn: 'root'
})
export class OpenViduComponentsConfigService {
private token = <BehaviorSubject<string>>new BehaviorSubject('');
token$: Observable<string>;
/**
* Helper method to create a configuration item with BehaviorSubject and Observable
*/
private createConfigItem<T>(initialValue: T): ConfigItem<T> {
const subject = new BehaviorSubject<T>(initialValue);
const observable$ = subject.asObservable().pipe(distinctUntilChanged(), shareReplay(1));
return { subject, observable$ };
}
private livekitUrl = <BehaviorSubject<string>>new BehaviorSubject('');
livekitUrl$: Observable<string>;
/**
* Helper method for array configurations with optimized comparison
*/
private createArrayConfigItem<T>(initialValue: T[]): ConfigItem<T[]> {
const subject = new BehaviorSubject<T[]>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => {
if (prev.length !== curr.length) return false;
return prev.every((item, index) => this.deepEqual(item, curr[index]));
}),
shareReplay(1)
);
return { subject, observable$ };
}
private tokenError = <BehaviorSubject<any>>new BehaviorSubject(null);
tokenError$: Observable<any>;
private minimal = <BehaviorSubject<boolean>>new BehaviorSubject(false);
minimal$: Observable<boolean>;
private participantName = <BehaviorSubject<string>>new BehaviorSubject('');
participantName$: Observable<string>;
private prejoin = <BehaviorSubject<boolean>>new BehaviorSubject(true);
prejoin$: Observable<boolean>;
private prejoinDisplayParticipantName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
prejoinDisplayParticipantName$: Observable<boolean>;
/**
* Helper method for RecordingControls with specific comparison
*/
private createRecordingControlsConfigItem(initialValue: RecordingControls): ConfigItem<RecordingControls> {
const subject = new BehaviorSubject<RecordingControls>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged(
(prev, curr) =>
prev.play === curr.play &&
prev.download === curr.download &&
prev.delete === curr.delete &&
prev.externalView === curr.externalView
),
shareReplay(1)
);
return { subject, observable$ };
}
private videoEnabled = <BehaviorSubject<boolean>>new BehaviorSubject(true);
videoEnabled$: Observable<boolean>;
private audioEnabled = <BehaviorSubject<boolean>>new BehaviorSubject(true);
audioEnabled$: Observable<boolean>;
/**
* Helper method for ToolbarConfig with specific comparison
*/
private createToolbarConfigItem(initialValue: ToolbarConfig): ConfigItem<ToolbarConfig> {
const subject = new BehaviorSubject<ToolbarConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareToolbarConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
private recordingStreamBaseUrl = <BehaviorSubject<string>>new BehaviorSubject('call/api/recordings');
recordingStreamBaseUrl$: Observable<string>;
/**
* Helper method for StreamConfig with specific comparison
*/
private createStreamConfigItem(initialValue: StreamConfig): ConfigItem<StreamConfig> {
const subject = new BehaviorSubject<StreamConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareStreamConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
//Toolbar settings
private cameraButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
cameraButton$: Observable<boolean>;
/**
* Helper method for RecordingActivityConfig with specific comparison
*/
private createRecordingActivityConfigItem(initialValue: RecordingActivityConfig): ConfigItem<RecordingActivityConfig> {
const subject = new BehaviorSubject<RecordingActivityConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareRecordingActivityConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
private microphoneButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
microphoneButton$: Observable<boolean>;
/**
* Helper method for AdminConfig with specific comparison
*/
private createAdminConfigItem(initialValue: AdminConfig): ConfigItem<AdminConfig> {
const subject = new BehaviorSubject<AdminConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareAdminConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
private screenshareButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
screenshareButton$: Observable<boolean>;
/**
* Helper method for GeneralConfig with specific comparison
*/
private createGeneralConfigItem(initialValue: GeneralConfig): ConfigItem<GeneralConfig> {
const subject = new BehaviorSubject<GeneralConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareGeneralConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
private fullscreenButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
fullscreenButton$: Observable<boolean>;
/**
* Optimized deep equality check
*/
private deepEqual(a: any, b: any): boolean {
if (a === b) return true;
if (a == null || b == null) return a === b;
if (typeof a !== typeof b) return false;
if (typeof a !== 'object') return a === b;
private captionsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
captionsButton$: Observable<boolean>;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
private toolbarSettingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
toolbarSettingsButton$: Observable<boolean>;
return keysA.every((key) => this.deepEqual(a[key], b[key]));
}
private leaveButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
leaveButton$: Observable<boolean>;
/**
* Compare ToolbarConfig efficiently
*/
private compareToolbarConfig(prev: ToolbarConfig, curr: ToolbarConfig): boolean {
return (
prev.camera === curr.camera &&
prev.microphone === curr.microphone &&
prev.screenshare === curr.screenshare &&
prev.fullscreen === curr.fullscreen &&
prev.captions === curr.captions &&
prev.settings === curr.settings &&
prev.leave === curr.leave &&
prev.participantsPanel === curr.participantsPanel &&
prev.chatPanel === curr.chatPanel &&
prev.activitiesPanel === curr.activitiesPanel &&
prev.displayRoomName === curr.displayRoomName &&
prev.roomName === curr.roomName &&
prev.displayLogo === curr.displayLogo &&
prev.backgroundEffects === curr.backgroundEffects &&
prev.recording === curr.recording &&
prev.viewRecordings === curr.viewRecordings &&
prev.broadcasting === curr.broadcasting &&
prev.brandingLogo === curr.brandingLogo &&
prev.additionalButtonsPosition === curr.additionalButtonsPosition
);
}
private participantsPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
participantsPanelButton$: Observable<boolean>;
/**
* Compare StreamConfig efficiently
*/
private compareStreamConfig(prev: StreamConfig, curr: StreamConfig): boolean {
return (
prev.videoEnabled === curr.videoEnabled &&
prev.audioEnabled === curr.audioEnabled &&
prev.displayParticipantName === curr.displayParticipantName &&
prev.displayAudioDetection === curr.displayAudioDetection &&
prev.videoControls === curr.videoControls &&
prev.participantItemMuteButton === curr.participantItemMuteButton
);
}
private chatPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
chatPanelButton$: Observable<boolean>;
/**
* Compare RecordingActivityConfig efficiently
*/
private compareRecordingActivityConfig(prev: RecordingActivityConfig, curr: RecordingActivityConfig): boolean {
return (
prev.enabled === curr.enabled &&
prev.readOnly === curr.readOnly &&
prev.startStopButton === curr.startStopButton &&
prev.viewRecordingsButton === curr.viewRecordingsButton &&
prev.showRecordingsList === curr.showRecordingsList &&
prev.showControls.play === curr.showControls.play &&
prev.showControls.download === curr.showControls.download &&
prev.showControls.delete === curr.showControls.delete &&
prev.showControls.externalView === curr.showControls.externalView
);
}
private activitiesPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
activitiesPanelButton$: Observable<boolean>;
/**
* Compare AdminConfig efficiently
*/
private compareAdminConfig(prev: AdminConfig, curr: AdminConfig): boolean {
return (
prev.loginError === curr.loginError &&
prev.loginTitle === curr.loginTitle &&
prev.dashboardTitle === curr.dashboardTitle &&
prev.recordingsList.length === curr.recordingsList.length &&
prev.recordingsList.every((item, index) => this.deepEqual(item, curr.recordingsList[index]))
);
}
private displayRoomName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayRoomName$: Observable<boolean>;
/**
* Compare GeneralConfig efficiently
*/
private compareGeneralConfig(prev: GeneralConfig, curr: GeneralConfig): boolean {
return (
prev.token === curr.token &&
prev.livekitUrl === curr.livekitUrl &&
prev.tokenError === curr.tokenError &&
prev.minimal === curr.minimal &&
prev.participantName === curr.participantName &&
prev.prejoin === curr.prejoin &&
prev.prejoinDisplayParticipantName === curr.prejoinDisplayParticipantName &&
prev.showDisconnectionDialog === curr.showDisconnectionDialog &&
prev.recordingStreamBaseUrl === curr.recordingStreamBaseUrl
);
}
private brandingLogo = <BehaviorSubject<string>>new BehaviorSubject('');
brandingLogo$: Observable<string>;
// Grouped configuration items by domain
private generalConfig = this.createGeneralConfigItem({
token: '',
livekitUrl: '',
tokenError: null,
minimal: false,
participantName: '',
prejoin: true,
prejoinDisplayParticipantName: true,
showDisconnectionDialog: true,
recordingStreamBaseUrl: 'call/api/recordings'
});
private displayLogo = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayLogo$: Observable<boolean>;
private toolbarConfig = this.createToolbarConfigItem({
camera: true,
microphone: true,
screenshare: true,
fullscreen: true,
captions: true,
settings: true,
leave: true,
participantsPanel: true,
chatPanel: true,
activitiesPanel: true,
displayRoomName: true,
roomName: '',
displayLogo: true,
backgroundEffects: true,
recording: true,
viewRecordings: false,
broadcasting: true,
brandingLogo: '',
additionalButtonsPosition: ToolbarAdditionalButtonsPosition.AFTER_MENU
});
private toolbarAdditionalButtonsPosition = <BehaviorSubject<ToolbarAdditionalButtonsPosition>>(
new BehaviorSubject(ToolbarAdditionalButtonsPosition.AFTER_MENU)
private streamConfig = this.createStreamConfigItem({
videoEnabled: true,
audioEnabled: true,
displayParticipantName: true,
displayAudioDetection: true,
videoControls: true,
participantItemMuteButton: true
});
private recordingActivityConfig = this.createRecordingActivityConfigItem({
enabled: true,
readOnly: false,
showControls: {
play: true,
download: true,
delete: true,
externalView: false
},
startStopButton: true,
viewRecordingsButton: false,
showRecordingsList: true
});
private adminConfig = this.createAdminConfigItem({
recordingsList: [],
loginError: null,
loginTitle: '',
dashboardTitle: ''
});
// Individual configs that don't fit into groups
private broadcastingActivityConfig = this.createConfigItem(true);
private layoutRemoteParticipantsConfig = this.createConfigItem<ParticipantModel[] | undefined>(undefined);
// General observables
token$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.token));
livekitUrl$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.livekitUrl));
tokenError$: Observable<any> = this.generalConfig.observable$.pipe(map((config) => config.tokenError));
minimal$: Observable<boolean> = this.generalConfig.observable$.pipe(map((config) => config.minimal));
participantName$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.participantName));
prejoin$: Observable<boolean> = this.generalConfig.observable$.pipe(map((config) => config.prejoin));
prejoinDisplayParticipantName$: Observable<boolean> = this.generalConfig.observable$.pipe(
map((config) => config.prejoinDisplayParticipantName)
);
toolbarAdditionalButtonsPosition$: Observable<ToolbarAdditionalButtonsPosition>;
showDisconnectionDialog$: Observable<boolean> = this.generalConfig.observable$.pipe(map((config) => config.showDisconnectionDialog));
recordingStreamBaseUrl$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.recordingStreamBaseUrl));
private displayParticipantName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayParticipantName$: Observable<boolean>;
private displayAudioDetection = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayAudioDetection$: Observable<boolean>;
private streamVideoControls = <BehaviorSubject<boolean>>new BehaviorSubject(true);
streamVideoControls$: Observable<boolean>;
private participantItemMuteButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
participantItemMuteButton$: Observable<boolean>;
private backgroundEffectsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
backgroundEffectsButton$: Observable<boolean>;
private recordingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
recordingButton$: Observable<boolean>;
private broadcastingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
broadcastingButton$: Observable<boolean>;
private recordingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true);
recordingActivity$: Observable<boolean>;
private broadcastingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true);
broadcastingActivity$: Observable<boolean>;
// Stream observables
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled));
audioEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.audioEnabled));
displayParticipantName$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.displayParticipantName));
displayAudioDetection$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.displayAudioDetection));
streamVideoControls$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoControls));
participantItemMuteButton$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.participantItemMuteButton));
// Admin
private adminRecordingsList: BehaviorSubject<RecordingInfo[]> = new BehaviorSubject(<RecordingInfo[]>[]);
adminRecordingsList$: Observable<RecordingInfo[]>;
private adminLoginError = <BehaviorSubject<any>>new BehaviorSubject(null);
private adminLoginTitle = <BehaviorSubject<string>>new BehaviorSubject('');
private adminDashboardTitle = <BehaviorSubject<string>>new BehaviorSubject('');
adminLoginTitle$: Observable<string>;
adminDashboardTitle$: Observable<string>;
adminLoginError$: Observable<any>;
// Toolbar observables
cameraButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.camera));
microphoneButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.microphone));
screenshareButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.screenshare));
fullscreenButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.fullscreen));
captionsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.captions));
toolbarSettingsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.settings));
leaveButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.leave));
participantsPanelButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.participantsPanel));
chatPanelButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.chatPanel));
activitiesPanelButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.activitiesPanel));
displayRoomName$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.displayRoomName));
roomName$: Observable<string> = this.toolbarConfig.observable$.pipe(map((config) => config.roomName));
brandingLogo$: Observable<string> = this.toolbarConfig.observable$.pipe(map((config) => config.brandingLogo));
displayLogo$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.displayLogo));
toolbarAdditionalButtonsPosition$: Observable<ToolbarAdditionalButtonsPosition> = this.toolbarConfig.observable$.pipe(
map((config) => config.additionalButtonsPosition)
);
backgroundEffectsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.backgroundEffects));
recordingButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.recording));
toolbarViewRecordingsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.viewRecordings));
broadcastingButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.broadcasting));
// Internals
private layoutRemoteParticipants: BehaviorSubject<ParticipantModel[] | undefined> = new BehaviorSubject(<any>undefined);
layoutRemoteParticipants$: Observable<ParticipantModel[] | undefined>;
// Recording activity observables
recordingActivity$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(map((config) => config.enabled));
recordingActivityReadOnly$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(map((config) => config.readOnly));
recordingActivityShowControls$: Observable<RecordingControls> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.showControls)
);
recordingActivityStartStopRecordingButton$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.startStopButton)
);
recordingActivityViewRecordingsButton$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.viewRecordingsButton)
);
recordingActivityShowRecordingsList$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.showRecordingsList)
);
// Admin observables
adminRecordingsList$: Observable<RecordingInfo[]> = this.adminConfig.observable$.pipe(map((config) => config.recordingsList));
adminLoginError$: Observable<any> = this.adminConfig.observable$.pipe(map((config) => config.loginError));
adminLoginTitle$: Observable<string> = this.adminConfig.observable$.pipe(map((config) => config.loginTitle));
adminDashboardTitle$: Observable<string> = this.adminConfig.observable$.pipe(map((config) => config.dashboardTitle));
// Individual observables that don't fit into groups
broadcastingActivity$: Observable<boolean> = this.broadcastingActivityConfig.observable$;
layoutRemoteParticipants$: Observable<ParticipantModel[] | undefined> = this.layoutRemoteParticipantsConfig.observable$;
constructor() {
this.token$ = this.token.asObservable();
this.livekitUrl$ = this.livekitUrl.asObservable();
this.tokenError$ = this.tokenError.asObservable();
this.minimal$ = this.minimal.asObservable();
this.participantName$ = this.participantName.asObservable();
this.prejoin$ = this.prejoin.asObservable();
this.prejoinDisplayParticipantName$ = this.prejoinDisplayParticipantName.asObservable();
this.videoEnabled$ = this.videoEnabled.asObservable();
this.audioEnabled$ = this.audioEnabled.asObservable();
this.recordingStreamBaseUrl$ = this.recordingStreamBaseUrl.asObservable();
//Toolbar observables
this.cameraButton$ = this.cameraButton.asObservable();
this.microphoneButton$ = this.microphoneButton.asObservable();
this.screenshareButton$ = this.screenshareButton.asObservable();
this.fullscreenButton$ = this.fullscreenButton.asObservable();
this.backgroundEffectsButton$ = this.backgroundEffectsButton.asObservable();
this.leaveButton$ = this.leaveButton.asObservable();
this.participantsPanelButton$ = this.participantsPanelButton.asObservable();
this.chatPanelButton$ = this.chatPanelButton.asObservable();
this.activitiesPanelButton$ = this.activitiesPanelButton.asObservable();
this.displayRoomName$ = this.displayRoomName.asObservable();
this.displayLogo$ = this.displayLogo.asObservable();
this.brandingLogo$ = this.brandingLogo.asObservable();
this.recordingButton$ = this.recordingButton.asObservable();
this.broadcastingButton$ = this.broadcastingButton.asObservable();
this.toolbarSettingsButton$ = this.toolbarSettingsButton.asObservable();
this.captionsButton$ = this.captionsButton.asObservable();
this.toolbarAdditionalButtonsPosition$ = this.toolbarAdditionalButtonsPosition.asObservable();
//Stream observables
this.displayParticipantName$ = this.displayParticipantName.asObservable();
this.displayAudioDetection$ = this.displayAudioDetection.asObservable();
this.streamVideoControls$ = this.streamVideoControls.asObservable();
// Participant item observables
this.participantItemMuteButton$ = this.participantItemMuteButton.asObservable();
// Recording activity observables
this.recordingActivity$ = this.recordingActivity.asObservable();
// Broadcasting activity
this.broadcastingActivity$ = this.broadcastingActivity.asObservable();
// Admin dashboard
this.adminRecordingsList$ = this.adminRecordingsList.asObservable();
this.adminLoginError$ = this.adminLoginError.asObservable();
this.adminLoginTitle$ = this.adminLoginTitle.asObservable();
this.adminDashboardTitle$ = this.adminDashboardTitle.asObservable();
// Internals
this.layoutRemoteParticipants$ = this.layoutRemoteParticipants.asObservable();
// Constructor no longer needed - all observables are initialized directly
}
setToken(token: string) {
this.token.next(token);
// ============================================
// BATCH UPDATE METHODS
// ============================================
/**
* Update multiple general configuration properties at once
*/
updateGeneralConfig(partialConfig: Partial<GeneralConfig>): void {
const current = this.generalConfig.subject.getValue();
this.generalConfig.subject.next({ ...current, ...partialConfig });
}
setLivekitUrl(livekitUrl: string) {
this.livekitUrl.next(livekitUrl);
/**
* Update multiple toolbar configuration properties at once
*/
updateToolbarConfig(partialConfig: Partial<ToolbarConfig>): void {
const current = this.toolbarConfig.subject.getValue();
this.toolbarConfig.subject.next({ ...current, ...partialConfig });
}
/**
* Update multiple stream configuration properties at once
*/
updateStreamConfig(partialConfig: Partial<StreamConfig>): void {
const current = this.streamConfig.subject.getValue();
this.streamConfig.subject.next({ ...current, ...partialConfig });
}
/**
* Update multiple recording activity configuration properties at once
*/
updateRecordingActivityConfig(partialConfig: Partial<RecordingActivityConfig>): void {
const current = this.recordingActivityConfig.subject.getValue();
this.recordingActivityConfig.subject.next({ ...current, ...partialConfig });
}
/**
* Update multiple admin configuration properties at once
*/
updateAdminConfig(partialConfig: Partial<AdminConfig>): void {
const current = this.adminConfig.subject.getValue();
this.adminConfig.subject.next({ ...current, ...partialConfig });
}
/**
* Update recording controls specifically with batch support
*/
updateRecordingControls(partialControls: Partial<RecordingControls>): void {
const current = this.recordingActivityConfig.subject.getValue();
const updatedControls = { ...current.showControls, ...partialControls };
this.updateRecordingActivityConfig({ showControls: updatedControls });
}
// ============================================
// DIRECT ACCESS METHODS (for internal use)
// ============================================
/**
* @internal
* Get current participant name directly
*/
getCurrentParticipantName(): string {
return this.generalConfig.subject.getValue().participantName;
}
// ============================================
// INDIVIDUAL GETTER/SETTER METHODS
// ============================================
// General configuration methods
getLivekitUrl(): string {
return this.livekitUrl.getValue();
return this.generalConfig.subject.getValue().livekitUrl;
}
setTokenError(error: any) {
this.tokenError.next(error);
showPrejoin(): boolean {
return this.generalConfig.subject.getValue().prejoin;
}
setMinimal(minimal: boolean) {
this.minimal.next(minimal);
}
isMinimal(): boolean {
return this.minimal.getValue();
}
setParticipantName(participantName: string) {
this.participantName.next(participantName);
}
setPrejoin(prejoin: boolean) {
this.prejoin.next(prejoin);
}
setPrejoinDisplayParticipantName(prejoinDisplayParticipantName: boolean) {
this.prejoinDisplayParticipantName.next(prejoinDisplayParticipantName);
}
isPrejoin(): boolean {
return this.prejoin.getValue();
}
setVideoEnabled(videoEnabled: boolean) {
this.videoEnabled.next(videoEnabled);
}
isVideoEnabled(): boolean {
return this.videoEnabled.getValue();
}
setAudioEnabled(audioEnabled: boolean) {
this.audioEnabled.next(audioEnabled);
}
isAudioEnabled(): boolean {
return this.audioEnabled.getValue();
}
setRecordingStreamBaseUrl(recordingStreamBaseUrl: string) {
this.recordingStreamBaseUrl.next(recordingStreamBaseUrl);
getShowDisconnectionDialog(): boolean {
return this.generalConfig.subject.getValue().showDisconnectionDialog;
}
getRecordingStreamBaseUrl(): string {
let baseUrl = this.recordingStreamBaseUrl.getValue();
let baseUrl = this.generalConfig.subject.getValue().recordingStreamBaseUrl;
// Add trailing slash if not present
baseUrl += baseUrl.endsWith('/') ? '' : '/';
return baseUrl;
}
//Toolbar settings
// Stream configuration methods
setCameraButton(cameraButton: boolean) {
this.cameraButton.next(cameraButton);
isVideoEnabled(): boolean {
return this.streamConfig.subject.getValue().videoEnabled;
}
showCameraButton(): boolean {
return this.cameraButton.getValue();
isAudioEnabled(): boolean {
return this.streamConfig.subject.getValue().audioEnabled;
}
setMicrophoneButton(microphoneButton: boolean) {
this.microphoneButton.next(microphoneButton);
}
// Toolbar configuration methods
showMicrophoneButton(): boolean {
return this.microphoneButton.getValue();
}
setScreenshareButton(screenshareButton: boolean) {
this.screenshareButton.next(screenshareButton);
}
showScreenshareButton(): boolean {
return this.screenshareButton.getValue();
}
setFullscreenButton(fullscreenButton: boolean) {
this.fullscreenButton.next(fullscreenButton);
}
showFullscreenButton(): boolean {
return this.fullscreenButton.getValue();
}
setCaptionsButton(captionsButton: boolean) {
this.captionsButton.next(captionsButton);
}
showCaptionsButton(): boolean {
return this.captionsButton.getValue();
}
setToolbarSettingsButton(toolbarSettingsButton: boolean) {
this.toolbarSettingsButton.next(toolbarSettingsButton);
}
showToolbarSettingsButton(): boolean {
return this.toolbarSettingsButton.getValue();
}
setLeaveButton(leaveButton: boolean) {
this.leaveButton.next(leaveButton);
}
showLeaveButton(): boolean {
return this.leaveButton.getValue();
}
setParticipantsPanelButton(participantsPanelButton: boolean) {
this.participantsPanelButton.next(participantsPanelButton);
}
showParticipantsPanelButton(): boolean {
return this.participantsPanelButton.getValue();
}
setChatPanelButton(chatPanelButton: boolean) {
this.chatPanelButton.next(chatPanelButton);
}
showChatPanelButton(): boolean {
return this.chatPanelButton.getValue();
}
setActivitiesPanelButton(activitiesPanelButton: boolean) {
this.activitiesPanelButton.next(activitiesPanelButton);
}
showActivitiesPanelButton(): boolean {
return this.activitiesPanelButton.getValue();
}
setDisplayRoomName(displayRoomName: boolean) {
this.displayRoomName.next(displayRoomName);
}
setBrandingLogo(brandingLogo: string) {
this.brandingLogo.next(brandingLogo);
}
showRoomName(): boolean {
return this.displayRoomName.getValue();
}
setDisplayLogo(displayLogo: boolean) {
this.displayLogo.next(displayLogo);
}
showLogo(): boolean {
return this.displayLogo.getValue();
}
getToolbarAdditionalButtonsPosition(): ToolbarAdditionalButtonsPosition {
return this.toolbarAdditionalButtonsPosition.getValue();
}
setToolbarAdditionalButtonsPosition(toolbarAdditionalButtonsPosition: ToolbarAdditionalButtonsPosition) {
this.toolbarAdditionalButtonsPosition.next(toolbarAdditionalButtonsPosition);
}
setRecordingButton(recordingButton: boolean) {
this.recordingButton.next(recordingButton);
}
showRecordingButton(): boolean {
return this.recordingButton.getValue();
getRoomName(): string {
return this.toolbarConfig.subject.getValue().roomName;
}
setBroadcastingButton(broadcastingButton: boolean) {
this.broadcastingButton.next(broadcastingButton);
}
showBroadcastingButton(): boolean {
return this.broadcastingButton.getValue();
}
setRecordingActivity(recordingActivity: boolean) {
this.recordingActivity.next(recordingActivity);
}
showRecordingActivity(): boolean {
return this.recordingActivity.getValue();
}
setBroadcastingActivity(broadcastingActivity: boolean) {
this.broadcastingActivity.next(broadcastingActivity);
}
showBroadcastingActivity(): boolean {
return this.broadcastingActivity.getValue();
}
//Stream settings
setDisplayParticipantName(displayParticipantName: boolean) {
this.displayParticipantName.next(displayParticipantName);
}
isParticipantNameDisplayed(): boolean {
return this.displayParticipantName.getValue();
}
setDisplayAudioDetection(displayAudioDetection: boolean) {
this.displayAudioDetection.next(displayAudioDetection);
}
isAudioDetectionDisplayed(): boolean {
return this.displayAudioDetection.getValue();
}
setStreamVideoControls(streamVideoControls: boolean) {
this.streamVideoControls.next(streamVideoControls);
}
showStreamVideoControls(): boolean {
return this.streamVideoControls.getValue();
}
setParticipantItemMuteButton(participantItemMuteButton: boolean) {
this.participantItemMuteButton.next(participantItemMuteButton);
}
showParticipantItemMuteButton(): boolean {
return this.participantItemMuteButton.getValue();
}
setBackgroundEffectsButton(backgroundEffectsButton: boolean) {
this.backgroundEffectsButton.next(backgroundEffectsButton);
this.updateToolbarConfig({ broadcasting: broadcastingButton });
}
showBackgroundEffectsButton(): boolean {
return this.backgroundEffectsButton.getValue();
return this.toolbarConfig.subject.getValue().backgroundEffects;
}
// Admin dashboard
// Activity methods (these remain individual as they don't fit cleanly into toolbar config)
setAdminRecordingsList(adminRecordingsList: RecordingInfo[]) {
this.adminRecordingsList.next(adminRecordingsList);
}
getAdminRecordingsList(): RecordingInfo[] {
return this.adminRecordingsList.getValue();
}
setAdminLoginError(adminLoginError: any) {
this.adminLoginError.next(adminLoginError);
}
getAdminLoginError(): any {
return this.adminLoginError.getValue();
}
getAdminLoginTitle(): string {
return this.adminLoginTitle.getValue();
}
setAdminLoginTitle(title: string) {
this.adminLoginTitle.next(title);
}
getAdminDashboardTitle(): string {
return this.adminDashboardTitle.getValue();
}
setAdminDashboardTitle(title: string) {
this.adminDashboardTitle.next(title);
}
isRecordingEnabled(): boolean {
return this.recordingButton.getValue() && this.recordingActivity.getValue();
}
isBroadcastingEnabled(): boolean {
return this.broadcastingButton.getValue() && this.broadcastingActivity.getValue();
setBroadcastingActivity(broadcastingActivity: boolean) {
this.broadcastingActivityConfig.subject.next(broadcastingActivity);
}
// Internals
setLayoutRemoteParticipants(participants: ParticipantModel[] | undefined) {
this.layoutRemoteParticipants.next(participants);
this.layoutRemoteParticipantsConfig.subject.next(participants);
}
// Recording Activity Configuration methods
showRecordingActivityRecordingsList(): boolean {
return this.recordingActivityConfig.subject.getValue().showRecordingsList;
}
}

View File

@ -64,6 +64,12 @@ export class OpenViduService {
* @internal
*/
initRoom(): void {
// If room already exists, don't recreate it
if (this.room) {
this.log.d('Room already initialized, skipping re-initialization');
return;
}
const videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
const roomOptions: RoomOptions = {
@ -88,6 +94,7 @@ export class OpenViduService {
disconnectOnPageLeave: true
};
this.room = new Room(roomOptions);
this.log.d('Room initialized successfully');
}
/**
@ -130,12 +137,20 @@ export class OpenViduService {
*/
getRoom(): Room {
if (!this.room) {
this.log.e('Room is not initialized');
throw new Error('Room is not initialized');
this.log.e('Room is not initialized. Make sure token is set before accessing the room.');
throw new Error('Room is not initialized. Make sure token is set before accessing the room.');
}
return this.room;
}
/**
* Checks if room is initialized without throwing an error
* @returns true if room is initialized, false otherwise
*/
isRoomInitialized(): boolean {
return !!this.room;
}
/**
* Returns the room name
*/
@ -151,6 +166,14 @@ export class OpenViduService {
return this.room?.state === ConnectionState.Connected;
}
hasRoomTracksPublished(): boolean {
const { localParticipant, remoteParticipants } = this.getRoom();
const localTracks = localParticipant.getTrackPublications();
const remoteTracks = Array.from(remoteParticipants.values()).flatMap((p) => p.getTrackPublications());
return localTracks.length > 0 || remoteTracks.length > 0;
}
/**
* @internal
*/
@ -163,6 +186,13 @@ export class OpenViduService {
this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent');
throw new Error('Livekit URL is not defined');
}
// Initialize room if it doesn't exist yet
// This ensures that getRoom() won't fail if token is set before onTokenRequested
if (!this.room) {
this.log.d('Room not initialized yet, initializing room due to token assignment');
this.initRoom();
}
// return this.room.prepareConnection(this.livekitUrl, this.livekitToken);
}

View File

@ -20,7 +20,7 @@ export class RecordingService {
private recordingStatus = <BehaviorSubject<RecordingStatusInfo>>new BehaviorSubject({
status: RecordingStatus.STOPPED,
recordingList: [] as RecordingInfo[],
recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0)
startedAt: new Date(0, 0, 0, 0, 0, 0)
});
private log: ILogger;
@ -41,13 +41,9 @@ export class RecordingService {
* @internal
*/
setRecordingStarted(recordingInfo?: RecordingInfo, startTimestamp?: number) {
// Register the start timestamp of the recording
// to calculate the elapsed time
debugger;
this.recordingStartTimestamp = recordingInfo?.startedAt || Date.now();
// Initialize the recording elapsed time
this.startRecordingTimer();
// Determine the actual start timestamp of the recording
// Priority: startTimestamp parameter > recordingInfo.startedAt > current time
this.recordingStartTimestamp = startTimestamp || recordingInfo?.startedAt || Date.now();
const { recordingList } = this.recordingStatus.getValue();
let updatedRecordingList = [...recordingList];
@ -62,17 +58,22 @@ export class RecordingService {
updatedRecordingList = [recordingInfo, ...updatedRecordingList];
}
}
// Calculate the elapsed time based on the actual start timestamp
const recordingElapsedTime = new Date(0, 0, 0, 0, 0, 0);
if (startTimestamp) {
const elapsedSeconds = Math.floor((Date.now() - startTimestamp) / 1000);
recordingElapsedTime.setSeconds(elapsedSeconds);
if (this.recordingStartTimestamp) {
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
recordingElapsedTime.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative
}
this.updateStatus({
status: RecordingStatus.STARTED,
recordingList: updatedRecordingList,
recordingElapsedTime
startedAt: recordingElapsedTime
});
// Start the timer after updating the initial state
this.startRecordingTimer();
}
/**
@ -97,7 +98,7 @@ export class RecordingService {
this.updateStatus({
status: RecordingStatus.STOPPED,
recordingList: updatedRecordingList,
recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0)
startedAt: new Date(0, 0, 0, 0, 0, 0)
});
this.recordingStartTimestamp = null;
@ -108,11 +109,11 @@ export class RecordingService {
* The `started` stastus will be updated automatically when the recording is actually started.
*/
setRecordingStarting() {
const { recordingList, recordingElapsedTime } = this.recordingStatus.getValue();
const { recordingList, startedAt } = this.recordingStatus.getValue();
this.updateStatus({
status: RecordingStatus.STARTING,
recordingList,
recordingElapsedTime
startedAt
});
}
@ -122,11 +123,11 @@ export class RecordingService {
*/
setRecordingFailed(error: string) {
this.stopRecordingTimer();
const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue();
const { startedAt, recordingList } = this.recordingStatus.getValue();
const statusInfo: RecordingStatusInfo = {
status: RecordingStatus.FAILED,
recordingList,
recordingElapsedTime,
startedAt,
error
};
this.updateStatus(statusInfo);
@ -137,12 +138,12 @@ export class RecordingService {
* The `stopped` stastus will be updated automatically when the recording is actually stopped.
*/
setRecordingStopping() {
const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue();
const { startedAt, recordingList } = this.recordingStatus.getValue();
this.updateStatus({
status: RecordingStatus.STOPPING,
recordingList,
recordingElapsedTime
startedAt
});
}
@ -200,14 +201,14 @@ export class RecordingService {
* @internal
*/
deleteRecording(recording: RecordingInfo) {
const { recordingList, status, recordingElapsedTime } = this.recordingStatus.getValue();
const { recordingList, status, startedAt } = this.recordingStatus.getValue();
const updatedList = recordingList.filter((item) => item.id !== recording.id);
if (updatedList.length !== recordingList.length) {
this.updateStatus({
status,
recordingList: updatedList,
recordingElapsedTime
startedAt
});
return true;
}
@ -220,11 +221,11 @@ export class RecordingService {
* @internal
*/
setRecordingList(recordings: RecordingInfo[]) {
const { status, recordingElapsedTime, error } = this.recordingStatus.getValue();
const { status, startedAt, error } = this.recordingStatus.getValue();
this.updateStatus({
status,
recordingList: recordings,
recordingElapsedTime,
startedAt,
error
});
}
@ -234,19 +235,21 @@ export class RecordingService {
* @param status {@link RecordingStatus}
*/
private updateStatus(statusInfo: RecordingStatusInfo) {
const { status, recordingList, error, recordingElapsedTime } = statusInfo;
const { status, recordingList, error, startedAt } = statusInfo;
this.recordingStatus.next({
status,
recordingList,
recordingElapsedTime,
startedAt,
error
});
}
private startRecordingTimer() {
// Don't override the timestamp if it's already set correctly
if (this.recordingStartTimestamp === null) {
this.recordingStartTimestamp = Date.now();
}
if (this.recordingTimeInterval) {
clearInterval(this.recordingTimeInterval);
}
@ -254,29 +257,29 @@ export class RecordingService {
this.recordingTimeInterval = setInterval(() => {
if (!this.recordingStartTimestamp) return;
let { recordingElapsedTime } = this.recordingStatus.getValue();
if (recordingElapsedTime) {
// Calculamos con precisión el tiempo transcurrido
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
const updatedElapsedTime = new Date(0, 0, 0, 0, 0, 0);
updatedElapsedTime.setSeconds(elapsedSeconds);
// Calculate elapsed time based on the actual recording start timestamp
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
const startedAt = new Date(0, 0, 0, 0, 0, 0);
startedAt.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative
const { recordingList, status } = this.recordingStatus.getValue();
this.updateStatus({
status,
recordingList,
recordingElapsedTime: updatedElapsedTime
});
}
const { recordingList, status } = this.recordingStatus.getValue();
this.updateStatus({
status,
recordingList,
startedAt
});
}, 1000);
}
private stopRecordingTimer() {
clearInterval(this.recordingTimeInterval);
if (this.recordingTimeInterval) {
clearInterval(this.recordingTimeInterval);
}
const { recordingList, status, error } = this.recordingStatus.getValue();
const statusInfo: RecordingStatusInfo = {
status,
recordingList,
startedAt: new Date(0, 0, 0, 0, 0, 0), // Reset elapsed time when stopped
error
};
this.updateStatus(statusInfo);

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { ILogger } from '../../models/logger.model';
import { STORAGE_PREFIX, StorageKeys } from '../../models/storage.model';
import { STORAGE_PREFIX, StorageKeys, SESSION_KEYS, TAB_MANAGEMENT_KEYS, TAB_SPECIFIC_KEYS, SHARED_PERSISTENT_KEYS } from '../../models/storage.model';
import { LoggerService } from '../logger/logger.service';
import { CustomDevice } from '../../models/device.model';
@ -10,13 +10,125 @@ import { CustomDevice } from '../../models/device.model';
@Injectable({
providedIn: 'root'
})
export class StorageService {
public storage = window.localStorage;
export class StorageService implements OnDestroy {
public localStorage = window.localStorage;
public sessionStorage = window.sessionStorage;
public log: ILogger;
protected PREFIX_KEY = STORAGE_PREFIX;
private tabId: string;
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds
private cleanupInterval: any;
constructor(protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('StorageService');
this.initializeTabManagement();
}
/**
* Initializes tab management system
* Creates unique tab ID and sets up cleanup mechanism
*/
private initializeTabManagement(): void {
// Generate unique tab ID
this.tabId = this.generateTabId();
this.setSessionValue(StorageKeys.TAB_ID, this.tabId);
// Register this tab as active
this.registerActiveTab();
// Set up periodic cleanup of inactive tabs
this.setupTabCleanup();
// Listen for page unload to clean up this tab
window.addEventListener('beforeunload', () => {
this.unregisterActiveTab();
});
this.log.d(`Tab initialized with ID: ${this.tabId}`);
}
/**
* Generates a unique tab identifier
*/
private generateTabId(): string {
return `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
/**
* Registers current tab as active
*/
private registerActiveTab(): void {
const activeTabs = this.getActiveTabsFromStorage() || {};
activeTabs[this.tabId] = Date.now();
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
}
/**
* Unregisters current tab from active tabs
*/
private unregisterActiveTab(): void {
const activeTabs = this.getActiveTabsFromStorage() || {};
delete activeTabs[this.tabId];
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
this.cleanupTabData(this.tabId);
}
/**
* Sets up periodic cleanup of inactive tabs
*/
private setupTabCleanup(): void {
this.cleanupInterval = setInterval(() => {
this.cleanupInactiveTabs();
}, this.TAB_CLEANUP_INTERVAL);
}
/**
* Cleans up data from inactive tabs
*/
private cleanupInactiveTabs(): void {
const activeTabs = this.getActiveTabsFromStorage() || {};
const currentTime = Date.now();
const timeoutThreshold = this.TAB_CLEANUP_INTERVAL * 2; // 60 seconds
Object.keys(activeTabs).forEach(tabId => {
const lastActivity = activeTabs[tabId];
if (currentTime - lastActivity > timeoutThreshold) {
this.log.d(`Cleaning up inactive tab: ${tabId}`);
delete activeTabs[tabId];
this.cleanupTabData(tabId);
}
});
// Update heartbeat for current tab
activeTabs[this.tabId] = currentTime;
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
}
/**
* Cleans up data associated with a specific tab
*/
private cleanupTabData(tabId: string): void {
// Clean up tab-specific data from localStorage
TAB_SPECIFIC_KEYS.forEach(key => {
const storageKey = `${this.PREFIX_KEY}${tabId}_${key}`;
this.localStorage.removeItem(storageKey);
});
this.log.d(`Cleaned up data for tab: ${tabId}`);
}
/**
* Gets active tabs from localStorage
*/
private getActiveTabsFromStorage(): { [key: string]: number } | null {
return this.getLocalValue(StorageKeys.ACTIVE_TABS);
}
/**
* Gets the current tab ID
*/
public getTabId(): string {
return this.tabId;
}
getParticipantName(): string | null {
@ -106,24 +218,164 @@ export class StorageService {
}
protected set(key: string, item: any) {
const value = JSON.stringify({ item: item });
this.storage.setItem(this.PREFIX_KEY + key, value);
if (SESSION_KEYS.includes(key as StorageKeys)) {
this.setSessionValue(key, item);
} else {
this.setLocalValue(key, item);
}
}
protected get(key: string): any {
const str = this.storage.getItem(this.PREFIX_KEY + key);
if (SESSION_KEYS.includes(key as StorageKeys)) {
return this.getSessionValue(key);
} else {
return this.getLocalValue(key);
}
}
protected remove(key: string) {
if (SESSION_KEYS.includes(key as StorageKeys)) {
this.removeSessionValue(key);
} else {
this.removeLocalValue(key);
}
}
/**
* Determines if a key should use tab-specific storage in localStorage
*/
private shouldUseTabSpecificKey(key: string): boolean {
return TAB_SPECIFIC_KEYS.includes(key as StorageKeys);
}
/**
* Sets value in localStorage with tab-specific key if needed
*/
private setLocalValue(key: string, item: any): void {
const value = JSON.stringify({ item: item });
const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${key}`;
this.localStorage.setItem(storageKey, value);
}
/**
* Gets value from localStorage with tab-specific key if needed
*/
private getLocalValue(key: string): any {
const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${key}`;
const str = this.localStorage.getItem(storageKey);
if (!!str) {
return JSON.parse(str).item;
}
return null;
}
protected remove(key: string) {
this.storage.removeItem(this.PREFIX_KEY + key);
/**
* Removes value from localStorage with tab-specific key if needed
*/
private removeLocalValue(key: string): void {
const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${key}`;
this.localStorage.removeItem(storageKey);
}
/**
* Sets value in sessionStorage
*/
private setSessionValue(key: string, item: any): void {
const value = JSON.stringify({ item: item });
this.sessionStorage.setItem(this.PREFIX_KEY + key, value);
}
/**
* Gets value from sessionStorage
*/
private getSessionValue(key: string): any {
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key);
if (!!str) {
return JSON.parse(str).item;
}
return null;
}
/**
* Removes value from sessionStorage
*/
private removeSessionValue(key: string): void {
this.sessionStorage.removeItem(this.PREFIX_KEY + key);
}
public clear() {
this.log.d('Clearing localStorage');
this.storage.clear();
this.log.d('Clearing localStorage and sessionStorage');
// Clear only our prefixed keys from localStorage
Object.keys(this.localStorage).forEach(key => {
if (key.startsWith(this.PREFIX_KEY)) {
this.localStorage.removeItem(key);
}
});
// Clear only our prefixed keys from sessionStorage
Object.keys(this.sessionStorage).forEach(key => {
if (key.startsWith(this.PREFIX_KEY)) {
this.sessionStorage.removeItem(key);
}
});
}
/**
* Clears only session data (tab-specific data)
*/
public clearSessionData(): void {
this.log.d('Clearing session data');
Object.keys(this.sessionStorage).forEach(key => {
if (key.startsWith(this.PREFIX_KEY)) {
this.sessionStorage.removeItem(key);
}
});
}
/**
* Clears only tab-specific data for current tab
*/
public clearTabSpecificData(): void {
this.log.d('Clearing tab-specific data');
TAB_SPECIFIC_KEYS.forEach(key => {
this.removeLocalValue(key);
});
}
/**
* Clears only persistent data
*/
public clearPersistentData(): void {
this.log.d('Clearing persistent data');
SHARED_PERSISTENT_KEYS.forEach(key => {
this.removeLocalValue(key);
});
TAB_MANAGEMENT_KEYS.forEach(key => {
this.removeLocalValue(key);
});
}
/**
* Cleanup method to be called when service is destroyed
*/
public destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.unregisterActiveTab();
}
/**
* Angular lifecycle hook - called when service is destroyed
*/
ngOnDestroy(): void {
this.destroy();
}
}

View File

@ -0,0 +1,431 @@
import { Injectable, TemplateRef } from '@angular/core';
import { ILogger } from '../../models/logger.model';
import { LoggerService } from '../logger/logger.service';
import {
ActivitiesPanelDirective,
AdditionalPanelsDirective,
ChatPanelDirective,
LayoutDirective,
PanelDirective,
ParticipantPanelItemDirective,
ParticipantPanelItemElementsDirective,
ParticipantsPanelDirective,
StreamDirective,
ToolbarAdditionalButtonsDirective,
ToolbarAdditionalPanelButtonsDirective,
ToolbarDirective
} from '../../directives/template/openvidu-components-angular.directive';
import {
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective
} from '../../directives/template/internals.directive';
/**
* Configuration object for all templates in the videoconference component
*/
export interface TemplateConfiguration {
// Toolbar templates
toolbarTemplate: TemplateRef<any>;
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
// Panel templates
panelTemplate: TemplateRef<any>;
chatPanelTemplate: TemplateRef<any>;
participantsPanelTemplate: TemplateRef<any>;
activitiesPanelTemplate: TemplateRef<any>;
additionalPanelsTemplate?: TemplateRef<any>;
// Participant templates
participantPanelAfterLocalParticipantTemplate?: TemplateRef<any>;
participantPanelItemTemplate: TemplateRef<any>;
participantPanelItemElementsTemplate?: TemplateRef<any>;
// Layout templates
layoutTemplate: TemplateRef<any>;
streamTemplate: TemplateRef<any>;
layoutAdditionalElementsTemplate?: TemplateRef<any>;
// PreJoin template
preJoinTemplate?: TemplateRef<any>;
}
/**
* Configuration object for panel component templates
*/
export interface PanelTemplateConfiguration {
participantsPanelTemplate?: TemplateRef<any>;
chatPanelTemplate?: TemplateRef<any>;
activitiesPanelTemplate?: TemplateRef<any>;
additionalPanelsTemplate?: TemplateRef<any>;
backgroundEffectsPanelTemplate?: TemplateRef<any>;
settingsPanelTemplate?: TemplateRef<any>;
}
/**
* Configuration object for toolbar component templates
*/
export interface ToolbarTemplateConfiguration {
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
}
/**
* Configuration object for layout component templates
*/
export interface LayoutTemplateConfiguration {
layoutStreamTemplate?: TemplateRef<any>;
layoutAdditionalElementsTemplate?: TemplateRef<any>;
}
/**
* Configuration object for participants panel component templates
*/
export interface ParticipantsPanelTemplateConfiguration {
participantPanelItemTemplate?: TemplateRef<any>;
participantPanelAfterLocalParticipantTemplate?: TemplateRef<any>;
}
/**
* Configuration object for participant panel item component templates
*/
export interface ParticipantPanelItemTemplateConfiguration {
participantPanelItemElementsTemplate?: TemplateRef<any>;
}
/**
* Configuration object for session component templates
*/
export interface SessionTemplateConfiguration {
toolbarTemplate?: TemplateRef<any>;
panelTemplate?: TemplateRef<any>;
layoutTemplate?: TemplateRef<any>;
}
/**
* External directives provided by the consumer
*/
export interface ExternalDirectives {
toolbar?: ToolbarDirective;
toolbarAdditionalButtons?: ToolbarAdditionalButtonsDirective;
toolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
additionalPanels?: AdditionalPanelsDirective;
panel?: PanelDirective;
chatPanel?: ChatPanelDirective;
activitiesPanel?: ActivitiesPanelDirective;
participantsPanel?: ParticipantsPanelDirective;
participantPanelAfterLocalParticipant?: ParticipantPanelAfterLocalParticipantDirective;
participantPanelItem?: ParticipantPanelItemDirective;
participantPanelItemElements?: ParticipantPanelItemElementsDirective;
layout?: LayoutDirective;
stream?: StreamDirective;
preJoin?: PreJoinDirective;
layoutAdditionalElements?: LayoutAdditionalElementsDirective;
}
/**
* Default templates provided by the component
*/
export interface DefaultTemplates {
toolbar: TemplateRef<any>;
panel: TemplateRef<any>;
chatPanel: TemplateRef<any>;
participantsPanel: TemplateRef<any>;
activitiesPanel: TemplateRef<any>;
participantPanelItem: TemplateRef<any>;
layout: TemplateRef<any>;
stream: TemplateRef<any>;
}
/**
* Service responsible for managing and configuring templates for the videoconference component.
* This service centralizes all template setup logic, making the main component cleaner and more maintainable.
*/
@Injectable({
providedIn: 'root'
})
export class TemplateManagerService {
private log: ILogger;
constructor(private loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('TemplateManagerService');
}
/**
* Sets up all templates based on external directives and default templates
*/
setupTemplates(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateConfiguration {
this.log.d('Setting up templates...');
const config: TemplateConfiguration = {
toolbarTemplate: this.setupToolbarTemplate(externalDirectives, defaultTemplates),
panelTemplate: this.setupPanelTemplate(externalDirectives, defaultTemplates),
layoutTemplate: this.setupLayoutTemplate(externalDirectives, defaultTemplates),
preJoinTemplate: this.setupPreJoinTemplate(externalDirectives),
// Individual templates
chatPanelTemplate: this.setupChatPanelTemplate(externalDirectives, defaultTemplates),
participantsPanelTemplate: this.setupParticipantsPanelTemplate(externalDirectives, defaultTemplates),
activitiesPanelTemplate: this.setupActivitiesPanelTemplate(externalDirectives, defaultTemplates),
participantPanelItemTemplate: this.setupParticipantPanelItemTemplate(externalDirectives, defaultTemplates),
streamTemplate: this.setupStreamTemplate(externalDirectives, defaultTemplates),
participantPanelAfterLocalParticipantTemplate: this.setupParticipantPanelAfterLocalParticipantTemplate(externalDirectives)
};
// Optional templates
if (externalDirectives.toolbarAdditionalButtons) {
config.toolbarAdditionalButtonsTemplate = externalDirectives.toolbarAdditionalButtons.template;
this.log.d('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS');
}
if (externalDirectives.toolbarAdditionalPanelButtons) {
config.toolbarAdditionalPanelButtonsTemplate = externalDirectives.toolbarAdditionalPanelButtons.template;
this.log.d('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS');
}
if (externalDirectives.additionalPanels) {
config.additionalPanelsTemplate = externalDirectives.additionalPanels.template;
this.log.d('Setting EXTERNAL ADDITIONAL PANELS');
}
if (externalDirectives.participantPanelItemElements) {
config.participantPanelItemElementsTemplate = externalDirectives.participantPanelItemElements.template;
this.log.d('Setting EXTERNAL PARTICIPANT PANEL ITEM ELEMENTS');
}
if (externalDirectives.layoutAdditionalElements) {
this.log.d('Setting EXTERNAL ADDITIONAL LAYOUT ELEMENTS');
config.layoutAdditionalElementsTemplate = externalDirectives.layoutAdditionalElements.template;
}
this.log.d('Template setup completed', config);
return config;
}
/**
* Sets up the participantPanelAfterLocalParticipant template
*/
private setupParticipantPanelAfterLocalParticipantTemplate(externalDirectives: ExternalDirectives): TemplateRef<any> | undefined {
if (externalDirectives.participantPanelAfterLocalParticipant) {
this.log.d('Setting EXTERNAL PARTICIPANT PANEL AFTER LOCAL PARTICIPANT');
return (externalDirectives.participantPanelAfterLocalParticipant as any).template;
}
return undefined;
}
/**
* Sets up the toolbar template
*/
private setupToolbarTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.toolbar) {
this.log.d('Setting EXTERNAL TOOLBAR');
return externalDirectives.toolbar.template;
} else {
this.log.d('Setting DEFAULT TOOLBAR');
return defaultTemplates.toolbar;
}
}
/**
* Sets up the panel template
*/
private setupPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.panel) {
this.log.d('Setting EXTERNAL PANEL');
return externalDirectives.panel.template;
} else {
this.log.d('Setting DEFAULT PANEL');
return defaultTemplates.panel;
}
}
/**
* Sets up the layout template
*/
private setupLayoutTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.layout) {
this.log.d('Setting EXTERNAL LAYOUT');
return externalDirectives.layout.template;
} else {
this.log.d('Setting DEFAULT LAYOUT');
return defaultTemplates.layout;
}
}
/**
* Sets up the prejoin template
*/
private setupPreJoinTemplate(externalDirectives: ExternalDirectives): TemplateRef<any> | undefined {
if (externalDirectives.preJoin) {
this.log.d('Setting EXTERNAL PREJOIN');
return externalDirectives.preJoin.template;
} else {
this.log.d('Setting DEFAULT PREJOIN (none)');
return undefined;
}
}
/**
* Sets up the chat panel template
*/
private setupChatPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.chatPanel) {
this.log.d('Setting EXTERNAL CHAT PANEL');
return externalDirectives.chatPanel.template;
} else {
this.log.d('Setting DEFAULT CHAT PANEL');
return defaultTemplates.chatPanel;
}
}
/**
* Sets up the participants panel template
*/
private setupParticipantsPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.participantsPanel) {
this.log.d('Setting EXTERNAL PARTICIPANTS PANEL');
return externalDirectives.participantsPanel.template;
} else {
this.log.d('Setting DEFAULT PARTICIPANTS PANEL');
return defaultTemplates.participantsPanel;
}
}
/**
* Sets up the activities panel template
*/
private setupActivitiesPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.activitiesPanel) {
this.log.d('Setting EXTERNAL ACTIVITIES PANEL');
return externalDirectives.activitiesPanel.template;
} else {
this.log.d('Setting DEFAULT ACTIVITIES PANEL');
return defaultTemplates.activitiesPanel;
}
}
/**
* Sets up the participant panel item template
*/
private setupParticipantPanelItemTemplate(
externalDirectives: ExternalDirectives,
defaultTemplates: DefaultTemplates
): TemplateRef<any> {
if (externalDirectives.participantPanelItem) {
this.log.d('Setting EXTERNAL PARTICIPANT PANEL ITEM');
return externalDirectives.participantPanelItem.template;
} else {
this.log.d('Setting DEFAULT PARTICIPANT PANEL ITEM');
return defaultTemplates.participantPanelItem;
}
}
/**
* Sets up the stream template
*/
private setupStreamTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.stream) {
this.log.d('Setting EXTERNAL STREAM');
return externalDirectives.stream.template;
} else {
this.log.d('Setting DEFAULT STREAM');
return defaultTemplates.stream;
}
}
/**
* Sets up templates for the PanelComponent
*/
setupPanelTemplates(
externalParticipantsPanel?: ParticipantsPanelDirective,
externalChatPanel?: ChatPanelDirective,
externalActivitiesPanel?: ActivitiesPanelDirective,
externalAdditionalPanels?: AdditionalPanelsDirective
): PanelTemplateConfiguration {
this.log.d('Setting up panel templates...');
return {
participantsPanelTemplate: externalParticipantsPanel?.template,
chatPanelTemplate: externalChatPanel?.template,
activitiesPanelTemplate: externalActivitiesPanel?.template,
additionalPanelsTemplate: externalAdditionalPanels?.template
};
}
/**
* Sets up templates for the ToolbarComponent
*/
setupToolbarTemplates(
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective
): ToolbarTemplateConfiguration {
this.log.d('Setting up toolbar templates...');
return {
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template,
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template
};
}
/**
* Sets up templates for the LayoutComponent
*/
setupLayoutTemplates(
externalStream?: StreamDirective,
externalLayoutAdditionalElements?: LayoutAdditionalElementsDirective
): LayoutTemplateConfiguration {
this.log.d('Setting up layout templates...');
return {
layoutStreamTemplate: externalStream?.template,
layoutAdditionalElementsTemplate: externalLayoutAdditionalElements?.template
};
}
/**
* Sets up templates for the ParticipantsPanelComponent
*/
setupParticipantsPanelTemplates(
externalParticipantPanelItem?: ParticipantPanelItemDirective,
defaultParticipantPanelItem?: TemplateRef<any>,
externalParticipantPanelAfterLocalParticipant?: TemplateRef<any>
): ParticipantsPanelTemplateConfiguration {
this.log.d('Setting up participants panel templates...');
return {
participantPanelItemTemplate: externalParticipantPanelItem?.template || defaultParticipantPanelItem,
participantPanelAfterLocalParticipantTemplate: externalParticipantPanelAfterLocalParticipant
};
}
/**
* Sets up templates for the ParticipantPanelItemComponent
*/
setupParticipantPanelItemTemplates(
externalParticipantPanelItemElements?: ParticipantPanelItemElementsDirective
): ParticipantPanelItemTemplateConfiguration {
this.log.d('Setting up participant panel item templates...');
return {
participantPanelItemElementsTemplate: externalParticipantPanelItemElements?.template
};
}
/**
* Sets up templates for the SessionComponent
*/
setupSessionTemplates(
toolbarTemplate?: TemplateRef<any>,
panelTemplate?: TemplateRef<any>,
layoutTemplate?: TemplateRef<any>
): SessionTemplateConfiguration {
this.log.d('Setting up session templates...');
return {
toolbarTemplate,
panelTemplate,
layoutTemplate
};
}
}

View File

@ -18,6 +18,7 @@ export * from './lib/components/toolbar/toolbar.component';
export * from './lib/components/videoconference/videoconference.component';
export * from './lib/config/openvidu-components-angular.config';
// Directives
export * from './lib/directives/template/internals.directive';
export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/admin.directive';
export * from './lib/directives/api/api.directive.module';

View File

@ -32,6 +32,17 @@
[activitiesPanelRecordingActivity]="activitiesPanelRecordingActivity"
[activitiesPanelBroadcastingActivity]="activitiesPanelBroadcastingActivity"
[toolbarSettingsButton]="toolbarSettingsButton"
[toolbarViewRecordingsButton]="toolbarViewRecordingsButton"
[recordingActivityShowControls]="{
play: false,
download: false,
delete: false,
externalView: true
}"
[recordingActivityReadOnly]="false"
[recordingActivityStartStopRecordingButton]="recordingActivityStartStopRecordingButton"
[recordingActivityViewRecordingsButton]="recordingActivityViewRecordingsButton"
[recordingActivityShowRecordingsList]="true"
(onTokenRequested)="onTokenRequested($event)"
(onReadyToJoin)="onReadyToJoin()"
(onRoomCreated)="onRoomCreated($event)"
@ -54,6 +65,7 @@
(onBroadcastingStopRequested)="onBroadcastingStopRequested($event)"
(onSettingsPanelStatusChanged)="onSettingsPanelStatusChanged($event)"
(onActivitiesPanelStatusChanged)="onActivitiesPanelStatusChanged($event)"
(onViewRecordingClicked)="onRoomDisconnected()"
>
</ov-videoconference>
}

View File

@ -34,7 +34,6 @@ export class CallComponent implements OnInit {
{ name: 'custom', lang: 'cus' }
];
prejoin: boolean = true;
prejoinDisplayParticipantName: boolean = true;
participantName: string = `Participant${Math.floor(Math.random() * 1000)}`;
videoEnabled: boolean = true;
audioEnabled: boolean = true;
@ -59,6 +58,13 @@ export class CallComponent implements OnInit {
activitiesPanelBroadcastingActivity: boolean = true;
toolbarSettingsButton: boolean = true;
fakeDevices: boolean = false;
// Internal directive inputs (public for E2E)
prejoinDisplayParticipantName: boolean = true;
public recordingActivityViewRecordingsButton: boolean = false;
public recordingActivityStartStopRecordingButton: boolean = true;
toolbarViewRecordingsButton: boolean = false;
private redirectToHomeOnLeaves: boolean = true;
private staticVideos = [
@ -104,8 +110,6 @@ export class CallComponent implements OnInit {
} catch {}
}
if (params['prejoin'] !== undefined) this.prejoin = params['prejoin'] === 'true';
if (params['displayParticipantName'] !== undefined)
this.prejoinDisplayParticipantName = params['displayParticipantName'] === 'true';
if (params['participantName']) this.participantName = params['participantName'];
if (params['videoEnabled'] !== undefined) this.videoEnabled = params['videoEnabled'] === 'true';
if (params['audioEnabled'] !== undefined) this.audioEnabled = params['audioEnabled'] === 'true';
@ -141,6 +145,15 @@ export class CallComponent implements OnInit {
if (params['fakeDevices'] !== undefined) this.fakeDevices = params['fakeDevices'] === 'true';
// Internal/private directive params
if (params['prejoinDisplayParticipantName'] !== undefined)
this.prejoinDisplayParticipantName = params['prejoinDisplayParticipantName'] === 'true';
if (params['recordingActivityViewRecordingsButton'] !== undefined)
this.recordingActivityViewRecordingsButton = params['recordingActivityViewRecordingsButton'] === 'true';
if (params['recordingActivityStartStopRecordingButton'] !== undefined)
this.recordingActivityStartStopRecordingButton = params['recordingActivityStartStopRecordingButton'] === 'true';
if (params['toolbarViewRecordingsButton'] !== undefined)
this.toolbarViewRecordingsButton = params['toolbarViewRecordingsButton'] === 'true';
if (params['redirectToHome'] === undefined) {
this.redirectToHomeOnLeaves = true;
} else {
@ -198,7 +211,9 @@ export class CallComponent implements OnInit {
if (publication.videoTrack?.attachedElements) {
this.replaceWithStaticVideos(publication.videoTrack?.attachedElements);
const firstVideo = this.staticVideos.shift();
this.staticVideos.push(firstVideo);
if (firstVideo) {
this.staticVideos.push(firstVideo);
}
}
}, 2000);
}

View File

@ -41,6 +41,13 @@ Parameters:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
Type: String
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
TurnDomainName:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String
@ -220,6 +227,10 @@ Metadata:
default: S3 bucket for application data and recordings
Parameters:
- S3AppDataBucketName
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -260,10 +271,9 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"ENABLED_MODULES": "none"
}
@ -356,7 +366,7 @@ Resources:
'/usr/local/bin/install.sh':
content: !Sub |
#!/bin/bash -x
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.44.5
@ -399,11 +409,10 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app")"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,openviduMeet")"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
@ -428,14 +437,25 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS
if [[ "${TurnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")
@ -615,10 +635,9 @@ Resources:
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -669,10 +688,9 @@ Resources:
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -913,6 +931,14 @@ Resources:
FromPort: 1935
ToPort: 1935
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
@ -929,14 +955,6 @@ Resources:
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
Outputs:
ServicesAndCredentials:

View File

@ -159,6 +159,8 @@ param adminUsername string
@secure()
param adminSshKey object
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
//Condition for ipValid if is filled
@ -263,11 +265,12 @@ var stringInterpolationParams = {
turnOwnPublicCertificate: turnOwnPublicCertificate
turnOwnPrivateCertificate: turnOwnPrivateCertificate
keyVaultName: keyVaultName
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplate = '''
#!/bin/bash -x
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
apt-get update && apt-get install -y \
@ -297,13 +300,12 @@ DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet")"
# Base command
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)"
@ -326,14 +328,25 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS
if [[ "${turnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}")
@ -462,10 +475,9 @@ export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultN
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
export LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
@ -482,10 +494,9 @@ sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNA
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
@ -532,10 +543,9 @@ GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CONFIG_DIR}/openvidu.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CONFIG_DIR}/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
@ -554,10 +564,9 @@ az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAM
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''
@ -1065,22 +1074,6 @@ resource webServerSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-11
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 190
direction: 'Inbound'
}
}
]
}
}

File diff suppressed because one or more lines are too long

View File

@ -285,6 +285,28 @@
}
]
},
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{
"name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS",
@ -370,7 +392,8 @@
"adminUsername": "[steps('parameters INSTANCE').adminUsername]",
"adminSshKey": "[steps('parameters INSTANCE').adminSshKey]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]"
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
}
}
}

View File

@ -3,7 +3,7 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
@ -15,10 +15,11 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
@ -180,10 +181,11 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -41,6 +41,13 @@ Parameters:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
Type: String
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
TurnDomainName:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String
@ -417,6 +424,10 @@ Metadata:
- OpenViduVPC
- OpenViduMasterNodeSubnet
- OpenViduMediaNodeSubnets
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -462,10 +473,9 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"OPENVIDU_VERSION": "none",
"ENABLED_MODULES": "none"
}
@ -651,7 +661,7 @@ Resources:
content: !Sub |
#!/bin/bash
set -e
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.44.5
@ -723,13 +733,12 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,openviduMeet")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
# Base command
@ -757,14 +766,25 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS
if [[ "${TurnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")
@ -947,10 +967,9 @@ Resources:
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -1006,10 +1025,9 @@ Resources:
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -1320,10 +1338,14 @@ Resources:
docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
TIME_PASSED=0
HEARTBEAT_MAX=1800
# Wait for running containers to not be openvidu, ingress or egress
while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..."
@ -1669,7 +1691,7 @@ Resources:
ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
OpenViduMediaNodeToMasterMeetWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -1701,6 +1723,14 @@ Resources:
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
@ -1717,6 +1747,14 @@ Resources:
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduMasterNodeToMediaNodeRTMPIngress:
Type: AWS::EC2::SecurityGroupIngress
@ -1729,7 +1767,6 @@ Resources:
OpenViduMasterNodeTurnTLSToMediaNodeIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Condition: TurnTLSIsEnabled
Properties:
GroupId: !Ref OpenViduMediaNodeSG
IpProtocol: tcp

View File

@ -293,6 +293,8 @@ param maxNumberOfMediaNodes int = 5
@description('Target CPU percentage to scale up or down')
param scaleTargetCPU int = 50
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
@ -420,11 +422,12 @@ var stringInterpolationParamsMaster = {
openviduLicense: openviduLicense
rtcEngine: rtcEngine
keyVaultName: keyVaultName
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplateMaster = '''
#!/bin/bash -x
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
# Assume azure cli is installed
@ -487,14 +490,13 @@ DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
OPENVIDU_VERSION="$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION "${OPENVIDU_VERSION}")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
# Base command
@ -522,14 +524,24 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS
if [[ "${turnDomainName}" != '' ]]; then
@ -667,10 +679,9 @@ export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultN
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
export LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values
@ -688,10 +699,9 @@ sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNA
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -739,10 +749,9 @@ GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CLUSTER_CONFIG_DIR}/openvidu.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret
@ -762,10 +771,9 @@ az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAM
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''
@ -1110,7 +1118,6 @@ while true; do
sleep $WAIT_INTERVAL
done
set -e
# Get current shared secret
DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)
OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)
@ -1165,9 +1172,13 @@ if [ -x "$(command -v docker)" ]; then
docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
# Wait for running containers to not be openvidu, ingress or egress
while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..."
@ -1187,6 +1198,21 @@ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}"
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
'''
var delete_mediaNode_ScriptMediaTemplate = '''
#!/bin/bash
set -e
az login --identity
RESOURCE_GROUP_NAME=${resourceGroupName}
VM_SCALE_SET_NAME=${vmScaleSetName}
BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId')
INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
'''
@ -1203,6 +1229,10 @@ chmod +x /usr/local/bin/install.sh
echo ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh
chmod +x /usr/local/bin/stop_media_node.sh
# delete_media_node.sh
echo ${base64delete} | base64 -d > /usr/local/bin/delete_media_node.sh
chmod +x /usr/local/bin/delete_media_node.sh
apt-get update && apt-get install -y
apt-get install -y jq
@ -1221,11 +1251,10 @@ az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME -
export HOME="/root"
# Install OpenVidu
/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; }
/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/delete_media_node.sh; }
# Start OpenVidu
systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; }
#/usr/local/bin/set_as_unhealthy.sh
systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/delete_media_node.sh; }
'''
var installScriptMedia = reduce(
@ -1244,9 +1273,18 @@ var stop_media_nodesScriptMedia = reduce(
var base64stopMediaNode = base64(stop_media_nodesScriptMedia)
var delete_mediaNode_ScriptMedia = reduce(
items(stopMediaNodeParams),
{ value: delete_mediaNode_ScriptMediaTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64delete_mediaNode_ScriptMedia = base64(delete_mediaNode_ScriptMedia)
var userDataParamsMedia = {
base64install: base64installMedia
base64stop: base64stopMediaNode
base64delete: base64delete_mediaNode_ScriptMedia
resourceGroupName: resourceGroup().name
vmScaleSetName: '${stackName}-mediaNodeScaleSet'
}
@ -1819,9 +1857,9 @@ resource mediaToMasterV2CompatibilityWebhookIngress 'Microsoft.Network/networkSe
}
}
resource mediaToMasterDefaultAppWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
resource mediaToMasterMeetWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS'
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
@ -1915,6 +1953,22 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
]
}
}
@ -1942,7 +1996,7 @@ resource masterToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/secur
]
destinationPortRange: '1935'
access: 'Allow'
priority: 150
priority: 160
direction: 'Inbound'
}
}
@ -1965,7 +2019,7 @@ resource masterToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/se
]
destinationPortRange: '5349'
access: 'Allow'
priority: 160
priority: 170
direction: 'Inbound'
}
}
@ -1988,7 +2042,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '7880'
access: 'Allow'
priority: 170
priority: 180
direction: 'Inbound'
}
}
@ -2011,7 +2065,7 @@ resource masterToMediaHttpWhipIngress 'Microsoft.Network/networkSecurityGroups/s
]
destinationPortRange: '8080'
access: 'Allow'
priority: 180
priority: 190
direction: 'Inbound'
}
}

File diff suppressed because one or more lines are too long

View File

@ -458,6 +458,28 @@
}
]
},
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{
"name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS",
@ -552,7 +574,8 @@
"datetime": "[steps('parameters SCALING').datetime]",
"automationAccountName": "[steps('parameters SCALING').automationAccountName]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]"
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
}
}
}

View File

@ -3,7 +3,7 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
@ -15,10 +15,11 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
@ -180,10 +181,11 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,7 +3,7 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
@ -15,10 +15,11 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
@ -180,10 +181,11 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

File diff suppressed because it is too large Load Diff

View File

@ -330,6 +330,13 @@ Parameters:
Type: String
Description: Name of the S3 bucket to store cluster data. If empty, a bucket will be created
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
OpenViduVPC:
Description: "Dedicated VPC for OpenVidu cluster"
Type: AWS::EC2::VPC::Id
@ -397,6 +404,10 @@ Metadata:
default: Volumes configuration
Parameters:
- MasterNodesDiskSize
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -407,6 +418,22 @@ Conditions:
TurnTLSIsEnabled: !Or [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]]
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""]
# ---
# Experimental TURN TLS with main domain
ExperimentalTurnTLSWithMainDomain:
Fn::Not:
- Fn::Equals:
- !Ref AdditionalInstallFlags
- !Select [0, !Split ["--experimental-turn-tls-with-main-domain", !Ref AdditionalInstallFlags]]
NotExperimentalTurnTLSWithMainDomain:
Fn::Or:
- Fn::Equals:
- !Ref AdditionalInstallFlags
- !Select [0, !Split ["--experimental-turn-tls-with-main-domain", !Ref AdditionalInstallFlags]]
- Fn::Equals:
- !Ref AdditionalInstallFlags
- ""
# ---
Resources:
@ -438,10 +465,9 @@ Resources:
"GRAFANA_URL": "none",
"GRAFANA_ADMIN_USERNAME": "none",
"GRAFANA_ADMIN_PASSWORD": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"ENABLED_MODULES": "none",
@ -746,7 +772,7 @@ Resources:
content: !Sub |
#!/bin/bash -x
set -e
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.44.5
@ -840,13 +866,12 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,openviduMeet")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
fi
@ -912,10 +937,9 @@ Resources:
DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD')
GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME')
GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD')
DEFAULT_APP_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_USERNAME')
DEFAULT_APP_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_PASSWORD')
DEFAULT_APP_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_USERNAME')
DEFAULT_APP_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_PASSWORD')
MEET_ADMIN_USER=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_USER')
MEET_ADMIN_SECRET=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_SECRET')
MEET_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.MEET_API_KEY')
LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY')
LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET')
ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES')
@ -946,14 +970,25 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
if [[ "${!LIVEKIT_TURN_DOMAIN_NAME}" != "none" ]]; then
COMMON_ARGS+=("--turn-domain-name='${!LIVEKIT_TURN_DOMAIN_NAME}'")
fi
@ -1074,10 +1109,9 @@ Resources:
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -1127,10 +1161,9 @@ Resources:
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -1561,10 +1594,14 @@ Resources:
docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
TIME_PASSED=0
HEARTBEAT_MAX=1800
# Wait for running containers to not be openvidu, ingress or egress
while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..."
@ -2003,7 +2040,7 @@ Resources:
ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMasterToMasterDefaultAppIngress:
OpenViduMasterToMasterMeetIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
@ -2012,7 +2049,7 @@ Resources:
ToPort: 6080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
OpenViduMediaNodeToMasterMeetWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -2044,6 +2081,14 @@ Resources:
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
@ -2060,6 +2105,14 @@ Resources:
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduLoadBalancerToMediaNodeRTMPIngressSG:
Type: AWS::EC2::SecurityGroupIngress
@ -2118,6 +2171,29 @@ Resources:
ToPort: 8080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
# ---
# Experimental TURN TLS with main domain
OpenViduTurnTLSMasterNodeToMediaNodeIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
GroupId: !Ref OpenViduMediaNodeSG
IpProtocol: tcp
FromPort: 5349
ToPort: 5349
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduTurnTLSLoadBalancerToMediaNodeIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG
# ---
OpenViduLoadBalancerSG:
Type: AWS::EC2::SecurityGroup
Properties:
@ -2208,6 +2284,7 @@ Resources:
OpenViduMasterNodeListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Condition: NotExperimentalTurnTLSWithMainDomain
Properties:
DefaultActions:
- Type: forward
@ -2218,6 +2295,22 @@ Resources:
Certificates:
- CertificateArn: !Ref OpenViduCertificateARN
# ---
# Experimental TURN TLS with main domain
OpenViduMasterNodeWithTurnTLSListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref OpenViduMasterNodeWithTurnTLSTG
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: TLS
Certificates:
- CertificateArn: !Ref OpenViduCertificateARN
# ---
OpenViduRTMPMediaNodeListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
@ -2245,6 +2338,7 @@ Resources:
OpenViduMasterNodeTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: NotExperimentalTurnTLSWithMainDomain
Properties:
Name:
Fn::Join:
@ -2278,6 +2372,45 @@ Resources:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group
# ---
# Experimental TURN TLS with main domain
OpenViduMasterNodeWithTurnTLSTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
Name:
Fn::Join:
# Generate a not too long and unique target id
# Getting a unique identifier from the stack id
- ''
- - OVTurnTLSMaster-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
TargetType: instance
Targets:
- Id: !Ref OpenViduMasterNode1
- Id: !Ref OpenViduMasterNode2
- Id: !Ref OpenViduMasterNode3
- Id: !Ref OpenViduMasterNode4
VpcId: !Ref OpenViduVPC
Port: 443
Protocol: TCP
Matcher:
HttpCode: '200'
HealthCheckIntervalSeconds: 10
HealthCheckPath: /health/caddy
HealthCheckProtocol: HTTP
HealthCheckPort: '7880'
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 3
UnhealthyThresholdCount: 4
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60
Tags:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN TLS Master Target Group
# ---
OpenViduMediaNodeRTMPTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:

View File

@ -39,12 +39,6 @@ param turnOwnPublicCertificate string = ''
@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.')
param turnOwnPrivateCertificate string = ''
@description('Name of the PublicIPAddress resource in Azure when using TURN server with TLS')
param turnPublicIpAddressObject object = {
name: ''
id: ''
}
@description('Visit https://openvidu.io/account')
@secure()
param openviduLicense string
@ -302,6 +296,8 @@ param maxNumberOfMediaNodes int = 5
@description('Target CPU percentage to scale up or down')
param scaleTargetCPU int = 50
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
var masterNodeVMSettings = {
@ -442,6 +438,7 @@ var stringInterpolationParamsMaster1 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '1'
additionalInstallFlags: additionalInstallFlags
}
var stringInterpolationParamsMaster2 = {
@ -458,6 +455,7 @@ var stringInterpolationParamsMaster2 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '2'
additionalInstallFlags: additionalInstallFlags
}
var stringInterpolationParamsMaster3 = {
@ -474,6 +472,7 @@ var stringInterpolationParamsMaster3 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '3'
additionalInstallFlags: additionalInstallFlags
}
var stringInterpolationParamsMaster4 = {
@ -490,12 +489,13 @@ var stringInterpolationParamsMaster4 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '4'
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplateMaster = '''
#!/bin/bash -x
set -e
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
# Assume azure cli is installed
@ -571,14 +571,13 @@ if [[ $MASTER_NODE_NUM -eq 1 ]] && [[ "$ALL_SECRETS_GENERATED" == "" || "$ALL_SE
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
OPENVIDU_VERSION="$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION "${OPENVIDU_VERSION}")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
fi
@ -626,10 +625,9 @@ GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --
GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
@ -660,16 +658,27 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
if [[ $LIVEKIT_TURN_DOMAIN_NAME != "" ]]; then
COMMON_ARGS+=("--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME}")
COMMON_ARGS+=("--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME")
fi
# Certificate arguments
@ -792,10 +801,9 @@ export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultN
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
export LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values
@ -813,10 +821,9 @@ sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNA
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -864,10 +871,9 @@ GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CLUSTER_CONFIG_DIR}/openvidu.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret
@ -887,10 +893,9 @@ az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAM
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''
@ -1060,7 +1065,10 @@ var store_secretScriptMaster = reduce(
var blobStorageParams = {
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : existingStorageAccount.name
storageAccountKey: listKeys(storageAccount.id, '2021-04-01').keys[0].value
storageAccountContainerName: isEmptyContainerName ? 'openvidu-appdata' : '${containerName}'
storageAccountContainerName: isEmptyAppDataContainerName ? 'openvidu-appdata' : '${appDataContainerName}'
storageAccountClusterContainerName: isEmptyClusterContainerName
? 'openvidu-clusterdata'
: '${clusterDataContainerName}'
}
var config_blobStorageScript = reduce(
@ -1489,9 +1497,13 @@ if [ -x "$(command -v docker)" ]; then
docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
# Wait for running containers to not be openvidu, ingress or egress
while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..."
@ -1511,6 +1523,21 @@ RESOURCE_ID=/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP_NAME/
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}"
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
'''
var delete_mediaNode_ScriptMediaTemplate = '''
#!/bin/bash
set -e
az login --identity
RESOURCE_GROUP_NAME=${resourceGroupName}
VM_SCALE_SET_NAME=${vmScaleSetName}
BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId')
INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
'''
@ -1545,10 +1572,10 @@ az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME -
export HOME="/root"
# Install OpenVidu
/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; }
/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/delete_media_node.sh; }
# Start OpenVidu
systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; }
systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/delete_media_node.sh; }
'''
var installScriptMedia = reduce(
@ -1563,12 +1590,20 @@ var stop_media_nodesScriptMedia = reduce(
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var delete_mediaNode_ScriptMedia = reduce(
items(stopMediaNodeParams),
{ value: delete_mediaNode_ScriptMediaTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64installMedia = base64(installScriptMedia)
var base64stopMediaNode = base64(stop_media_nodesScriptMedia)
var base64delete_mediaNode_ScriptMedia = base64(delete_mediaNode_ScriptMedia)
var userDataParamsMedia = {
base64install: base64installMedia
base64stop: base64stopMediaNode
base64delete_mediaNode: base64delete_mediaNode_ScriptMedia
resourceGroupName: resourceGroup().name
vmScaleSetName: '${stackName}-mediaNodeScaleSet'
}
@ -1865,7 +1900,6 @@ resource scaleInActivityLogRule 'Microsoft.Insights/activityLogAlerts@2020-10-01
/*------------------------------------------- NETWORK -------------------------------------------*/
var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
var turnIsEmptyIp = turnPublicIpAddressObject.newOrExistingOrNone == 'none'
var lbName = '${stackName}-loadBalancer'
var lbFrontEndName = 'LoadBalancerFrontEnd'
var lbBackendPoolNameMasterNode = 'LoadBalancerBackEndMasterNode'
@ -1894,32 +1928,6 @@ resource publicIP_LoadBalancer_ifNew 'Microsoft.Network/publicIPAddresses@2023-1
name: publicIpAddressObject.name
}
var ipTURNEmpty = turnPublicIpAddressObject.newOrExistingOrNone == 'none'
resource publicIPAddressTurnTLSLoadBalancer 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (ipTURNEmpty && turnTLSIsEnabled == true) {
name: '${stackName}-publicIPAddressTurnTLSLoadBalancer'
location: location
sku: {
name: 'Standard'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
}
}
var ipTURNExists = turnPublicIpAddressObject.newOrExistingOrNone == 'existing'
resource publicIP_TurnTLSLoadBalancer_ifExisting 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (ipTURNExists && turnTLSIsEnabled == true) {
name: turnPublicIpAddressObject.name
}
var ipTURNNew = turnPublicIpAddressObject.newOrExistingOrNone == 'new'
resource publicIP_TurnTLSLoadBalancer_ifNew 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (ipTURNNew && turnTLSIsEnabled == true) {
name: turnPublicIpAddressObject.name
}
resource LoadBalancer 'Microsoft.Network/loadBalancers@2024-05-01' = {
name: lbName
location: location
@ -2035,74 +2043,6 @@ resource LoadBalancer 'Microsoft.Network/loadBalancers@2024-05-01' = {
}
}
var tlbName = '${stackName}-turnloadBalancer'
var tlbFrontEndName = 'TurnLoadBalancerFrontEnd'
resource TurnTLSLoadbalancer 'Microsoft.Network/loadBalancers@2024-05-01' = if (turnTLSIsEnabled == true) {
name: tlbName
location: location
sku: {
name: 'Standard'
}
properties: {
frontendIPConfigurations: [
{
name: tlbFrontEndName
properties: {
privateIPAllocationMethod: 'Dynamic'
privateIPAddressVersion: 'IPv4'
publicIPAddress: {
id: turnIsEmptyIp
? publicIPAddressTurnTLSLoadBalancer.id
: ipTURNNew ? publicIP_TurnTLSLoadBalancer_ifNew.id : publicIP_TurnTLSLoadBalancer_ifExisting.id
}
}
}
]
backendAddressPools: [
{
name: lbBackendPoolNameMasterNode
}
]
loadBalancingRules: [
{
name: 'TURNTLSRuleforMasterNode'
properties: {
frontendIPConfiguration: {
id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', tlbName, tlbFrontEndName)
}
backendAddressPool: {
id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', tlbName, lbBackendPoolNameMasterNode)
}
frontendPort: 443
backendPort: 443
enableFloatingIP: false
protocol: 'Tcp'
enableTcpReset: true
loadDistribution: 'Default'
disableOutboundSnat: true
probe: {
id: resourceId('Microsoft.Network/loadBalancers/probes', tlbName, 'probeForHTTPSRuleMasterNode')
}
}
}
]
probes: [
{
name: 'probeForTURNTLSRuleMasterNode'
properties: {
protocol: 'Http'
requestPath: '/'
port: 443
probeThreshold: 3
intervalInSeconds: 10
numberOfProbes: 5
}
}
]
}
}
resource natGateway 'Microsoft.Network/natGateways@2021-05-01' = {
name: '${stackName}-natGateway'
location: location
@ -2189,6 +2129,9 @@ resource subnetMasterNode2 'Microsoft.Network/virtualNetworks/subnets@2023-11-01
id: natGateway.id
}
}
dependsOn: [
subnetMasterNode1
]
}
resource netInterfaceMasterNode1 'Microsoft.Network/networkInterfaces@2023-11-01' = {
@ -2778,9 +2721,9 @@ resource mediaToMasterV2CompatibilityWebhookIngress 'Microsoft.Network/networkSe
}
}
resource masterToMasterDefaultApp 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
resource masterToMasterMeet 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'masterNode_to_masterNode_DEFAULTAPP_INGRESS'
name: 'masterNode_to_masterNode_MEET_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
@ -2801,9 +2744,9 @@ resource masterToMasterDefaultApp 'Microsoft.Network/networkSecurityGroups/secur
}
}
resource mediaToMasterDefaultAppWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
resource mediaToMasterMeetWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS'
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
@ -2897,6 +2840,22 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
]
}
}
@ -2920,7 +2879,7 @@ resource loadBalancerToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups
]
destinationPortRange: '1945'
access: 'Allow'
priority: 150
priority: 160
direction: 'Inbound'
}
}
@ -2939,7 +2898,7 @@ resource loadBalancerToMediaHealthcheckIngress 'Microsoft.Network/networkSecurit
]
destinationPortRange: '9092'
access: 'Allow'
priority: 160
priority: 170
direction: 'Inbound'
}
}
@ -2958,7 +2917,7 @@ resource loadBalancerToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGro
]
destinationPortRange: '5349'
access: 'Allow'
priority: 170
priority: 180
direction: 'Inbound'
}
}
@ -2977,7 +2936,7 @@ resource loadBalancerToMediaTurnTlsHealthCheckIngress 'Microsoft.Network/network
]
destinationPortRange: '7880'
access: 'Allow'
priority: 180
priority: 190
direction: 'Inbound'
}
}
@ -3000,7 +2959,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '7880'
access: 'Allow'
priority: 190
priority: 200
direction: 'Inbound'
}
}
@ -3023,7 +2982,7 @@ resource masterToMediaClientIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '8080'
access: 'Allow'
priority: 200
priority: 210
direction: 'Inbound'
}
}
@ -3060,14 +3019,28 @@ resource blobContainerScaleIn 'Microsoft.Storage/storageAccounts/blobServices/co
}
@description('Name of the bucket where OpenVidu will store the recordings if a new Storage account is being creating. If not specified, a default bucket will be created. If you want to use an existing storage account, fill this parameter with the name of the container where the recordings are stored.')
param containerName string = ''
param appDataContainerName string = ''
var isEmptyContainerName = containerName == ''
var isEmptyAppDataContainerName = appDataContainerName == ''
resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) {
name: isEmptyContainerName
name: isEmptyAppDataContainerName
? '${storageAccount.name}/default/openvidu-appdata'
: '${storageAccount.name}/default/${containerName}'
: '${storageAccount.name}/default/${appDataContainerName}'
properties: {
publicAccess: 'None'
}
}
@description('Name of the bucket where OpenVidu will store the recordings if a new Storage account is being creating. If not specified, a default bucket will be created. If you want to use an existing storage account, fill this parameter with the name of the container where the recordings are stored.')
param clusterDataContainerName string = ''
var isEmptyClusterContainerName = clusterDataContainerName == ''
resource clusterDatablobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) {
name: isEmptyClusterContainerName
? '${storageAccount.name}/default/openvidu-clusterdata'
: '${storageAccount.name}/default/${clusterDataContainerName}'
properties: {
publicAccess: 'None'
}

File diff suppressed because one or more lines are too long

View File

@ -473,6 +473,28 @@
}
]
},
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{
"name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS",
@ -534,25 +556,6 @@
},
"infoMessages": [],
"visible": true
},
{
"name": "turnPublicIpAddressObject",
"type": "Microsoft.Network.PublicIpAddressCombo",
"label": {
"publicIpAddress": "Turn Public Ip Address"
},
"toolTip": {
"publicIpAddress": "Name of the PublicIPAddress resource in Azure when using TURN server with TLS"
},
"defaultValue": {
"publicIpAddressName": "defaultName"
},
"options": {
"hideNone": false,
"hideDomainNameLabel": true,
"hideExisting": false
},
"visible": true
}
]
}
@ -573,7 +576,6 @@
"turnDomainName": "[steps('parameters TURN').turnDomainName]",
"turnOwnPublicCertificate": "[steps('parameters TURN').turnOwnPublicCertificate]",
"turnOwnPrivateCertificate": "[steps('parameters TURN').turnOwnPrivateCertificate]",
"turnPublicIpAddressObject": "[steps('parameters TURN').turnPublicIpAddressObject]",
"openviduLicense": "[steps('parameters OPENVIDU').openviduLicense]",
"rtcEngine": "[steps('parameters OPENVIDU').rtcEngine]",
"masterNodeInstanceType": "[steps('parameters INSTANCE').masterNodeInstanceType]",
@ -588,7 +590,8 @@
"datetime": "[steps('parameters SCALING').datetime]",
"automationAccountName": "[steps('parameters SCALING').automationAccountName]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]"
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
}
}
}

View File

@ -3,7 +3,7 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
@ -15,10 +15,11 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
@ -180,10 +181,11 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,7 +3,7 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
@ -15,10 +15,11 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
@ -180,10 +181,11 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -41,6 +41,13 @@ Parameters:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
Type: String
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
TurnDomainName:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String
@ -240,6 +247,10 @@ Metadata:
default: S3 bucket for application data and recordings
Parameters:
- S3AppDataBucketName
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -282,10 +293,9 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"ENABLED_MODULES": "none"
}
@ -378,7 +388,7 @@ Resources:
'/usr/local/bin/install.sh':
content: !Sub |
#!/bin/bash -x
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.44.5
@ -423,11 +433,10 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app,v2compatibility")"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,openviduMeet,v2compatibility")"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
@ -454,14 +463,25 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS
if [[ "${TurnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")
@ -643,10 +663,9 @@ Resources:
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -699,10 +718,9 @@ Resources:
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -951,6 +969,14 @@ Resources:
FromPort: 7885
ToPort: 7885
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 50000
ToPort: 60000

View File

@ -170,6 +170,8 @@ param adminUsername string
@secure()
param adminSshKey object
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
//Condition for ipValid if is filled
@ -276,11 +278,12 @@ var stringInterpolationParams = {
keyVaultName: keyVaultName
openviduLicense: openviduLicense
rtcEngine: rtcEngine
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplate = '''
#!/bin/bash -x
OPENVIDU_VERSION=3.2.0
OPENVIDU_VERSION=main
DOMAIN=
apt-get update && apt-get install -y \
@ -311,13 +314,12 @@ DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
# Base command
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)"
@ -342,14 +344,25 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS
if [[ "${turnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}")
@ -480,17 +493,16 @@ export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultN
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
export LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values
sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/" "${CONFIG_DIR}/openvidu.env"
@ -502,10 +514,9 @@ sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNA
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
@ -541,8 +552,8 @@ fi
REDIS_PASSWORD="$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${CONFIG_DIR}/openvidu.env")"
DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")"
OPENVIDU_RTC_ENGINE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${CLUSTER_CONFIG_DIR}/openvidu.env")"
OPENVIDU_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${CLUSTER_CONFIG_DIR}/openvidu.env")"
OPENVIDU_RTC_ENGINE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${CONFIG_DIR}/openvidu.env")"
OPENVIDU_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")"
MONGO_REPLICA_SET_KEY="$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${CONFIG_DIR}/openvidu.env")"
@ -554,10 +565,9 @@ GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CONFIG_DIR}/openvidu.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CONFIG_DIR}/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
@ -578,10 +588,9 @@ az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAM
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''

File diff suppressed because one or more lines are too long

View File

@ -335,6 +335,28 @@
}
]
},
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{
"name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS",
@ -422,7 +444,8 @@
"adminUsername": "[steps('parameters INSTANCE').adminUsername]",
"adminSshKey": "[steps('parameters INSTANCE').adminSshKey]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]"
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
}
}
}

View File

@ -3,7 +3,7 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
@ -15,10 +15,11 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
@ -180,10 +181,11 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,7 +3,7 @@ set -eu
export INSTALL_PREFIX="${INSTALL_PREFIX:-/opt/openvidu}"
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export REGISTRY="${REGISTRY:-docker.io}"
export UPDATER_IMAGE="${UPDATER_IMAGE:-${REGISTRY}/openvidu/openvidu-updater:${OPENVIDU_VERSION}}"

View File

@ -59,6 +59,7 @@
<version.dockerjava>3.4.1</version.dockerjava>
<version.stringsimilarity>2.0.0</version.stringsimilarity>
<version.openvidu-test-browsers>1.1.0</version.openvidu-test-browsers>
<version.minio>8.5.17</version.minio>
</properties>
<build>
@ -122,6 +123,11 @@
<artifactId>java-string-similarity</artifactId>
<version>${version.stringsimilarity}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${version.minio}</version>
</dependency>
</dependencies>
</project>

View File

@ -17,6 +17,10 @@
package io.openvidu.test.e2e;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
@ -44,6 +48,7 @@ import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.google.common.collect.ImmutableList;
@ -52,6 +57,16 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.minio.ListObjectsArgs;
import io.minio.MinioClient;
import io.minio.Result;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.InvalidResponseException;
import io.minio.errors.ServerException;
import io.minio.errors.XmlParserException;
import io.minio.messages.Item;
import io.openvidu.test.e2e.annotations.OnlyMediasoup;
import io.openvidu.test.e2e.annotations.OnlyPion;
import livekit.LivekitIngress.IngressInfo;
@ -116,6 +131,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
user.getDriver().findElement(By.id("one2one-btn")).click();
user.getEventManager().waitUntilEventReaches("signalConnected", "RoomEvent", 2);
user.getEventManager().waitUntilEventReaches("connected", "RoomEvent", 2);
user.getEventManager().waitUntilEventReaches("participantActive", "RoomEvent", 2);
user.getEventManager().waitUntilEventReaches("connectionStateChanged", "RoomEvent", 2);
user.getEventManager().waitUntilEventReaches("localTrackPublished", "RoomEvent", 4);
user.getEventManager().waitUntilEventReaches("localTrackPublished", "ParticipantEvent", 4);
@ -159,6 +175,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
user.getEventManager().waitUntilEventReaches("connected", "RoomEvent", 2);
user.getEventManager().waitUntilEventReaches("connectionStateChanged", "RoomEvent", 2);
user.getEventManager().waitUntilEventReaches("participantConnected", "RoomEvent", 1);
user.getEventManager().waitUntilEventReaches("participantActive", "RoomEvent", 1);
// Broadcast signal
Collection<Entry<String, String>> assertions = new ArrayList<>();
@ -1507,6 +1524,152 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
gracefullyLeaveParticipants(user, 2);
}
@Test
@DisplayName("Egress")
void egressTest() throws Exception {
OpenViduTestappUser user = setupBrowserAndConnectToOpenViduTestapp("chrome");
log.info("Egress test");
this.addPublisherSubscriber(user, true, true);
user.getDriver().findElement(By.cssSelector(".connect-btn")).sendKeys(Keys.ENTER);
user.getEventManager().waitUntilEventReaches("localTrackPublished", "RoomEvent", 2);
user.getDriver().findElement(By.cssSelector("#room-api-btn-0")).click();
Thread.sleep(300);
// Room composite
this.egressTestByType(user, "room-composite");
// Track composite
this.egressTestByType(user, "track-composite");
// Track
this.egressTestByType(user, "track");
}
private void egressTestByType(OpenViduTestappUser user, String egressType) throws Exception {
user.getEventManager().clearAllCurrentEvents();
user.getDriver().findElement(By.id("egress-id-field")).clear();
if ("track".equals(egressType)) {
user.getDriver().findElement(By.id("audio-track-id-field"))
.sendKeys(Keys.chord(Keys.CONTROL, "a", Keys.DELETE));
}
user.getDriver().findElement(By.cssSelector("#start-" + egressType + "-egress-api-btn")).click();
// TODO: UNCOMMENT WHEN MEDIASOUP IS ABLE TO MANAGE ABRUPT
// CONNECTIONSTATECHANGED DISOCONNECTED/FAILED
// https://mediasoup.discourse.group/t/mediasoup-3-13-20-released-with-server-side-ice-consent-check-and-a-critical-fix/5864
// user.getEventManager().waitUntilEventReaches("recordingStatusChanged",
// "RoomEvent", 1);
WebElement egressIdField = user.getDriver().findElement(By.id("egress-id-field"));
WebDriverWait wait = new WebDriverWait(user.getDriver(), Duration.ofSeconds(10));
wait.until(ExpectedConditions.textToBePresentInElementValue(egressIdField, "EG_"));
String egressId = egressIdField.getDomProperty("value");
this.waitUntilEgressStatus(user, egressId, "EGRESS_ACTIVE", 10000);
Thread.sleep(1000);
user.getDriver().findElement(By.cssSelector("#stop-egress-api-btn")).click();
JsonObject egress = this.waitUntilEgressStatus(user, egressId, "EGRESS_COMPLETE", 10000);
this.checkRecordingInBucket(egress);
// TODO: UNCOMMENT WHEN MEDIASOUP IS ABLE TO MANAGE ABRUPT
// CONNECTIONSTATECHANGED DISOCONNECTED/FAILED
// https://mediasoup.discourse.group/t/mediasoup-3-13-20-released-with-server-side-ice-consent-check-and-a-critical-fix/5864
// user.getEventManager().waitUntilEventReaches("recordingStatusChanged",
// "RoomEvent", 2);
}
private JsonObject waitUntilEgressStatus(OpenViduTestappUser user, String egressId, String egressStatus,
int timeoutMillis) {
final int intervalWait = 250;
final int MAX_ITERATIONS = timeoutMillis / intervalWait;
int iteration = 0;
boolean egressActive = false;
JsonObject egressObject = null;
while (!egressActive && iteration < MAX_ITERATIONS) {
iteration++;
try {
user.getDriver().findElement(By.cssSelector("#list-egress-api-btn")).click();
String textareaContent = user.getDriver().findElement(By.cssSelector("#api-response-text-area"))
.getDomProperty("value");
JsonArray egressArray = JsonParser.parseString(textareaContent).getAsJsonArray();
// Find the egress object with the matching egressId
JsonObject targetEgress = null;
for (int i = 0; i < egressArray.size(); i++) {
egressObject = egressArray.get(i).getAsJsonObject();
if (egressId.equals(egressObject.get("egressId").getAsString())) {
targetEgress = egressObject;
break;
}
}
if (targetEgress != null && egressStatus.equals(targetEgress.get("status").getAsString())) {
egressActive = true;
}
} catch (Exception e) {
// Continue polling if there's an exception
}
if (!egressActive) {
try {
Thread.sleep(intervalWait);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (!egressActive) {
Assertions.fail("Timeout waiting for egress '" + egressId + "' to reach status '" + egressStatus + "'");
}
return egressObject;
}
private void checkRecordingInBucket(JsonObject egress) {
MinioClient minioClient = MinioClient.builder().endpoint("localhost", 9000, false)
.credentials("minioadmin", "minioadmin").build();
Iterable<Result<Item>> results = minioClient
.listObjects(ListObjectsArgs.builder().bucket("openvidu-appdata").build());
final boolean[] metadataFound = { false };
final boolean[] videoFound = { false };
results.forEach(result -> {
Item object = null;
try {
object = result.get();
} catch (InvalidKeyException | ErrorResponseException | IllegalArgumentException | InsufficientDataException
| InternalException | InvalidResponseException | NoSuchAlgorithmException | ServerException
| XmlParserException | IOException e) {
e.printStackTrace();
Assertions.fail("Error accessing Minio object");
}
String expectedFileName = egress.get("egressId").getAsString() + ".json";
if (expectedFileName.equals(object.objectName())) {
metadataFound[0] = true;
}
expectedFileName = egress.get("file").getAsJsonObject().get("filename").getAsString();
if (expectedFileName.equals(object.objectName())) {
videoFound[0] = true;
}
});
if (!metadataFound[0]) {
Assertions.fail("Recording metadata file not found in Minio");
}
if (!videoFound[0]) {
Assertions.fail("Recording media file not found in Minio");
}
}
@Test
@DisplayName("Ingress VP8 Simulcast Chrome")
// TODO: remove tag when not forcing VP8 no-simulcast in ingress with mediasoup
@ -1916,7 +2079,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
@Test
@DisplayName("RTSP ingress AAC")
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the RTSP server
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the
// RTSP server
void rtspIngressAACTest() throws Exception {
log.info("RTSP ingress AAC");
String rtspUri = startRtspServer(null, "AAC");
@ -1925,7 +2089,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
@Test
@DisplayName("RTSP ingress MP3")
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the RTSP server
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the
// RTSP server
void rtspIngressMP3Test() throws Exception {
log.info("RTSP ingress MP3");
String rtspUri = startRtspServer(null, "MP3");
@ -1934,7 +2099,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
@Test
@DisplayName("RTSP ingress OPUS")
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the RTSP server
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the
// RTSP server
void rtspIngressOPUSTest() throws Exception {
log.info("RTSP ingress OPUS");
String rtspUri = startRtspServer(null, "OPUS");
@ -1943,7 +2109,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
@Test
@DisplayName("RTSP ingress G711")
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the RTSP server
@Disabled // Audio ingress is flaky, only works if the ingress inmediately connects to the
// RTSP server
void rtspIngressG711Test() throws Exception {
log.info("RTSP ingress G711");
String rtspUri = startRtspServer(null, "G711");
@ -2413,7 +2580,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
}
private void openInfoDialog(OpenViduTestappUser user, WebElement video) {
String videoId = video.getAttribute("id");
String videoId = video.getDomProperty("id");
// Open the track info dialog if required
if (!user.getDriver().findElements(By.cssSelector("app-info-dialog")).isEmpty()) {
// Dialog already opened

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