Compare commits

..

90 Commits

Author SHA1 Message Date
cruizba 4c37d8cf7c openvidu-deployment: Update LiveKit Egress server image to version 1.10.0 across deployment scripts 2025-08-22 15:21:50 +02:00
Carlos Santos 82630f3508 ov-components: Increase Jasmine default timeout interval to 60 seconds and remove obsolete test for BACKGROUND panel on prejoin page 2025-08-22 14:06:09 +02:00
Carlos Santos dfb297a6d2 ov-components: Add E2E tests for virtual backgrounds with setup and cleanup steps 2025-08-22 13:32:18 +02:00
Carlos Santos c8f53a48ce ov-components: Add virtual backgrounds tests and enhance utility functions for background effects 2025-08-22 13:29:27 +02:00
Carlos Santos 1b9277145c ov-components: Fix test case for recording start and stop events by enabling it in events test suite 2025-08-22 12:36:16 +02:00
Carlos Santos ee30c3ce95 ov-components: Update device selection logic and improve UI handling for audio and video devices 2025-08-22 12:30:05 +02:00
Carlos Santos 03fb7c0a93 ov-components: Refactor recording stop event logging to include room name for better traceability 2025-08-22 12:23:21 +02:00
Carlos Santos 1762c43769 ov-components: Refactor prejoin logic and improve observable handling in configuration service 2025-08-21 19:01:51 +02:00
Carlos Santos bcbf24b84d ov-components: Refactor recording list display logic in RecordingActivityComponent 2025-08-21 16:49:50 +02:00
Carlos Santos fc73aca4a2 Revert "ov-components: Update RecordingActivityComponent to avoid showing recordings when not expanded"
This reverts commit 9e9684c4db.
2025-08-21 14:30:44 +02:00
Carlos Santos f92ee9b886 ov-components: Improve prejoin card styles 2025-08-21 13:08:31 +02:00
Carlos Santos 68d855a245 ov-components: Enhanced camera track handling and processor application in virtual background service 2025-08-21 13:08:31 +02:00
Carlos Santos 72e7469012 ov-components: Enhanced prejoin component
- Introduced background effect feature with options for 'none', 'blur', 'office', and 'nature'.
- Enhanced error handling during device initialization with retry logic and user feedback.
- Updated participant name handling to trim whitespace and clear errors on input change.

style(audio-devices): refactor audio device selection UI

- Redesigned audio device selection to use buttons instead of dropdowns for better UX.
- Improved styling for audio toggle button and device selection menu.

style(video-devices): refactor video device selection UI

- Updated video device selection to use buttons for toggling camera and selecting devices.
- Enhanced styling for video toggle button and device selection menu.

style(lang-selector): improve language selection UI

- Redesigned language selector for better usability with compact and full versions.
- Enhanced styling for language selection buttons and menu items.

style(participant-name-input): refactor participant name input field

- Updated participant name input to use a custom styled input field instead of mat-form-field.
- Improved styling for input field and error handling.

style: general UI improvements across components

- Enhanced overall styling for better consistency and user experience across various components.
2025-08-21 13:08:31 +02:00
cruizba 622a2f6707 openvidu-deployment: Change MinIO and Mimir images to use OpenVidu registry instead of bitnami. 2025-08-20 22:38:37 +02:00
Carlos Santos 72697bafa5 ov-components:Enhance logging by adding verbose level and updating log calls in TemplateManagerService 2025-08-19 11:25:56 +02:00
Carlos Santos 4fb4878342 ov-components: Refactor LoggerService to improve logger function creation and cache handling 2025-08-19 10:50:13 +02:00
Carlos Santos 57e76fe69b ov-components: Update participant name handling in Session and Toolbar components 2025-08-18 18:28:13 +02:00
Carlos Santos e1f16a6179 ov-components: Update MATERIAL_ICONS_URL to remove unnecessary icon names 2025-08-18 14:28:20 +02:00
Carlos Santos 9e9684c4db ov-components: Update RecordingActivityComponent to avoid showing recordings when not expanded 2025-08-18 14:00:16 +02:00
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
116 changed files with 7878 additions and 2527 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
@ -411,3 +443,36 @@ jobs:
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_virtual_backgrounds:
needs: test_setup
name: Virtual Backgrounds E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-virtual-backgrounds --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main

View File

@ -89,7 +89,7 @@ describe('Testing API Directives', () => {
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact');
await utils.waitForElement('.language-selector');
const element = await utils.waitForElement('#join-button');
expect(await element.getText()).toEqual('Unirme ahora');
@ -108,20 +108,20 @@ describe('Testing API Directives', () => {
const panelTitle = await utils.waitForElement('.panel-title');
expect(await panelTitle.getText()).toEqual('Configuración');
const element = await utils.waitForElement('#lang-selected-name');
expect(await element.getAttribute('innerText')).toEqual('Español');
const element = await utils.waitForElement('.lang-name');
expect(await element.getAttribute('innerText')).toEqual('Español expand_more');
});
it('should override the LANG OPTIONS', async () => {
await browser.get(`${url}&prejoin=true&langOptions=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact');
await utils.clickOn('#lang-btn-compact');
await utils.waitForElement('.language-selector');
await utils.clickOn('.language-selector');
await browser.sleep(500);
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
await utils.clickOn('.lang-menu-opt');
await utils.clickOn('.language-option');
await browser.sleep(500);
await utils.clickOn('#join-button');
@ -136,12 +136,12 @@ describe('Testing API Directives', () => {
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.waitForElement('.lang-button');
await utils.clickOn('.lang-button');
await utils.waitForElement('.full-lang-button');
await utils.clickOn('.full-lang-button');
await browser.sleep(500);
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
});
it('should show the PREJOIN page', async () => {

View File

@ -1,3 +1,3 @@
export const LAUNCH_MODE = process.env.LAUNCH_MODE || 'DEV';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

View File

@ -92,35 +92,12 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
});
it('should receive the onVideoEnabledChanged event when clicking on the settings panel', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.clickOn('#video-opt');
await utils.waitForElement('ov-video-devices-select');
await utils.clickOn('ov-video-devices-select #camera-button');
// Checking if onVideoEnabledChanged has been received
await utils.waitForElement('#onVideoEnabledChanged-false');
expect(await utils.isPresent('#onVideoEnabledChanged-false')).toBeTrue();
await utils.clickOn('ov-video-devices-select #camera-button');
await utils.waitForElement('#onVideoEnabledChanged-true');
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
});
it('should receive the onVideoDeviceChanged event on prejoin', async () => {
await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#video-devices-form');
await utils.clickOn('#video-devices-form');
await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-dropdown');
await utils.waitForElement('#option-custom_fake_video_1');
await utils.clickOn('#option-custom_fake_video_1');
@ -142,8 +119,8 @@ describe('Testing videoconference EVENTS', () => {
await utils.clickOn('#video-opt');
await utils.waitForElement('ov-video-devices-select');
await utils.waitForElement('#video-devices-form');
await utils.clickOn('#video-devices-form');
await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-dropdown');
await utils.waitForElement('#option-custom_fake_video_1');
await utils.clickOn('#option-custom_fake_video_1');
@ -184,35 +161,12 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
});
it('should receive the onAudioEnabledChanged event when clicking on the settings panel', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.clickOn('#audio-opt');
await utils.waitForElement('ov-audio-devices-select');
await utils.clickOn('ov-audio-devices-select #microphone-button');
// Checking if onAudioEnabledChanged has been received
await utils.waitForElement('#onAudioEnabledChanged-false');
expect(await utils.isPresent('#onAudioEnabledChanged-false')).toBeTrue();
await utils.clickOn('ov-audio-devices-select #microphone-button');
await utils.waitForElement('#onAudioEnabledChanged-true');
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
});
it('should receive the onAudioDeviceChanged event on prejoin', async () => {
await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#audio-devices-form');
await utils.clickOn('#audio-devices-form');
await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#option-custom_fake_audio_1');
await utils.clickOn('#option-custom_fake_audio_1');
@ -234,8 +188,8 @@ describe('Testing videoconference EVENTS', () => {
await utils.clickOn('#audio-opt');
await utils.waitForElement('ov-audio-devices-select');
await utils.waitForElement('#audio-devices-form');
await utils.clickOn('#audio-devices-form');
await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#option-custom_fake_audio_1');
await utils.clickOn('#option-custom_fake_audio_1');
@ -248,8 +202,8 @@ describe('Testing videoconference EVENTS', () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact');
await utils.clickOn('#lang-btn-compact');
await utils.waitForElement('.language-selector');
await utils.clickOn('.language-selector');
await browser.sleep(500);
await utils.clickOn('#lang-opt-es');
@ -269,8 +223,8 @@ describe('Testing videoconference EVENTS', () => {
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.waitForElement('.lang-button');
await utils.clickOn('.lang-button');
await utils.waitForElement('.full-lang-button');
await utils.clickOn('.full-lang-button');
await browser.sleep(500);
await utils.clickOn('#lang-opt-es');
@ -398,7 +352,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onSettingsPanelStatusChanged-false')).toBeTrue();
});
it('should receive the onRecordingStartRequested event when clicking toolbar button', async () => {
it('should receive the onRecordingStartRequested and onRecordingStopRequested event when clicking toolbar button', async () => {
const roomName = 'recordingToolbarEvent';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -410,9 +364,15 @@ describe('Testing videoconference EVENTS', () => {
// Checking if onRecordingStartRequested has been received
await utils.waitForElement(`#onRecordingStartRequested-${roomName}`);
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onRecordingStopRequested event when clicking toolbar button', async () => {});
await utils.waitForElement('.activity-status.started');
await utils.toggleRecordingFromToolbar();
// Checking if onRecordingStopRequested has been received
await utils.waitForElement(`#onRecordingStopRequested-${roomName}`);
expect(await utils.isPresent(`#onRecordingStopRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onBroadcastingStopRequested event when clicking toolbar button', async () => {
await browser.get(`${url}&prejoin=false`);
@ -446,7 +406,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onBroadcastingStopRequested')).toBeTrue();
});
it('should receive the onRecordingStartRequested when clicking from activities panel', async () => {
it('should receive the onRecordingStartRequested and onRecordingStopRequested when clicking from activities panel', async () => {
const roomName = 'recordingActivitiesEvent';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -472,8 +432,6 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onRecordingStopRequested when clicking from activities panel', async () => {});
xit('should receive the onRecordingDeleteRequested event', async () => {
let element;
const roomName = 'deleteRecordingEvent';

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

@ -33,7 +33,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
await browser.get(`${url}&fakeDevices=true`);
let videoDevices = await utils.waitForElement('#video-devices-form');
let videoDevices = await utils.waitForElement('#video-dropdown');
await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click();
@ -63,7 +63,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
await browser.sleep(500);
await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
let videoDevices = await utils.waitForElement('#video-devices-form');
let videoDevices = await utils.waitForElement('#video-dropdown');
await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click();
@ -130,16 +130,15 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
await browser.quit();
});
it('should disable camera and microphone buttons in the prejoin page when permissions are denied', async () => {
it('should camera and microphone buttons be disabled in the prejoin page when permissions are denied', async () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).toBeFalse();
button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).toBeFalse();
await utils.waitForElement('#no-video-device-message');
await utils.waitForElement('#no-audio-device-message');
expect(await utils.isPresent('#backgrounds-button')).toBeFalse();
});
it('should disable camera and microphone buttons in the room page when permissions are denied', async () => {
it('should camera and microphone buttons be disabled in the room page when permissions are denied', async () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
await utils.clickOn('#join-button');
@ -151,7 +150,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
expect(await button.isEnabled()).toBeFalse();
});
it('should disable camera and microphone buttons in the room page without prejoin when permissions are denied', async () => {
it('should camera and microphone buttons be disabled in the room page without prejoin when permissions are denied', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
@ -161,7 +160,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
expect(await button.isEnabled()).toBeFalse();
});
it('should disable camera and microphone device selection buttons in settings when permissions are denied', async () => {
it('should show an audio and video device warning in settings when permissions are denied', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
@ -170,11 +169,9 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
expect(await utils.isPresent('.settings-container')).toBeTrue();
await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).toBeFalse();
await utils.waitForElement('#no-video-device-message');
await utils.clickOn('#audio-opt');
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).toBeFalse();
await utils.waitForElement('#no-audio-device-message');
});
});

View File

@ -29,40 +29,6 @@ describe('Panels: UI Navigation and Section Switching', () => {
await browser.quit();
});
/**
* TODO
* It only works with OpenVidu PRO because this is a PRO feature
*/
// it('should toggle BACKGROUND panel on prejoin page when VIDEO is MUTED', async () => {
// let element;
// await browser.get(`${url}`);
// element = await utils.waitForElement('#pre-join-container');
// expect(await utils.isPresent('#pre-join-container')).toBeTrue();
// const backgroundButton = await utils.waitForElement('#background-effects-btn');
// expect(await utils.isPresent('#background-effects-btn')).toBeTrue();
// expect(await backgroundButton.isEnabled()).toBeTrue();
// await backgroundButton.click();
// await browser.sleep(500);
// await utils.waitForElement('#background-effects-container');
// expect(await utils.isPresent('#background-effects-container')).toBeTrue();
// element = await utils.waitForElement('#camera-button');
// expect(await utils.isPresent('#camera-button')).toBeTrue();
// expect(await element.isEnabled()).toBeTrue();
// await element.click();
// await browser.sleep(500);
// element = await utils.waitForElement('#video-poster');
// expect(await utils.isPresent('#video-poster')).toBeTrue();
// expect(await backgroundButton.isDisplayed()).toBeTrue();
// expect(await backgroundButton.isEnabled()).toBeFalse();
// expect(await utils.isPresent('#background-effects-container')).toBeFalse();
// });
it('should open and close the CHAT panel and verify its content', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();

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

@ -1,4 +1,8 @@
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
import * as fs from 'fs';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
type PNGWithMetadata = PNG & { data: Buffer };
export class OpenViduComponentsPO {
private TIMEOUT = 10 * 1000;
@ -188,6 +192,16 @@ export class OpenViduComponentsPO {
await this.waitForElement('#participants-panel-btn');
await this.clickOn('#participants-panel-btn');
break;
case 'backgrounds':
await this.waitForElement('#more-options-btn');
await this.clickOn('#more-options-btn');
await this.browser.sleep(500);
await this.waitForElement('#virtual-bg-btn');
await this.clickOn('#virtual-bg-btn');
await this.browser.sleep(1000);
break;
case 'settings':
await this.toggleToolbarMoreOptions();
@ -195,5 +209,74 @@ export class OpenViduComponentsPO {
await this.clickOn('#toolbar-settings-btn');
break;
}
await this.browser.sleep(500);
}
async applyBackground(bgId: string) {
await this.waitForElement('ov-background-effects-panel');
await this.browser.sleep(1000);
await this.waitForElement(`#effect-${bgId}`);
await this.clickOn(`#effect-${bgId}`);
await this.browser.sleep(2000);
}
async applyVirtualBackgroundFromPrejoin(bgId: string): Promise<void> {
await this.waitForElement('#backgrounds-button');
await this.clickOn('#backgrounds-button');
await this.applyBackground(bgId);
await this.clickOn('#backgrounds-button');
}
async saveScreenshot(filename: string, element: WebElement) {
const image = await element.takeScreenshot();
fs.writeFileSync(filename, image, 'base64');
}
async expectVirtualBackgroundApplied(
img1Name: string,
img2Name: string,
{
threshold = 0.4,
minDiffPixels = 500,
debug = false
}: {
threshold?: number;
minDiffPixels?: number;
debug?: boolean;
} = {}
): Promise<void> {
const beforeImg = PNG.sync.read(fs.readFileSync(img1Name));
const afterImg = PNG.sync.read(fs.readFileSync(img2Name));
const { width, height } = beforeImg;
const diff = new PNG({ width, height });
// const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {
// threshold: 0.4
// // alpha: 0.5,
// // includeAA: false,
// // diffColor: [255, 0, 0]
// });
const numDiffPixels = pixelmatch(beforeImg.data, afterImg.data, diff.data, width, height, {
threshold
// includeAA: true
});
if (numDiffPixels <= minDiffPixels) {
// Sólo guardar los archivos de debug si falla la prueba
if (debug) {
fs.writeFileSync('before.png', PNG.sync.write(beforeImg));
fs.writeFileSync('after.png', PNG.sync.write(afterImg));
fs.writeFileSync('diff.png', PNG.sync.write(diff));
}
}
expect(numDiffPixels).toBeGreaterThan(minDiffPixels, 'The virtual background was not applied correctly');
// fs.writeFileSync('diff.png', PNG.sync.write(diff));
// expect(numDiffPixels).to.be.greaterThan(500, 'The virtual background was not applied correctly');
}
}

View File

@ -0,0 +1,155 @@
import { Builder, WebDriver } from 'selenium-webdriver';
import { TestAppConfig } from './selenium.conf';
import { OpenViduComponentsPO } from './utils.po.test';
const url = TestAppConfig.appUrl;
describe('Prejoin: Virtual Backgrounds', () => {
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);
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should close BACKGROUNDS on prejoin page when VIDEO is disabled', async () => {
let element;
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
const backgroundButton = await utils.waitForElement('#backgrounds-button');
expect(await utils.isPresent('#backgrounds-button')).toBeTrue();
expect(await backgroundButton.isEnabled()).toBeTrue();
await utils.clickOn('#backgrounds-button');
await browser.sleep(500);
await utils.waitForElement('#background-effects-container');
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
await utils.clickOn('#camera-button');
await browser.sleep(500);
element = await utils.waitForElement('#video-poster');
expect(await utils.isPresent('#video-poster')).toBeTrue();
expect(await backgroundButton.isDisplayed()).toBeTrue();
expect(await backgroundButton.isEnabled()).toBeFalse();
await browser.sleep(1000);
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
});
it('should open and close BACKGROUNDS panel on prejoin page', async () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
const backgroundButton = await utils.waitForElement('#backgrounds-button');
expect(await utils.isPresent('#backgrounds-button')).toBeTrue();
expect(await backgroundButton.isEnabled()).toBeTrue();
await backgroundButton.click();
await browser.sleep(500);
await utils.waitForElement('#background-effects-container');
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
await utils.clickOn('#backgrounds-button');
await browser.sleep(1000);
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
});
it('should apply a background effect on prejoin page', async () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
let videoElement = await utils.waitForElement('.OV_video-element');
await utils.saveScreenshot('before.png', videoElement);
await utils.applyVirtualBackgroundFromPrejoin('1');
await browser.sleep(1000);
videoElement = await utils.waitForElement('.OV_video-element');
await utils.saveScreenshot('after.png', videoElement);
await utils.expectVirtualBackgroundApplied('before.png', 'after.png');
});
});
describe('Room: Virtual Backgrounds', () => {
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);
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should open and close BACKGROUNDS panel in the room', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
await utils.checkToolbarIsPresent();
await utils.togglePanel('backgrounds');
await utils.waitForElement('#background-effects-container');
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
await utils.togglePanel('backgrounds');
await browser.sleep(1000);
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
});
it('should apply a background effect in the room', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
await utils.togglePanel('backgrounds');
await utils.waitForElement('#background-effects-container');
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
let videoElement = await utils.waitForElement('.OV_video-element');
await utils.saveScreenshot('before.png', videoElement);
await utils.applyBackground('1');
await browser.sleep(1000);
videoElement = await utils.waitForElement('.OV_video-element');
await utils.saveScreenshot('after.png', videoElement);
await utils.expectVirtualBackgroundApplied('before.png', 'after.png');
});
});

View File

@ -33,9 +33,10 @@
"@compodoc/compodoc": "^1.1.25",
"@types/jasmine": "^5.1.4",
"@types/node": "20.12.14",
"@types/pngjs": "^6.0.5",
"@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12",
"chromedriver": "136.0.2",
"chromedriver": "138.0.0",
"concat": "^1.0.3",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0",
@ -57,6 +58,8 @@
"lint-staged": "^15.2.10",
"ng-packagr": "19.2.2",
"npm-watch": "^0.13.0",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0",
"prettier": "3.3.3",
"selenium-webdriver": "4.32.0",
"ts-node": "10.9.2",
@ -2937,33 +2940,6 @@
}
}
},
"node_modules/@compodoc/compodoc/node_modules/@angular-devkit/schematics/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/@compodoc/compodoc/node_modules/@babel/core": {
"version": "7.25.8",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz",
@ -3136,21 +3112,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@compodoc/compodoc/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@compodoc/compodoc/node_modules/magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
@ -3161,36 +3122,6 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/@compodoc/compodoc/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/@compodoc/compodoc/node_modules/readdirp/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@compodoc/live-server": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz",
@ -6659,6 +6590,16 @@
"@types/node": "*"
}
},
"node_modules/@types/pngjs": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@ -8126,9 +8067,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 +8086,7 @@
"chromedriver": "bin/chromedriver"
},
"engines": {
"node": ">=18"
"node": ">=20"
}
},
"node_modules/cli-cursor": {
@ -15821,6 +15762,19 @@
"@napi-rs/nice": "^1.0.1"
}
},
"node_modules/pixelmatch": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
"integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
"dev": true,
"license": "ISC",
"dependencies": {
"pngjs": "^7.0.0"
},
"bin": {
"pixelmatch": "bin/pixelmatch"
}
},
"node_modules/pkg-dir": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
@ -15925,6 +15879,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/portfinder": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz",

View File

@ -25,9 +25,10 @@
"@compodoc/compodoc": "^1.1.25",
"@types/jasmine": "^5.1.4",
"@types/node": "20.12.14",
"@types/pngjs": "^6.0.5",
"@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12",
"chromedriver": "136.0.2",
"chromedriver": "138.0.0",
"concat": "^1.0.3",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0",
@ -49,6 +50,8 @@
"lint-staged": "^15.2.10",
"ng-packagr": "19.2.2",
"npm-watch": "^0.13.0",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0",
"prettier": "3.3.3",
"selenium-webdriver": "4.32.0",
"ts-node": "10.9.2",
@ -84,18 +87,20 @@
"lib:pack": "cd ./dist/openvidu-components-angular && npm pack",
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/frontend",
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
"e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js",
"e2e:nested-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js",
"e2e: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-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
"e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
"e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
"e2e:lib-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
"e2e:lib-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/panels.test.js",
"e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
"e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
"e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
"e2e:lib-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/panels.test.js",
"e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
"e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
"e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
"e2e:lib-virtual-backgrounds": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/virtual-backgrounds.test.js",
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
"husky": "cd .. && husky install"
},

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

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

@ -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,8 +293,97 @@ 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;
@ -239,4 +395,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,10 +1,17 @@
<div class="panel-container" id="background-effects-container">
<div class="panel-header-container">
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon>
<div class="panel-container" id="background-effects-container" [class.prejoin-mode]="mode === 'prejoin'">
@if (mode === 'meeting') {
<div class="panel-header-container">
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon>
</button>
</div>
} @else {
<button class="pansel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>arrow_back</mat-icon>
</button>
</div>
}
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
<div>

View File

@ -1,5 +1,11 @@
.prejoin-mode {
margin: 0 10px 0px 10px;
max-height: 100%;
min-height: 100%;
}
.background-title {
color: var(--ov-text-surface-color);
margin: 10px 0;
}
.effects-container {
display: block !important;

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
import { PanelType } from '../../../models/panel.model';
@ -16,6 +16,9 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
standalone: false
})
export class BackgroundEffectsPanelComponent implements OnInit {
@Input() mode: 'prejoin' | 'meeting' = 'meeting';
@Output() onClose = new EventEmitter<void>();
backgroundSelectedId: string;
effectType = EffectType;
backgroundImages: BackgroundEffect[] = [];
@ -53,7 +56,11 @@ export class BackgroundEffectsPanelComponent implements OnInit {
}
close() {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
if (this.mode === 'prejoin') {
this.onClose.emit();
} else {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
}
async applyBackground(effect: BackgroundEffect) {

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

@ -54,7 +54,7 @@
::ng-deep .lang-selector .expand-more-icon,
::ng-deep .lang-selector mat-icon {
color: var(--ov-secondary-action-color) !important;
color: var(--ov-text-surface-color) !important;
}
::ng-deep .lang-selector div,

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

@ -1,64 +1,122 @@
<div class="container" id="prejoin-container">
<div *ngIf="isLoading" id="loading-container">
<mat-spinner [diameter]="50"></mat-spinner>
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
<div class="prejoin-container" id="prejoin-container" [class.name-error]="!!_error">
<!-- Top Language Toolbar -->
<div class="top-toolbar" *ngIf="!isMinimal">
<ov-lang-selector [compact]="false" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
</div>
<div *ngIf="!isLoading" id="prejoin-card">
<ov-lang-selector *ngIf="!isMinimal" [compact]="true" class="lang-btn" (onLangChanged)="onLangChanged.emit($event)">
</ov-lang-selector>
<div>
<div class="video-container">
<div id="video-poster">
<ov-media-element
[track]="videoTrack"
[showAvatar]="!videoTrack || videoTrack.isMuted"
[avatarName]="participantName"
[avatarColor]="'hsl(48, 100%, 50%)'"
[isLocal]="true"
></ov-media-element>
</div>
</div>
<div class="media-controls-container">
<!-- Camera -->
<div class="video-controls-container" *ngIf="showCameraButton">
<ov-video-devices-select
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="videoEnabledChanged($event)"
></ov-video-devices-select>
</div>
<!-- Microphone -->
<div class="audio-controls-container" *ngIf="showMicrophoneButton">
<ov-audio-devices-select
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="audioEnabledChanged($event)"
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
></ov-audio-devices-select>
</div>
<div class="participant-name-container" *ngIf="showParticipantName">
<ov-participant-name-input
[isPrejoinPage]="true"
[error]="!!_error"
(onNameUpdated)="onParticipantNameChanged($event)"
(onEnterPressed)="onEnterPressed()"
></ov-participant-name-input>
</div>
<div *ngIf="!!_error" id="token-error">
<span class="error">{{ _error }}</span>
</div>
<div class="join-btn-container">
<button mat-flat-button (click)="joinSession()" id="join-button">
{{ 'PREJOIN.JOIN' | translate }}
</button>
</div>
<!-- Loading State -->
@if (isLoading) {
<div class="loading-overlay">
<div class="loading-content">
<mat-spinner [diameter]="40"></mat-spinner>
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
</div>
</div>
</div>
} @else {
<!-- Main Content -->
<div class="prejoin-content">
<!-- Main Card -->
<div class="prejoin-main">
<!-- Video Preview Section -->
<div class="video-preview-section">
<div class="video-preview-container" [@containerResize]="showBackgroundPanel ? 'compact' : 'normal'">
<div class="video-frame">
<ov-media-element
[track]="videoTrack"
[showAvatar]="!videoTrack || videoTrack.isMuted"
[avatarName]="participantName"
[avatarColor]="'hsl(48, 100%, 50%)'"
[isLocal]="true"
class="video-element"
>
</ov-media-element>
<!-- Video Controls Overlay -->
<div class="video-overlay">
<div class="device-controls">
<div class="control-group" *ngIf="showCameraButton">
<ov-video-devices-select
[compact]="true"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="videoEnabledChanged($event)"
(onVideoDevicesLoaded)="onVideoDevicesLoaded($event)"
class="device-selector"
>
</ov-video-devices-select>
</div>
<div class="control-group" *ngIf="showMicrophoneButton">
<ov-audio-devices-select
[compact]="true"
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="audioEnabledChanged($event)"
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
class="device-selector"
>
</ov-audio-devices-select>
</div>
</div>
<!-- Virtual Background Button -->
@if (backgroundEffectEnabled && hasVideoDevices) {
<div class="background-control">
<button
mat-icon-button
class="background-button"
(click)="toggleBackgroundPanel()"
[matTooltip]="'Virtual Backgrounds'"
[disabled]="!isVideoEnabled"
id="backgrounds-button"
>
<mat-icon class="material-symbols-outlined">background_replace</mat-icon>
</button>
</div>
}
</div>
</div>
</div>
</div>
@if (showBackgroundPanel) {
<div class="vb-container" [@slideInOut]>
<ov-background-effects-panel [mode]="'prejoin'" (onClose)="closeBackgroundPanel()"> </ov-background-effects-panel>
</div>
} @else {
<!-- Configuration Section -->
<div class="configuration-section">
<!-- Participant Name Input -->
<div class="participant-name-container input-section" *ngIf="showParticipantName">
<ov-participant-name-input
[isPrejoinPage]="true"
[error]="!!_error"
(onNameUpdated)="onParticipantNameChanged($event)"
(onEnterPressed)="onEnterPressed()"
class="name-input"
>
</ov-participant-name-input>
</div>
<!-- Error Message -->
<div *ngIf="!!_error" class="error-message" id="token-error">
<mat-icon class="error-icon">error_outline</mat-icon>
<span class="error-text">{{ _error }}</span>
</div>
<!-- Join Button -->
<div class="join-section">
<button
mat-flat-button
(click)="join()"
class="join-button"
id="join-button"
[disabled]="showParticipantName && !participantName"
>
{{ 'PREJOIN.JOIN' | translate }}
</button>
</div>
</div>
}
</div>
</div>
}
</div>

View File

@ -1,159 +1,417 @@
:host {
.container {
height: 100%;
background-color: var(--ov-background-color);
display: flex;
justify-content: center;
align-items: center;
}
display: block;
width: 100%;
height: 100%;
#loading-container {
position: absolute;
top: 40%;
left: 0;
right: 0;
text-align: center;
color: var(--ov-text-primary-color);
.mat-mdc-progress-spinner {
margin: auto;
.prejoin-container {
min-height: 100vh;
background: var(--ov-background-color);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
position: relative;
transition: all 0.3s ease;
&.name-error {
.prejoin-main {
min-height: fit-content;
}
}
.prejoin-content {
display: flex;
justify-content: center;
width: 100%;
.prejoin-main {
max-width: 480px;
width: 100%;
}
}
}
#prejoin-card {
@keyframes slideInFromRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// Top Language Toolbar
.top-toolbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
justify-content: flex-end;
padding: 20px 24px;
background: transparent;
}
// Loading State
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--ov-background-color, #f5f5f5);
z-index: 1000;
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.loading-text {
color: var(--ov-text-primary-color, #333);
font-size: 16px;
font-weight: 500;
}
.mat-mdc-progress-spinner {
--mdc-circular-progress-active-indicator-color: var(--ov-primary-action-color, #4285f4);
}
}
}
// Main Content
.prejoin-main {
width: 100%;
max-width: 520px;
max-height: 544px;
background: var(--ov-surface-color, #ffffff);
border-radius: var(--ov-surface-radius);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: auto;
border-radius: var(--ov-surface-radius);
width: 90%;
max-width: 370px;
// max-height: 650px;
height: min-content;
padding: 55px 30px;
background-color: var(--ov-surface-color);
box-shadow: 6px 4px 20px rgba(0, 0, 0, 0.3);
position: relative;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
::ng-deep .lang-btn {
position: absolute;
top: 10px;
right: 10px;
height: 25px !important;
font-size: 14px !important;
// Video Preview Section
.video-preview-section {
padding: 0;
.video-preview-container {
position: relative;
width: 100%;
aspect-ratio: 4/3;
border-radius: var(--ov-surface-radius) var(--ov-surface-radius) 0 0;
overflow: hidden;
background: #000;
.video-frame {
width: 100%;
height: 100%;
position: relative;
::ng-deep .video-element {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
video {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0;
}
}
}
.video-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
z-index: 9999;
display: flex;
justify-content: center;
align-items: flex-end;
.device-controls {
display: flex;
gap: 12px;
}
.background-control {
position: absolute;
bottom: 16px;
left: 16px;
.background-button {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: #333333;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transform: translateZ(0);
&:active {
transform: translateY(-1px);
transition: all 0.15s ease;
}
&.mat-mdc-button-disabled {
background: rgba(255, 255, 255, 0.137);
color: rgba(233, 233, 233, 0.5);
cursor: not-allowed;
&:hover {
transform: none;
}
}
mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
opacity: 0.9;
transition: opacity 0.2s ease;
}
&:hover mat-icon {
opacity: 1;
}
}
}
}
}
}
::ng-deep .lang-btn mat-icon {
color: var(--ov-text-surface-color) !important;
}
.video-container {
margin: auto;
height: 35vh;
width: 100%;
max-width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#video-poster {
height: 100%;
width: 100%;
position: relative;
border-radius: var(--ov-surface-radius);
.vb-container {
height: fit-content;
overflow: hidden;
}
.media-controls-container {
// Configuration Section
.configuration-section {
padding: 24px 24px 24px; // Added top padding since video has no padding
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
margin-top: 15px;
height: auto;
}
flex-direction: column;
gap: 20px;
.participant-name-container {
display: block !important;
width: 100%;
margin: 10px 0;
}
.input-section {
::ng-deep .name-input {
.mat-mdc-form-field {
width: 100%;
.video-controls-container,
.audio-controls-container {
width: calc(50% - 10px);
margin: 5px 0;
}
.mat-mdc-text-field-wrapper {
border-radius: var(--ov-surface-radius);
background-color: var(--ov-input-background, #f8f9fa);
border: 1px solid var(--ov-border-color, #e0e0e0);
transition: all 0.2s ease;
.join-btn-container {
width: 100%;
margin-top: 15px;
}
&:hover {
border-color: var(--ov-primary-action-color, #4285f4);
}
#join-button {
background-color: var(--ov-primary-action-color);
color: var(--ov-secondary-action-color);
font-weight: bold;
border-radius: var(--ov-surface-radius);
width: 100%;
height: 50px;
transition: background-color 0.3s;
}
&.mdc-text-field--focused {
border-color: var(--ov-primary-action-color, #4285f4);
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
}
}
// #join-button:hover {
// background-color: lighten(var(--ov-primary-action-color), 10%);
// }
.mat-mdc-form-field-subscript-wrapper {
display: none;
}
.error {
font-size: 12px;
font-weight: bold;
font-style: italic;
color: var(--ov-error-color);
margin-top: 5px;
}
input {
font-size: 16px;
font-weight: 500;
color: var(--ov-text-primary-color, #333);
padding: 16px;
@media (max-width: 768px) {
#prejoin-card {
padding: 10px;
&::placeholder {
color: var(--ov-text-secondary-color, #666);
font-weight: 400;
}
}
}
}
}
.video-container {
height: 40vh;
}
.media-controls-container {
flex-direction: column;
.error-message {
display: flex;
align-items: center;
height: auto;
gap: 8px;
padding: 12px 16px;
background-color: rgba(244, 67, 54, 0.08);
border: 1px solid rgba(244, 67, 54, 0.2);
border-radius: var(--ov-surface-radius);
color: var(--ov-error-color, #d32f2f);
.error-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.error-text {
font-size: 14px;
font-weight: 500;
}
}
.video-controls-container,
.audio-controls-container {
width: 100%;
.join-section {
.join-button {
width: 100%;
height: 56px;
background: var(--ov-primary-action-color, #4285f4);
color: white;
border-radius: var(--ov-surface-radius);
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.2);
&:hover:not([disabled]) {
background: var(--ov-primary-action-hover, #3367d6);
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(66, 133, 244, 0.3);
}
&:active:not([disabled]) {
transform: translateY(0);
}
&[disabled] {
background: var(--ov-disabled-color, #ccc);
color: var(--ov-disabled-text-color, #999);
cursor: not-allowed;
box-shadow: none;
}
.join-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
}
}
@media (max-width: 800px) and (orientation: landscape) {
.media-controls-container {
flex-direction: row;
justify-content: space-between;
// Responsive Design
@media (max-width: 640px) {
.prejoin-container {
padding: 16px;
min-height: 100vh;
}
.video-controls-container,
.audio-controls-container {
width: 48%;
.prejoin-main {
max-width: 100%;
border-radius: var(--ov-surface-radius);
}
.video-preview-section {
padding: 0px 0px 12px;
.video-preview-container {
aspect-ratio: 4/3;
}
}
.configuration-section {
padding: 0 20px 20px;
gap: 16px;
}
.top-toolbar {
padding: 16px 20px;
}
}
@media (max-height: 630px) {
.video-container {
height: 30vh;
@media (max-width: 480px) {
.prejoin-container {
padding: 12px;
}
.media-controls-container {
height: auto;
.configuration-section {
padding: 0 16px 16px;
}
.video-overlay .device-controls {
gap: 8px;
::ng-deep .device-selector .mat-mdc-icon-button {
width: 44px;
height: 44px;
mat-icon {
font-size: 18px;
}
}
}
.top-toolbar {
padding: 12px 16px;
}
}
@media (max-height: 640px) {
.prejoin-container {
align-items: flex-start;
padding-top: 60px; // Add space for top toolbar
}
.video-preview-section .video-preview-container {
aspect-ratio: 4/3; // Keep the taller aspect ratio even on small screens
}
}
// Dark theme support
@media (prefers-color-scheme: dark) {
.prejoin-container {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
}
.prejoin-main {
background: #2d2d2d;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
0 2px 8px rgba(0, 0, 0, 0.2);
}
.configuration-section .input-section ::ng-deep .name-input .participant-name-input-container .input-wrapper {
background-color: #3a3a3a;
border-color: #555;
}
}
// Animation keyframes
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.prejoin-main {
animation: fadeIn 0.3s ease-out;
transform: translateZ(0);
}
}

View File

@ -9,7 +9,8 @@ import {
OnInit,
Output
} from '@angular/core';
import { Subscription } from 'rxjs';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { filter, Subject, take, 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 +20,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
@ -29,7 +29,47 @@ import { StorageService } from '../../services/storage/storage.service';
templateUrl: './pre-join.component.html',
styleUrls: ['./pre-join.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
standalone: false,
animations: [
trigger('containerResize', [
state(
'normal',
style({
height: '*'
})
),
state(
'compact',
style({
height: '28vh'
})
),
transition('normal => compact', [animate('250ms cubic-bezier(0.25, 0.8, 0.25, 1)')]),
transition('compact => normal', [animate('350ms cubic-bezier(0.25, 0.8, 0.25, 1)')])
]),
trigger('slideInOut', [
transition(':enter', [
style({
opacity: 0
}),
animate(
'300ms cubic-bezier(0.34, 1.56, 0.64, 1)',
style({
opacity: 1
})
)
]),
transition(':leave', [
animate(
'200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)',
style({
opacity: 0,
transform: 'translateY(-10px)'
})
)
])
])
]
})
export class PreJoinComponent implements OnInit, OnDestroy {
@Input() set error(error: { name: string; message: string } | undefined) {
@ -43,7 +83,6 @@ export class PreJoinComponent implements OnInit, OnDestroy {
@Output() onReadyToJoin = new EventEmitter<any>();
_error: string | undefined;
windowSize: number;
isLoading = true;
participantName: string | undefined = '';
@ -57,15 +96,17 @@ export class PreJoinComponent implements OnInit, OnDestroy {
showLogo: boolean = true;
showParticipantName: boolean = true;
// Future feature preparation
backgroundEffectEnabled: boolean = true; // Enable virtual backgrounds by default
showBackgroundPanel: boolean = false;
videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined;
isVideoEnabled: boolean = false;
hasVideoDevices: boolean = true;
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 +119,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,29 +139,19 @@ 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();
});
}
}
private async initializeDevices() {
try {
this.tracks = await this.openviduService.createLocalTracks();
this.openviduService.setLocalTracks(this.tracks);
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
} catch (error) {
this.log.e('Error creating local tracks:', error);
}
await this.initializeDevicesWithRetry();
}
onDeviceSelectorClicked() {
@ -130,68 +160,98 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.cdkSrv.setSelector('#prejoin-container');
}
joinSession() {
if (this.showParticipantName && !this.participantName) {
join() {
if (this.showParticipantName && !this.participantName?.trim()) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return;
}
// Clear any previous errors
this._error = undefined;
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
// Assign participant name to the observable if it is defined
if(this.participantName) this.libService.setParticipantName(this.participantName);
if (this.participantName?.trim()) {
this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
this.onReadyToJoin.emit();
this.libService.participantName$
.pipe(
filter((name) => name === this.participantName?.trim()),
take(1)
)
.subscribe(() => this.onReadyToJoin.emit());
} else {
// No participant name to set, emit immediately
this.onReadyToJoin.emit();
}
}
onParticipantNameChanged(name: string) {
if (name) this.participantName = name;
this.participantName = name?.trim() || '';
// Clear error when user starts typing
if (this._error && this.participantName) {
this._error = undefined;
}
}
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();
});
}
async videoEnabledChanged(enabled: boolean) {
if (enabled && !this.videoTrack) {
this.isVideoEnabled = enabled;
if (!enabled) {
this.closeBackgroundPanel();
} else if (!this.videoTrack) {
const newVideoTrack = await this.openviduService.createLocalTracks(true, false);
this.videoTrack = newVideoTrack[0];
this.tracks.push(this.videoTrack);
this.openviduService.setLocalTracks(this.tracks);
}
this.onVideoEnabledChanged.emit(enabled);
}
onVideoDevicesLoaded(devices: CustomDevice[]) {
this.hasVideoDevices = devices.length > 0;
}
async audioEnabledChanged(enabled: boolean) {
if (enabled && !this.audioTrack) {
const newAudioTrack = await this.openviduService.createLocalTracks(false, true);
@ -201,4 +261,68 @@ export class PreJoinComponent implements OnInit, OnDestroy {
}
this.onAudioEnabledChanged.emit(enabled);
}
/**
* Toggle virtual background panel visibility with smooth animation
*/
toggleBackgroundPanel() {
// Add a small delay to ensure smooth transition
if (!this.showBackgroundPanel) {
// Opening panel
this.showBackgroundPanel = true;
this.changeDetector.markForCheck();
} else {
// Closing panel - add slight delay for smooth animation
setTimeout(() => {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
}, 50);
}
}
/**
* Close virtual background panel with smooth animation
*/
closeBackgroundPanel() {
// Add animation delay for smooth closing
setTimeout(() => {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
}, 100);
}
/**
* Enhanced error handling with better UX
*/
private handleError(error: any) {
this.log.e('PreJoin component error:', error);
this._error = error.message || 'An unexpected error occurred';
this.changeDetector.markForCheck();
}
/**
* Improved device initialization with error handling
*/
private async initializeDevicesWithRetry(maxRetries: number = 3): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
this.tracks = await this.openviduService.createLocalTracks();
this.openviduService.setLocalTracks(this.tracks);
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
this.isVideoEnabled = this.openviduService.isVideoTrackEnabled();
return; // Success, exit retry loop
} catch (error) {
this.log.w(`Device initialization attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
this.handleError(error);
} else {
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
}
}

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')
@ -191,7 +198,29 @@ export class SessionComponent implements OnInit, OnDestroy {
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();
@ -204,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
@ -230,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);
@ -237,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();
}
@ -248,7 +290,8 @@ export class SessionComponent implements OnInit, OnDestroy {
await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '',
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason
});
}, false);
@ -268,7 +311,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;
@ -287,7 +330,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;
});
}
@ -405,7 +448,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.log.d(`Data event received: ${topic}`);
switch (topic) {
case DataTopic.CHAT:
const participantName = participant?.identity || 'Unknown';
const participantName = participant?.name || 'Unknown';
this.chatService.addRemoteMessage(event.message, participantName);
break;
case DataTopic.RECORDING_STARTING:
@ -457,7 +500,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);
@ -490,9 +535,11 @@ 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 || '',
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.NETWORK_DISCONNECT
};
const messageErrorKey = 'ERRORS.DISCONNECT';
@ -532,7 +579,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)
@ -541,6 +588,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

@ -1,55 +1,99 @@
<div class="device-container-element" [class.mute-btn]="!isMicrophoneEnabled">
<!-- <button mat-stroked-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" id="audio-devices-menu">
<mat-icon class="audio-icon">mic</mat-icon>
<span class="device-label"> {{ microphoneSelected.label }} </span>
<mat-icon iconPositionEnd class="chevron-icon">
{{ menuTrigger.menuOpen ? 'expand_less' : 'expand_more' }}
</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let microphone of microphones">{{ microphone.label }}</button>
</mat-menu> -->
<mat-form-field id="audio-devices-form" *ngIf="microphones.length > 0">
<mat-select
[disabled]="!hasAudioDevices"
[compareWith]="compareObjectDevices"
[value]="microphoneSelected"
(selectionChange)="onMicrophoneSelected($event)"
>
<mat-select-trigger>
<div class="audio-device-selector" [class.compact]="compact">
<!-- Unified Device Button (Compact Mode) -->
@if (compact) {
@if (hasAudioDevices) {
<div class="unified-device-button">
<!-- Main toggle button -->
<button
mat-flat-button
id="microphone-button"
[disableRipple]="true"
class="toggle-section"
[disabled]="!hasAudioDevices || microphoneStatusChanging"
[class.mute-btn]="!isMicrophoneEnabled"
[class.device-enabled]="isMicrophoneEnabled"
[class.device-disabled]="!isMicrophoneEnabled"
(click)="toggleMic($event)"
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
[matTooltipDisabled]="!hasAudioDevices"
id="microphone-button"
>
<mat-icon *ngIf="isMicrophoneEnabled" id="mic"> mic </mat-icon>
<mat-icon *ngIf="!isMicrophoneEnabled" id="mic_off"> mic_off </mat-icon>
<mat-icon [id]="isMicrophoneEnabled ? 'mic' : 'mic_off'">{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
</button>
<span class="selected-text" *ngIf="!isMicrophoneEnabled">{{ 'PANEL.SETTINGS.DISABLED_AUDIO' | translate }}</span>
<span class="selected-text" *ngIf="isMicrophoneEnabled"> {{ microphoneSelected.label }} </span>
</mat-select-trigger>
<mat-option
*ngFor="let microphone of microphones"
[disabled]="!isMicrophoneEnabled"
[value]="microphone"
id="option-{{ microphone.label }}"
>
{{ microphone.label }}
</mat-option>
</mat-select>
</mat-form-field>
<div id="audio-devices-form" *ngIf="microphones.length === 0">
<div id="mat-select-trigger">
<button mat-icon-button id="microphone-button" class="mute-btn" [disabled]="true">
<mat-icon id="mic_off"> mic_off </mat-icon>
</button>
<span id="audio-devices-not-found"> {{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }} </span>
<!-- Dropdown section -->
@if (isMicrophoneEnabled) {
<button
mat-flat-button
id="audio-dropdown"
class="dropdown-section"
[matMenuTriggerFor]="microphoneMenu"
[disabled]="microphoneStatusChanging"
>
<mat-icon>expand_more</mat-icon>
</button>
}
</div>
} @else {
<!-- No Microphone Available -->
<div id="no-audio-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
</div>
}
} @else {
<!-- Normal Mode - Input Style Selector -->
<div class="normal-device-selector">
<!-- Input-style Device Selector -->
<div class="device-input-selector" [class.disabled]="!hasAudioDevices || !isMicrophoneEnabled">
<!-- When microphone is enabled -->
@if (isMicrophoneEnabled) {
<div class="device-input-selector">
<button
mat-flat-button
id="audio-dropdown"
class="selector-button"
[disabled]="microphoneStatusChanging || microphones.length <= 1"
[matMenuTriggerFor]="microphoneMenu"
[attr.aria-expanded]="false"
>
<mat-icon class="device-icon">mic</mat-icon>
<span class="selected-device-name">{{ microphoneSelected?.label || 'No microphone selected' }}</span>
<mat-icon class="dropdown-icon" *ngIf="microphones.length > 1">expand_more</mat-icon>
</button>
</div>
} @else {
@if (hasAudioDevices) {
<!-- When microphone is disabled -->
<div class="device-input-selector disabled">
<div class="selector-button disabled">
<mat-icon class="device-icon">mic_off</mat-icon>
<span class="selected-device-name">
{{ !hasAudioDevices ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }}
</span>
</div>
</div>
} @else {
<!-- No Microphone Available -->
<div id="no-audio-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
</div>
}
}
</div>
</div>
</div>
}
<!-- Device Selection Menu (Shared) -->
<mat-menu #microphoneMenu="matMenu" class="device-menu">
@for (microphone of microphones; track microphone.device) {
<button
mat-menu-item
id="option-{{ microphone.label }}"
(click)="onMicrophoneSelected({ value: microphone })"
[class.selected]="microphone.device === microphoneSelected.device"
>
<mat-icon *ngIf="microphone.device === microphoneSelected.device">check</mat-icon>
<span>{{ microphone.label }}</span>
</button>
}
</mat-menu>
</div>

View File

@ -1,103 +1,29 @@
$ov-selection-color-btn: #afafaf;
$ov-selection-color: #cccccc;
@use '../device-selector-shared' as shared;
:host {
.device-container-element {
border-radius: var(--ov-surface-radius);
border: 1px solid $ov-selection-color-btn;
}
.device-container-element.mute-btn {
border: 1px solid var(--ov-error-color);
}
#audio-devices-form {
width: 100%;
height: 50px;
}
display: flex;
align-items: center;
#audio-devices-not-found {
font-size: 13px;
}
.audio-device-selector {
@include shared.device-selector-base();
#microphone-button {
color:#000000
}
::ng-deep .mat-mdc-text-field-wrapper,
::ng-deep .mat-mdc-form-field-flex,
::ng-deep .mat-mdc-select-trigger {
height: 50px !important;
}
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none !important;
}
::ng-deep .mat-mdc-text-field-wrapper {
padding-left: 0px;
padding-right: 10px;
background-color: $ov-selection-color !important;
border-radius: var(--ov-surface-radius);
}
::ng-deep .mdc-button--unelevated {
border-top-left-radius: var(--ov-surface-radius);
border-bottom-left-radius: var(--ov-surface-radius);
border-bottom-right-radius: 0px !important;
border-top-right-radius: 0px !important;
background-color: $ov-selection-color-btn !important;
width: 48px !important;
min-width: 48px !important;
padding: 0;
height: 50px;
}
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
height: 24px;
width: 24px;
font-size: 24px !important;
margin: auto;
}
::ng-deep .mat-mdc-form-field-infix {
padding: 0px !important;
min-height: 100%;
}
.selected-text {
padding-left: 5px;
}
.mat-icon {
vertical-align: middle;
display: inline-flex;
}
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
border: 0px !important;
}
::ng-deep .mat-mdc-button-touch-target {
border-radius: var(--ov-surface-radius) !important;
}
.mute-btn {
color: #ffffff !important;
background-color: var(--ov-error-color) !important;
// Audio-specific overrides for normal mode
&:not(.compact) {
.normal-device-selector {
.device-input-selector {
&:not(.disabled) {
.selector-button {
// Audio-specific hover effect (simpler than video)
&:hover:not([disabled]) {
border-color: var(--ov-primary-action-color, #4285f4);
}
}
}
}
}
}
}
}
::ng-deep .mat-mdc-select-panel {
background-color: #ffffff !important;
}
::ng-deep .mat-mdc-option {
padding: 10px 10px !important;
}
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
color: var(--ov-primary-action-color) !important;
}
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
border-bottom-color: var(--ov-primary-action-color) !important;
}
::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
background-color: $ov-selection-color !important;
}
// Include shared device menu styles
@include shared.device-menu-styles();

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model';
import { DeviceService } from '../../../services/device/device.service';
@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
standalone: false
})
export class AudioDevicesComponent implements OnInit, OnDestroy {
@Input() compact: boolean = false;
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();

View File

@ -0,0 +1,246 @@
// Shared styles for device selectors (video and audio)
// This file contains common styling for both video-devices and audio-devices components
@mixin device-selector-base() {
display: flex;
align-items: center;
width: 100%;
// Compact Mode - Unified Button
&.compact {
.unified-device-button {
display: flex;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.toggle-section {
display: flex;
align-items: center;
justify-content: center;
min-width: 50px;
width: 50px;
height: 48px;
border: none;
background: transparent;
border-radius: 0;
padding: 0;
transition: all 0.2s ease;
&.device-enabled {
color: var(--ov-primary-action-color, #4285f4);
mat-icon {
color: var(--ov-primary-action-color, #4285f4);
}
}
&.device-disabled {
background: rgba(244, 67, 54, 0.9);
color: white;
mat-icon {
color: white;
}
}
&[disabled] {
background: rgba(150, 150, 150, 0.5);
color: rgba(150, 150, 150, 0.8);
cursor: not-allowed;
}
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
margin: 0;
}
}
.dropdown-section {
display: flex;
align-items: center;
justify-content: center;
min-width: 30px;
width: 30px;
height: 48px;
border: none;
background: transparent;
border-left: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0;
padding: 0;
color: var(--ov-text-secondary-color, #666);
transition: all 0.2s ease;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
margin: 0;
}
}
}
}
// Normal Mode - Input Style Selector
&:not(.compact) {
.normal-device-selector {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
.device-input-selector {
flex: 1;
&:not(.disabled) {
.selector-button {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
height: 48px;
padding: 0 16px;
background: var(--ov-surface-color, #ffffff);
border: 2px solid var(--ov-border-color, #e0e0e0);
border-radius: 8px;
color: var(--ov-text-surface-color);
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
text-align: left;
justify-content: flex-start;
&[disabled] {
background: var(--ov-disabled-background, #f5f5f5);
color: var(--ov-disabled-text-color, #999);
cursor: not-allowed;
border-color: var(--ov-disabled-border-color, #ddd);
}
.device-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--ov-text-secondary-color, #666);
flex-shrink: 0;
}
.selected-device-name {
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.dropdown-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--ov-text-secondary-color, #666);
flex-shrink: 0;
transition: transform 0.2s ease;
}
&[aria-expanded='true'] .dropdown-icon {
transform: rotate(180deg);
}
}
}
&.disabled {
.selector-button.disabled {
display: flex;
align-items: center;
gap: 12px;
height: 48px;
padding: 0 16px;
background: var(--ov-disabled-background, #f5f5f5);
border: 2px solid var(--ov-disabled-border-color, #ddd);
border-radius: 8px;
color: var(--ov-disabled-text-color, #999);
font-size: 14px;
cursor: not-allowed;
.device-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--ov-error-color, #d32f2f);
}
.selected-device-name {
flex: 1;
font-style: italic;
}
}
}
}
}
}
.no-device-message {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 8px;
color: var(--ov-warning-color, #ff9800);
font-size: 12px;
.warning-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
}
}
// Shared device menu styles
@mixin device-menu-styles() {
::ng-deep .device-menu.mat-mdc-menu-panel {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
border: 1px solid var(--ov-border-color, #e0e0e0);
overflow: hidden;
background-color: var(--ov-surface-color);
.mat-mdc-menu-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
transition: background-color 0.2s ease;
font-size: 14px;
&:hover {
background-color: var(--ov-hover-color, #f5f5f5);
}
&.selected {
background-color: rgba(66, 133, 244, 0.08);
color: var(--ov-primary-action-color, #4285f4);
mat-icon {
color: var(--ov-primary-action-color, #4285f4);
}
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
span {
flex: 1;
font-weight: 500;
}
}
}
}

View File

@ -1,18 +1,33 @@
<button id="lang-btn-compact" *ngIf="compact" mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>translate</mat-icon>
</button>
<button *ngIf="!compact" mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" id="lang-btn">
<span id="lang-selected-name">{{ langSelected?.name }}</span>
<mat-icon class="expand-more-icon">expand_more</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button
mat-menu-item
*ngFor="let lang of languages"
(click)="onLangSelected(lang.lang)"
[attr.id]="'lang-opt-' + lang.lang"
class="lang-menu-opt"
>
<span>{{ lang.name }}</span>
</button>
</mat-menu>
<div class="language-selector-container">
@if (compact) {
<!-- Compact version (icon only) -->
<button mat-icon-button [matMenuTriggerFor]="langMenu" class="compact-lang-button" [matTooltip]="'Change language'" disableRipple>
<mat-icon>translate</mat-icon>
</button>
} @else {
<!-- Full version (with text) -->
<button mat-flat-button [matMenuTriggerFor]="langMenu" class="full-lang-button">
<!-- <mat-icon class="lang-icon">translate</mat-icon> -->
<span class="lang-name">
{{ langSelected?.name }}
<mat-icon class="expand-icon">expand_more</mat-icon>
</span>
</button>
}
<!-- Language Menu -->
<mat-menu #langMenu="matMenu" class="language-menu">
@for (lang of languages; track lang.lang) {
<button
mat-menu-item
(click)="onLangSelected(lang.lang)"
[attr.id]="'lang-opt-' + lang.lang"
[class.selected]="langSelected?.lang === lang.lang"
class="language-option"
>
<mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon>
<span class="lang-option-name">{{ lang.name }}</span>
</button>
}
</mat-menu>
</div>

View File

@ -1,21 +1,113 @@
$ov-surface-color-lighter: color-mix(in srgb, var(--ov-surface-color), #fff 5%);
:host {
display: inline-block;
.lang-button {
background-color: var(--ov-primary-action-color) !important;
color: var(--ov-secondary-action-color) !important;
}
.lang-button .mat-icon {
color: var(--ov-secondary-action-color);
.language-selector-container {
.compact-lang-button {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border: 1px solid var(--ov-border-color, #e0e0e0);
border-radius: 10px;
transition: all 0.2s ease;
color: var(--ov-text-secondary-color, #666);
}
::ng-deep .mat-mdc-menu-panel {
border-radius: var(--ov-surface-radius) !important;
background-color: $ov-surface-color-lighter !important;
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
.full-lang-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--ov-surface-color, #ffffff);
border: 2px solid var(--ov-border-color, #e0e0e0);
border-radius: 12px;
transition: all 0.2s ease;
color: var(--ov-text-primary-color, #333);
font-weight: 500;
&:hover {
border-color: var(--ov-primary-action-color, #4285f4);
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.1);
}
.lang-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--ov-text-surface-color, #666);
}
.lang-name {
font-size: 14px;
font-weight: 500;
display: inline-block !important;
color: var(--ov-text-surface-color) !important;
}
.expand-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: var(--ov-text-secondary-color, #666);
transition: transform 0.2s ease;
}
&[aria-expanded='true'] .expand-icon {
transform: rotate(180deg);
}
}
}
}
::ng-deep .mat-mdc-menu-item,
.mat-mdc-menu-item:visited,
.mat-mdc-menu-item:link {
color: var(--ov-text-surface-color) !important;
::ng-deep .language-menu.mat-mdc-menu-panel {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
border: 1px solid var(--ov-border-color, #e0e0e0);
overflow: hidden;
background: var(--ov-surface-color, #ffffff);
.language-option {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
transition: background-color 0.2s ease;
font-size: 14px;
min-height: 48px;
color: var(--ov-text-surface-color);
&:hover {
background-color: var(--ov-hover-color, #f5f5f5);
}
&.selected {
background-color: rgba(66, 133, 244, 0.08);
color: var(--ov-primary-action-color, #4285f4);
.check-icon {
color: var(--ov-primary-action-color, #4285f4);
}
}
.check-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.lang-option-name {
flex: 1;
font-weight: 500;
}
&.selected .lang-option-name {
font-weight: 600;
}
}
}

View File

@ -1,20 +1,17 @@
<div id="name-input-container" [ngClass]="{ warn: !name }">
<mat-form-field id="name-form" [ngClass]="{ error: error }">
<mat-select-trigger>
<button mat-flat-button disabled>
<mat-icon>person</mat-icon>
</button>
</mat-select-trigger>
<div class="participant-name-input-container" [class.error]="error">
<div class="input-wrapper">
<mat-icon class="input-icon">person</mat-icon>
<input
id="name-input"
matInput
(change)="updateName()"
type="text"
maxlength="20"
[(ngModel)]="name"
autocomplete="off"
[disabled]="!isPrejoinPage"
(input)="updateName()"
(keypress)="eventKeyPress($event)"
[placeholder]="'PREJOIN.NICKNAME' | translate"
class="name-input-field"
/>
</mat-form-field>
</div>
</div>

View File

@ -1,67 +1,71 @@
$ov-selection-color-btn: #afafaf;
$ov-selection-color: #cccccc;
:host {
#name-input-container {
height: 70px;
border-radius: var(--ov-surface-radius);
}
display: block;
width: 100%;
#name-input-container mat-form-field {
.participant-name-input-container {
width: 100%;
color: var(--ov-secondary-action-color);
}
::ng-deep .mat-mdc-form-field-infix {
display: inline-flex;
padding: 0px !important;
}
::ng-deep .mat-mdc-text-field-wrapper {
padding: 0;
height: 70px;
background-color: $ov-selection-color !important;
border-radius: var(--ov-surface-radius);
}
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
border: 0px !important;
}
::ng-deep .mdc-button--unelevated {
border-top-left-radius: var(--ov-surface-radius) !important;
border-bottom-left-radius: var(--ov-surface-radius) !important;
border-bottom-right-radius: 0px !important;
border-top-right-radius: 0px !important;
background-color: $ov-selection-color-btn !important;
width: 48px !important;
min-width: 48px !important;
padding: 0;
height: 70px;
}
.input-wrapper {
display: flex;
align-items: center;
background: var(--ov-input-background, #f8f9fa);
border: 2px solid var(--ov-border-color, #e0e0e0);
border-radius: 12px;
padding: 0;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
.error {
::ng-deep .mdc-button--unelevated {
background-color: var(--ov-error-color) !important;
&:focus-within {
border-color: var(--ov-primary-action-color, #4285f4);
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
}
.input-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 56px;
background: var(--ov-surface-secondary, #f0f0f0);
color: var(--ov-text-secondary-color, #666);
font-size: 20px;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
flex-shrink: 0;
}
.name-input-field {
flex: 1;
height: 56px;
padding: 0 16px;
border: none;
outline: none;
background: transparent;
font-size: 16px;
font-weight: 500;
&::placeholder {
color: var(--ov-text-secondary-color, #666);
font-weight: 400;
}
&:disabled {
background: var(--ov-disabled-background, #f5f5f5);
color: var(--ov-disabled-text-color, #999);
cursor: not-allowed;
}
}
}
&.error .input-wrapper {
border-color: var(--ov-error-color, #d32f2f);
box-shadow: 0 0 0 3px rgba(211, 47, 47, 0.1);
.input-icon {
background: rgba(211, 47, 47, 0.1);
color: var(--ov-error-color, #d32f2f);
}
}
}
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
height: 24px;
width: 24px;
font-size: 24px !important;
margin: auto;
color: #000000 !important;
}
input {
padding-left: 10px !important;
border-top-right-radius: var(--ov-surface-radius) !important;
border-bottom-right-radius: var(--ov-surface-radius) !important;
border-bottom-left-radius: 0px !important;
border-top-left-radius: 0px !important;
border: 1px solid $ov-selection-color-btn;
color: #000000;
caret-color: #000000 !important;
}
}
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
color: var(--ov-primary-action-color) !important;
}

View File

@ -1,40 +1,95 @@
<div class="device-container-element" [class.mute-btn]="!isCameraEnabled">
<mat-form-field id="video-devices-form" *ngIf="cameras.length > 0">
<mat-select
[disabled]="!hasVideoDevices"
[compareWith]="compareObjectDevices"
[value]="cameraSelected"
(selectionChange)="onCameraSelected($event)"
>
<mat-select-trigger id="mat-select-trigger">
<div class="video-device-selector" [class.compact]="compact">
<!-- Unified Device Button (Compact Mode) -->
@if (compact) {
@if (hasVideoDevices) {
<div class="unified-device-button">
<!-- Main toggle button -->
<button
mat-flat-button
id="camera-button"
[disableRipple]="true"
class="toggle-section"
[disabled]="!hasVideoDevices || cameraStatusChanging"
[class.mute-btn]="!isCameraEnabled"
[class.device-enabled]="isCameraEnabled"
[class.device-disabled]="!isCameraEnabled"
(click)="toggleCam($event)"
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
[matTooltipDisabled]="!hasVideoDevices"
id="camera-button"
>
<mat-icon *ngIf="isCameraEnabled" id="videocam"> videocam </mat-icon>
<mat-icon *ngIf="!isCameraEnabled" id="videocam_off"> videocam_off </mat-icon>
<mat-icon [id]="isCameraEnabled ? 'videocam' : 'videocam_off'">
{{ isCameraEnabled ? 'videocam' : 'videocam_off' }}
</mat-icon>
</button>
<span class="selected-text" *ngIf="!isCameraEnabled"> {{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }} </span>
<span class="selected-text" *ngIf="isCameraEnabled"> {{ cameraSelected.label }} </span>
</mat-select-trigger>
<mat-option *ngFor="let camera of cameras" [disabled]="!isCameraEnabled" [value]="camera" id="option-{{ camera.label }}">
{{ camera.label }}
</mat-option>
</mat-select>
</mat-form-field>
<div id="video-devices-form" *ngIf="cameras.length === 0">
<div id="mat-select-trigger">
<button mat-icon-button id="camera-button" class="mute-btn" [disabled]="true">
<mat-icon id="videocam_off"> videocam_off </mat-icon>
</button>
<span id="video-devices-not-found"> {{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }} </span>
<!-- Dropdown section -->
@if (isCameraEnabled) {
<button
mat-flat-button
id="video-dropdown"
class="dropdown-section"
[matMenuTriggerFor]="cameraMenu"
[disabled]="cameraStatusChanging"
>
<mat-icon>expand_more</mat-icon>
</button>
}
</div>
} @else {
<!-- No Camera Available -->
<div id="no-video-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
</div>
}
} @else {
<!-- Normal Mode - Input-style Selector -->
<div class="normal-device-selector">
<!-- Device Selector (Input Style) -->
@if (isCameraEnabled) {
<div class="device-input-selector">
<button
mat-flat-button
id="video-dropdown"
class="selector-button"
[matMenuTriggerFor]="cameraMenu"
[disabled]="cameraStatusChanging || cameras.length <= 1"
>
<mat-icon class="device-icon">videocam</mat-icon>
<span class="selected-device-name">{{ cameraSelected?.label || 'No camera selected' }}</span>
<mat-icon class="dropdown-icon" *ngIf="cameras.length > 1">expand_more</mat-icon>
</button>
</div>
} @else {
@if (hasVideoDevices) {
<!-- Disabled state message -->
<div class="device-input-selector disabled">
<div class="selector-button disabled">
<mat-icon class="device-icon">videocam_off</mat-icon>
<span class="selected-device-name">{{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }}</span>
</div>
</div>
} @else {
<!-- No Camera Available -->
<div id="no-video-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
</div>
}
}
</div>
</div>
}
<!-- Device Selection Menu (Shared) -->
<mat-menu #cameraMenu="matMenu" class="device-menu">
@for (camera of cameras; track camera.device) {
<button
mat-menu-item
id="option-{{ camera.label }}"
(click)="onCameraSelected({ value: camera })"
[class.selected]="camera.device === cameraSelected?.device"
>
<mat-icon *ngIf="camera.device === cameraSelected?.device" class="check-icon">check</mat-icon>
<span>{{ camera.label }}</span>
</button>
}
</mat-menu>
</div>

View File

@ -1,106 +1,51 @@
@use '../device-selector-shared' as shared;
$ov-selection-color-btn: #afafaf;
$ov-selection-color: #cccccc;
:host {
.device-container-element {
border-radius: var(--ov-surface-radius);
border: 1px solid $ov-selection-color-btn;
}
.device-container-element.mute-btn {
border: 1px solid var(--ov-error-color);
}
#video-devices-form {
width: 100%;
height: 50px;
}
display: flex;
align-items: center;
#video-devices-not-found {
font-size: 13px;
}
.video-device-selector {
@include shared.device-selector-base();
#camera-button {
color: #000000;
}
// Video-specific overrides for compact mode
&.compact {
.unified-device-button {
.toggle-section {
display: flex-end; // Video-specific styling
}
}
}
::ng-deep .mat-mdc-text-field-wrapper,
::ng-deep .mat-mdc-form-field-flex,
::ng-deep .mat-mdc-select-trigger {
height: 50px !important;
}
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none !important;
}
::ng-deep .mat-mdc-text-field-wrapper {
padding-left: 0px;
padding-right: 10px;
background-color: $ov-selection-color !important;
border-radius: var(--ov-surface-radius);
}
::ng-deep .mdc-button--unelevated {
border-top-left-radius: var(--ov-surface-radius);
border-bottom-left-radius: var(--ov-surface-radius);
border-bottom-right-radius: 0px !important;
border-top-right-radius: 0px !important;
background-color: $ov-selection-color-btn !important;
width: 48px !important;
min-width: 48px !important;
padding: 0;
height: 50px;
}
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
height: 24px;
width: 24px;
font-size: 24px !important;
margin: auto;
}
::ng-deep .mat-mdc-form-field-infix {
padding: 0px !important;
min-height: 100%;
}
.selected-text {
padding-left: 5px;
}
.mat-icon {
vertical-align: middle;
display: inline-flex;
}
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
border: 0px !important;
}
::ng-deep .mat-mdc-button-touch-target {
border-radius: var(--ov-surface-radius) !important;
}
.mute-btn {
color: #ffffff !important;
background-color: var(--ov-error-color) !important;
// Video-specific overrides for normal mode
&:not(.compact) {
.normal-device-selector {
.device-input-selector {
&:not(.disabled) {
.selector-button {
// Video-specific hover effect with box-shadow
&:hover:not([disabled]) {
background-color: white !important;
border-color: var(--ov-primary-action-color);
}
}
}
}
}
}
}
}
::ng-deep .mat-mdc-select-panel {
background-color: var(--ov-surface-color) !important;
}
::ng-deep .mat-mdc-select-panel {
background-color: #e2e2e2 !important;
}
// Include shared device menu styles
@include shared.device-menu-styles();
::ng-deep .mat-mdc-option {
padding: 10px 10px !important;
}
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
color: var(--ov-primary-action-color-lighter) !important;
}
// Video-specific additional styles
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
border-bottom-color: var(--ov-primary-action-color-lighter) !important;
}
::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
background-color: $ov-selection-color !important;
}

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model';
import { DeviceService } from '../../../services/device/device.service';
@ -16,8 +16,10 @@ import { ParticipantModel } from '../../../models/participant.model';
standalone: false
})
export class VideoDevicesComponent implements OnInit, OnDestroy {
@Input() compact: boolean = false;
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
@Output() onVideoDevicesLoaded = new EventEmitter<CustomDevice[]>();
cameraStatusChanging: boolean;
isCameraEnabled: boolean;
@ -41,6 +43,7 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
this.cameraSelected = this.deviceSrv.getCameraSelected();
}
this.onVideoDevicesLoaded.emit(this.cameras);
this.isCameraEnabled = this.participantService.isMyCameraEnabled();
}

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

@ -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
*/
@ -527,7 +558,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '',
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.LEAVE
});
this.onRoomDisconnected.emit();
@ -538,10 +570,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 +606,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 +623,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 +676,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 +695,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 +723,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 +732,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 +756,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 +770,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 +788,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 +888,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 +906,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,55 +1,74 @@
<div id="call-container">
<div id="spinner" *ngIf="loading">
<mat-spinner [diameter]="50"></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>
<div id="spinner" *ngIf="!loading && error">
<mat-icon class="error-icon">error</mat-icon>
<span>{{ errorMessage }}</span>
</div>
<div [@inOutAnimation] id="vc-container" *ngIf="isRoomReady && !showPrejoin && !loading && !error">
<ov-session
(onRoomCreated)="onRoomCreated.emit($event)"
(onRoomReconnecting)="onRoomDisconnected.emit()"
(onRoomDisconnected)="onRoomDisconnected.emit()"
(onRoomReconnected)="onRoomReconnected.emit()"
(onParticipantConnected)="onParticipantCreated.emit($event)"
(onParticipantConnected)="onParticipantConnected.emit($event)"
(onParticipantLeft)="_onParticipantLeft($event)"
>
<ng-template #toolbar>
<ng-container *ngIf="openviduAngularToolbarTemplate">
<ng-container *ngTemplateOutlet="openviduAngularToolbarTemplate"></ng-container>
</ng-container>
<!-- Loading spinner -->
@if (componentState.isLoading) {
<div id="spinner" *ngIf="componentState.isLoading">
<mat-spinner [diameter]="spinnerDiameter"></mat-spinner>
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
</div>
} @else if (componentState.showPrejoin) {
<!-- Prejoin -->
<div [@inOutAnimation] id="pre-join-container">
<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>
} @else if (componentState.error?.hasError) {
<!-- Error -->
<ng-template #panel>
<ng-container *ngIf="openviduAngularPanelTemplate">
<ng-container *ngTemplateOutlet="openviduAngularPanelTemplate"></ng-container>
</ng-container>
</ng-template>
<div id="spinner">
<mat-icon class="error-icon">error</mat-icon>
<span>{{ componentState.error?.message }}</span>
</div>
} @else if (componentState.isRoomReady) {
<!-- VideoConference -->
<ng-template #layout>
<ng-container *ngIf="openviduAngularLayoutTemplate">
<ng-container *ngTemplateOutlet="openviduAngularLayoutTemplate"></ng-container>
</ng-container>
</ng-template>
</ov-session>
</div>
<div [@inOutAnimation] id="vc-container">
<ov-session
(onRoomCreated)="onRoomCreated.emit($event)"
(onRoomReconnecting)="onRoomDisconnected.emit()"
(onRoomDisconnected)="onRoomDisconnected.emit()"
(onRoomReconnected)="onRoomReconnected.emit()"
(onParticipantConnected)="onParticipantCreated.emit($event)"
(onParticipantConnected)="onParticipantConnected.emit($event)"
(onParticipantLeft)="_onParticipantLeft($event)"
>
<ng-template #toolbar>
<ng-container *ngIf="openviduAngularToolbarTemplate">
<ng-container *ngTemplateOutlet="openviduAngularToolbarTemplate"></ng-container>
</ng-container>
</ng-template>
<ng-template #panel>
<ng-container *ngIf="openviduAngularPanelTemplate">
<ng-container *ngTemplateOutlet="openviduAngularPanelTemplate"></ng-container>
</ng-container>
</ng-template>
<ng-template #layout>
<ng-container *ngIf="openviduAngularLayoutTemplate">
<ng-container *ngTemplateOutlet="openviduAngularLayoutTemplate"></ng-container>
</ng-container>
</ng-template>
</ov-session>
</div>
} @else {
<!-- Fallback / unknown state -->
<div id="unknown-state">
<mat-icon class="warning-icon">help_outline</mat-icon>
<span>An error occurred in the video conference</span>
</div>
}
</div>
<ng-template #defaultToolbar>
@ -64,6 +83,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 +148,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 +162,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 +183,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

@ -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,285 @@ 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';
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 +422,10 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal
*/
openviduAngularAdditionalPanelsTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularParticipantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
/**
* @internal
*/
@ -198,6 +442,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 +471,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 +573,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 +600,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 +626,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 +684,29 @@ 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');
this.addMaterialIconsIfNeeded();
// 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,116 +714,202 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal
*/
ngAfterViewInit() {
//Add material icons to the page if not already present
const existingLink = document.querySelector('link[href*="Material+Symbols+Outlined"]');
if (!existingLink) {
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.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');
@ -532,27 +919,68 @@ 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;
if (this.hasUserInitiatedJoin()) {
// 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
});
console.log(this.componentState);
console.warn(
this.componentState.isRoomReady &&
!this.componentState.showPrejoin &&
!this.componentState.isLoading &&
!this.componentState.error?.hasError
);
} else {
// Only update showPrejoin if user hasn't initiated join process yet
// This prevents prejoin from showing again after user clicked join
this.updateComponentState({
state: VideoconferenceState.PREJOIN_SHOWN,
isRoomReady: true,
showPrejoin: this.libService.showPrejoin()
});
}
} 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
@ -563,10 +991,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
@ -576,24 +1005,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,7 +58,6 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
standalone: false
})
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: string) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
@ -66,7 +65,10 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
navbarTitleValue: string = 'OpenVidu Dashboard';
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
@ -80,13 +82,10 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
}
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 });
}
}
@ -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

@ -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": "设置",
@ -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": "直播",

View File

@ -55,7 +55,8 @@
"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",
@ -89,7 +90,9 @@
"MICROPHONE": "MIKROFON",
"SCREEN": "BILDSCHIRM",
"NO_STREAMS": "KEINE",
"YOU": "Sie"
"YOU": "Sie",
"MUTE": "Stummschalten",
"UNMUTE": "Stummschaltung aufheben"
},
"SETTINGS": {
"TITLE": "Einstellungen",
@ -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",

View File

@ -55,7 +55,9 @@
"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",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROPHONE",
"SCREEN": "SCREEN",
"NO_STREAMS": "NONE",
"YOU": "You"
"YOU": "You",
"MUTE": "Mute",
"UNMUTE": "Unmute"
},
"SETTINGS": {
"TITLE": "Settings",
@ -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

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

@ -55,7 +55,9 @@
"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",
@ -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",
@ -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",

View File

@ -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": "सेटिंग्स",
@ -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": "स्ट्रीमिंग",

View File

@ -55,7 +55,9 @@
"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",
@ -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",
@ -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",

View File

@ -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": "設定",
@ -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": "ストリーミング",

View File

@ -55,7 +55,9 @@
"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",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROFOON",
"SCREEN": "SCHERM",
"NO_STREAMS": "GEEN",
"YOU": "Jij"
"YOU": "Jij",
"MUTE": "Dempen",
"UNMUTE": "Dempen opheffen"
},
"SETTINGS": {
"TITLE": "Instellingen",
@ -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",

View File

@ -55,7 +55,9 @@
"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",
@ -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",
@ -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",

View File

@ -3,6 +3,7 @@
*/
export interface ILogger {
d(...args: any[]): void;
v(...args: any[]): void;
w(...args: any[]): void;
e(...args: any[]): void;
}

View File

@ -18,6 +18,7 @@ import {
export interface ParticipantLeftEvent {
roomName: string;
participantName: string;
identity: string;
reason: ParticipantLeftReason;
}

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,6 +1,5 @@
import { Injectable } from '@angular/core';
import { ILogService } from '../../models/logger.model';
import { ILogService, ILogger } from '../../models/logger.model';
import { GlobalConfigService } from '../config/global-config.service';
/**
@ -10,42 +9,87 @@ import { GlobalConfigService } from '../config/global-config.service';
providedIn: 'root'
})
export class LoggerService implements ILogService {
public log;
public LOG_FNS = [];
public MSG_PREFIXES = [
['[', ']'],
private log: Console;
private LOG_FNS: Function[] = [];
private MSG_PREFIXES: string[][] = [
['[', '] DEBUG: '],
['[', '] VERBOSE: '],
['[', '] WARN: '],
['[', '] ERROR: ']
];
private loggerCache: Map<string, ILogger> = new Map();
constructor(private globalService: GlobalConfigService) {
this.initializeLogger();
}
private getLoggerFns(prefix: string) {
private initializeLogger(): void {
this.log = window.console;
this.LOG_FNS = [this.log.log, this.log.warn, this.log.error];
const loggerFns = this.LOG_FNS.map((logTemplFn, i) => {
return logTemplFn.bind(this.log, this.MSG_PREFIXES[i][0] + prefix + this.MSG_PREFIXES[i][1]);
});
return loggerFns;
this.LOG_FNS = [
this.log.log.bind(this.log),
this.log.debug.bind(this.log),
this.log.warn.bind(this.log),
this.log.error.bind(this.log)
];
}
public get(prefix: string) {
private createLoggerFunctions(
prefix: string
): [(...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void] {
const prodMode = this.globalService.isProduction();
const loggerService = this;
return {
d: function(...args: any[]) {
if (!prodMode) {
loggerService.getLoggerFns(prefix)[0].apply(this.log, arguments);
}
},
w: function(...args: any[]) {
loggerService.getLoggerFns(prefix)[1].apply(this.log, arguments);
},
e: function(...args: any[]) {
loggerService.getLoggerFns(prefix)[2].apply(this.log, arguments);
const debugFn = (...args: any[]): void => {
if (!prodMode) {
// Only log debug messages in non-production mode
this.LOG_FNS[0](this.MSG_PREFIXES[0][0] + prefix + this.MSG_PREFIXES[0][1], ...args);
}
};
const verboseFn = (...args: any[]): void => {
if (!prodMode) {
// Only log verbose messages in non-production mode and when verbose is enabled
this.LOG_FNS[1](this.MSG_PREFIXES[1][0] + prefix + this.MSG_PREFIXES[1][1], ...args);
}
};
const warnFn = (...args: any[]): void => {
this.LOG_FNS[2](this.MSG_PREFIXES[2][0] + prefix + this.MSG_PREFIXES[2][1], ...args);
};
const errorFn = (...args: any[]): void => {
this.LOG_FNS[3](this.MSG_PREFIXES[3][0] + prefix + this.MSG_PREFIXES[3][1], ...args);
};
return [debugFn, verboseFn, warnFn, errorFn];
}
public get(prefix: string): ILogger {
// Check cache first
if (this.loggerCache.has(prefix)) {
return this.loggerCache.get(prefix)!;
}
// Create new logger functions
const [debugFn, verboseFn, warnFn, errorFn] = this.createLoggerFunctions(prefix);
const logger: ILogger = {
d: debugFn,
v: verboseFn,
w: warnFn,
e: errorFn
};
// Cache the logger
this.loggerCache.set(prefix, logger);
return logger;
}
/**
* Clears the logger cache. Useful for testing or when configuration changes.
* @internal
*/
public clearCache(): void {
this.loggerCache.clear();
}
}

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,18 +166,37 @@ 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
*/
initializeAndSetToken(token: string, livekitUrl: string): void {
const livekitData = this.extractLivekitData(token, livekitUrl);
this.livekitToken = token;
this.livekitUrl = livekitData.livekitUrl;
initializeAndSetToken(token: string, livekitUrl?: string): void {
const { livekitUrl: urlFromToken } = this.extractLivekitData(token);
if (!this.livekitUrl) {
this.livekitToken = token;
const url = livekitUrl || urlFromToken;
if (!url) {
this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent');
throw new Error('Livekit URL is not defined');
}
this.livekitUrl = url;
// this.livekitRoomAdmin = !!livekitRoomAdmin;
// 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);
}
@ -208,23 +242,20 @@ export class OpenViduService {
videoDeviceId: string | boolean | undefined = undefined,
audioDeviceId: string | boolean | undefined = undefined
): Promise<LocalTrack[]> {
// If video and audio device IDs are not provided, check if they are enabled and use the default devices
if (videoDeviceId === undefined) videoDeviceId = this.deviceService.isCameraEnabled();
if (audioDeviceId === undefined) audioDeviceId = this.deviceService.isMicrophoneEnabled();
// Default values: true if device is enabled, false otherwise
videoDeviceId ??= this.deviceService.isCameraEnabled();
audioDeviceId ??= this.deviceService.isMicrophoneEnabled();
let options: CreateLocalTracksOptions = {
const options: CreateLocalTracksOptions = {
audio: { echoCancellation: true, noiseSuppression: true },
video: {}
};
// Video device
if (videoDeviceId === true) {
if (this.deviceService.hasVideoDeviceAvailable()) {
videoDeviceId = this.deviceService.getCameraSelected()?.device || 'default';
(options.video as VideoCaptureOptions).deviceId = videoDeviceId;
} else {
options.video = false;
}
options.video = this.deviceService.hasVideoDeviceAvailable()
? { deviceId: this.deviceService.getCameraSelected()?.device || 'default' }
: false;
} else if (videoDeviceId === false) {
options.video = false;
} else {
@ -249,13 +280,13 @@ export class OpenViduService {
if (options.audio || options.video) {
this.log.d('Creating local tracks with options', options);
newLocalTracks = await createLocalTracks(options);
// Mute tracks if devices are disabled
if (!this.deviceService.isCameraEnabled()) {
const videoTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Video);
if (videoTrack) videoTrack.mute();
newLocalTracks.find((t) => t.kind === Track.Kind.Video)?.mute();
}
if (!this.deviceService.isMicrophoneEnabled()) {
const audioTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Audio);
if (audioTrack) audioTrack.mute();
newLocalTracks.find((t) => t.kind === Track.Kind.Audio)?.mute();
}
}
return newLocalTracks;
@ -301,7 +332,7 @@ export class OpenViduService {
return this.deviceService.isCameraEnabled();
}
const videoTrack = this.localTracks.find((track) => track.kind === Track.Kind.Video);
return !!videoTrack && !videoTrack.isMuted;
return !!videoTrack && !videoTrack.isMuted && videoTrack?.mediaStreamTrack?.enabled;
}
/**
@ -314,7 +345,7 @@ export class OpenViduService {
return this.deviceService.isMicrophoneEnabled();
}
const audioTrack = this.localTracks.find((track) => track.kind === Track.Kind.Audio);
return !!audioTrack && !audioTrack.isMuted;
return !!audioTrack && !audioTrack.isMuted && audioTrack?.mediaStreamTrack?.enabled;
}
/**
@ -343,9 +374,8 @@ export class OpenViduService {
* @throws Error if there is an error decoding and parsing the token.
* @internal
*/
private extractLivekitData(token: string, livekitUrl: string): { livekitUrl: string; livekitRoomAdmin: boolean } {
private extractLivekitData(token: string): { livekitUrl?: string; livekitRoomAdmin: boolean } {
try {
const response = { livekitUrl, livekitRoomAdmin: false };
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
@ -361,13 +391,13 @@ export class OpenViduService {
const payload = JSON.parse(jsonPayload);
if (payload?.metadata) {
const tokenMetadata = JSON.parse(payload.metadata);
if (tokenMetadata.livekitUrl) {
response.livekitUrl = tokenMetadata.livekitUrl;
}
response.livekitRoomAdmin = tokenMetadata.roomAdmin;
return {
livekitUrl: tokenMetadata.livekitUrl,
livekitRoomAdmin: !!tokenMetadata.roomAdmin
};
}
return response;
return { livekitRoomAdmin: false };
} catch (error) {
throw new Error('Error decoding and parsing token: ' + error);
}

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,12 +41,9 @@ export class RecordingService {
* @internal
*/
setRecordingStarted(recordingInfo?: RecordingInfo, startTimestamp?: number) {
// Register the start timestamp of the recording
// to calculate the elapsed time
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];
@ -61,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();
}
/**
@ -96,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;
@ -107,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
});
}
@ -121,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);
@ -136,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
});
}
@ -199,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;
}
@ -219,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
});
}
@ -233,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);
}
@ -253,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.v('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.v('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS');
}
if (externalDirectives.toolbarAdditionalPanelButtons) {
config.toolbarAdditionalPanelButtonsTemplate = externalDirectives.toolbarAdditionalPanelButtons.template;
this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS');
}
if (externalDirectives.additionalPanels) {
config.additionalPanelsTemplate = externalDirectives.additionalPanels.template;
this.log.v('Setting EXTERNAL ADDITIONAL PANELS');
}
if (externalDirectives.participantPanelItemElements) {
config.participantPanelItemElementsTemplate = externalDirectives.participantPanelItemElements.template;
this.log.v('Setting EXTERNAL PARTICIPANT PANEL ITEM ELEMENTS');
}
if (externalDirectives.layoutAdditionalElements) {
this.log.v('Setting EXTERNAL ADDITIONAL LAYOUT ELEMENTS');
config.layoutAdditionalElementsTemplate = externalDirectives.layoutAdditionalElements.template;
}
this.log.v('Template setup completed', config);
return config;
}
/**
* Sets up the participantPanelAfterLocalParticipant template
*/
private setupParticipantPanelAfterLocalParticipantTemplate(externalDirectives: ExternalDirectives): TemplateRef<any> | undefined {
if (externalDirectives.participantPanelAfterLocalParticipant) {
this.log.v('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.v('Setting EXTERNAL TOOLBAR');
return externalDirectives.toolbar.template;
} else {
this.log.v('Setting DEFAULT TOOLBAR');
return defaultTemplates.toolbar;
}
}
/**
* Sets up the panel template
*/
private setupPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.panel) {
this.log.v('Setting EXTERNAL PANEL');
return externalDirectives.panel.template;
} else {
this.log.v('Setting DEFAULT PANEL');
return defaultTemplates.panel;
}
}
/**
* Sets up the layout template
*/
private setupLayoutTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
if (externalDirectives.layout) {
this.log.v('Setting EXTERNAL LAYOUT');
return externalDirectives.layout.template;
} else {
this.log.v('Setting DEFAULT LAYOUT');
return defaultTemplates.layout;
}
}
/**
* Sets up the prejoin template
*/
private setupPreJoinTemplate(externalDirectives: ExternalDirectives): TemplateRef<any> | undefined {
if (externalDirectives.preJoin) {
this.log.v('Setting EXTERNAL PREJOIN');
return externalDirectives.preJoin.template;
} else {
this.log.v('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.v('Setting EXTERNAL CHAT PANEL');
return externalDirectives.chatPanel.template;
} else {
this.log.v('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.v('Setting EXTERNAL PARTICIPANTS PANEL');
return externalDirectives.participantsPanel.template;
} else {
this.log.v('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.v('Setting EXTERNAL ACTIVITIES PANEL');
return externalDirectives.activitiesPanel.template;
} else {
this.log.v('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.v('Setting EXTERNAL PARTICIPANT PANEL ITEM');
return externalDirectives.participantPanelItem.template;
} else {
this.log.v('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.v('Setting EXTERNAL STREAM');
return externalDirectives.stream.template;
} else {
this.log.v('Setting DEFAULT STREAM');
return defaultTemplates.stream;
}
}
/**
* Sets up templates for the PanelComponent
*/
setupPanelTemplates(
externalParticipantsPanel?: ParticipantsPanelDirective,
externalChatPanel?: ChatPanelDirective,
externalActivitiesPanel?: ActivitiesPanelDirective,
externalAdditionalPanels?: AdditionalPanelsDirective
): PanelTemplateConfiguration {
this.log.v('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.v('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.v('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.v('Setting up participants panel templates...');
return {
participantPanelItemTemplate: externalParticipantPanelItem?.template || defaultParticipantPanelItem,
participantPanelAfterLocalParticipantTemplate: externalParticipantPanelAfterLocalParticipant
};
}
/**
* Sets up templates for the ParticipantPanelItemComponent
*/
setupParticipantPanelItemTemplates(
externalParticipantPanelItemElements?: ParticipantPanelItemElementsDirective
): ParticipantPanelItemTemplateConfiguration {
this.log.v('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.v('Setting up session templates...');
return {
toolbarTemplate,
panelTemplate,
layoutTemplate
};
}
}

View File

@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../models/background-effect.model';
import { ParticipantService } from '../participant/participant.service';
import { OpenViduService } from '../openvidu/openvidu.service';
import { StorageService } from '../storage/storage.service';
import { LocalTrack } from 'livekit-client';
import { LocalVideoTrack, Track } from 'livekit-client';
import { BackgroundBlur, BackgroundOptions, ProcessorWrapper, VirtualBackground } from '@livekit/track-processors';
import { LoggerService } from '../logger/logger.service';
import { ILogger } from '../../models/logger.model';
import { ParticipantTrackPublication } from '../../models/participant.model';
/**
* @internal
@ -49,6 +49,7 @@ export class VirtualBackgroundService {
private log: ILogger;
constructor(
private participantService: ParticipantService,
private openviduService: OpenViduService,
private storageService: StorageService,
private loggerSrv: LoggerService
) {
@ -79,11 +80,12 @@ export class VirtualBackgroundService {
// If the background is already applied, do nothing
if (this.backgroundIsAlreadyApplied(bg.id)) return;
const cameraTracks = this.getCameraTracks();
if (!cameraTracks) {
this.log.e('No camera tracks found. Cannot apply background.');
const cameraTrack = this.getCameraTrack();
if (!cameraTrack) {
this.log.e('No camera track found. Cannot apply background.');
return;
}
try {
// If no effect is selected, remove the background
if (bg.type === EffectType.NONE) {
@ -91,8 +93,7 @@ export class VirtualBackgroundService {
return;
}
const localTrack = cameraTracks[0].track as LocalTrack;
const currentProcessor = localTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
const currentProcessor = cameraTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
// Check if the background is the same type as the previous one
if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) {
@ -104,7 +105,7 @@ export class VirtualBackgroundService {
this.log.e('No processor found for the background effect.');
return;
}
await this.applyProcessorToCameraTracks(cameraTracks, newProcessor);
await this.applyProcessorToCameraTrack(cameraTrack, newProcessor);
}
this.storageService.setBackground(bg.id);
@ -128,15 +129,14 @@ export class VirtualBackgroundService {
async removeBackground() {
if (this.isBackgroundApplied()) {
this.backgroundIdSelected.next('no_effect');
const tracks = this.participantService.getLocalParticipant()?.tracks;
const promises = tracks?.map(async (t) => {
const cameraTrack = this.getCameraTrack();
if (cameraTrack) {
try {
await (t.track as LocalTrack).stopProcessor();
await cameraTrack.stopProcessor();
} catch (e) {
this.log.w('Error stopping processor:', e);
}
});
await Promise.all(promises || []);
}
this.storageService.removeBackground();
}
}
@ -160,26 +160,41 @@ export class VirtualBackgroundService {
return undefined;
}
private async applyProcessorToCameraTracks(
cameraTracks: ParticipantTrackPublication[],
processor: ProcessorWrapper<BackgroundOptions>
) {
const promises = cameraTracks.map((track) => {
return (track.track as LocalTrack).setProcessor(processor);
});
/**
* Gets the camera track from either the published tracks (if in room) or local tracks (if in prejoin)
* @returns The camera LocalTrack or undefined if not found
* @private
*/
private getCameraTrack(): LocalVideoTrack | undefined {
// First, try to get from published tracks (when in room)
if (this.openviduService.isRoomConnected()) {
const localParticipant = this.participantService.getLocalParticipant();
const cameraTrackPublication = localParticipant?.cameraTracks?.[0];
if (cameraTrackPublication?.track) {
return cameraTrackPublication.track as LocalVideoTrack;
}
}
await Promise.all(promises || []);
// Fallback to local tracks (when in prejoin or tracks not yet published)
const localTracks = this.openviduService.getLocalTracks();
const cameraTrack = localTracks.find((track) => track.kind === Track.Kind.Video);
return cameraTrack as LocalVideoTrack | undefined;
}
/**
* Applies a background processor to the camera track
* @param cameraTrack The camera track to apply the processor to
* @param processor The background processor to apply
* @private
*/
private async applyProcessorToCameraTrack(cameraTrack: LocalVideoTrack, processor: ProcessorWrapper<BackgroundOptions>): Promise<void> {
await cameraTrack.setProcessor(processor);
}
private backgroundIsAlreadyApplied(backgroundId: string): boolean {
return backgroundId === this.backgroundIdSelected.getValue();
}
private getCameraTracks(): ParticipantTrackPublication[] | undefined {
const localParticipant = this.participantService.getLocalParticipant();
return localParticipant?.cameraTracks;
}
/**
* Replaces the current background effect with a new one by updating the processor options.
*

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);
}
@ -317,7 +332,8 @@ export class CallComponent implements OnInit {
}
}
async onRecordingStopRequested(event: RecordingStopRequestedEvent) {
this.appendElement('onRecordingStopRequested');
this.appendElement('onRecordingStopRequested-' + event.roomName);
console.warn('STOP RECORDING CLICKED', event);
try {
await this.restService.stopRecording(event);

View File

@ -271,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"
}
@ -367,7 +366,7 @@ Resources:
'/usr/local/bin/install.sh':
content: !Sub |
#!/bin/bash -x
OPENVIDU_VERSION=3.3.0
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.44.5
@ -410,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)"
@ -439,10 +437,9 @@ 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"
)
@ -638,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
@ -692,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
@ -936,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
@ -952,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

@ -270,7 +270,7 @@ var stringInterpolationParams = {
var installScriptTemplate = '''
#!/bin/bash -x
OPENVIDU_VERSION=3.3.0
OPENVIDU_VERSION=main
DOMAIN=
apt-get update && apt-get install -y \
@ -300,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)"
@ -329,10 +328,9 @@ 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"
)
@ -477,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)
@ -497,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"
@ -547,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")"
@ -569,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
'''
@ -1080,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

@ -3,9 +3,9 @@
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.3.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_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,17 @@ 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 LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.10.0}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,7 +181,7 @@ 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 \

View File

@ -473,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"
}
@ -662,7 +661,7 @@ Resources:
content: !Sub |
#!/bin/bash
set -e
OPENVIDU_VERSION=3.3.0
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.44.5
@ -734,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
@ -768,10 +766,9 @@ 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"
)
@ -970,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
@ -1029,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
@ -1696,7 +1691,7 @@ Resources:
ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
OpenViduMediaNodeToMasterMeetWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -1728,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
@ -1744,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

View File

@ -427,7 +427,7 @@ var stringInterpolationParamsMaster = {
var installScriptTemplateMaster = '''
#!/bin/bash -x
OPENVIDU_VERSION=3.3.0
OPENVIDU_VERSION=main
DOMAIN=
# Assume azure cli is installed
@ -490,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
@ -525,10 +524,9 @@ 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"
)
@ -681,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
@ -702,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
@ -753,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
@ -776,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
'''
@ -1863,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: [
@ -1959,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'
}
}
]
}
}
@ -1986,7 +1996,7 @@ resource masterToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/secur
]
destinationPortRange: '1935'
access: 'Allow'
priority: 150
priority: 160
direction: 'Inbound'
}
}
@ -2009,7 +2019,7 @@ resource masterToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/se
]
destinationPortRange: '5349'
access: 'Allow'
priority: 160
priority: 170
direction: 'Inbound'
}
}
@ -2032,7 +2042,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '7880'
access: 'Allow'
priority: 170
priority: 180
direction: 'Inbound'
}
}
@ -2055,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

@ -3,9 +3,9 @@
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.3.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_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,17 @@ 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 LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.10.0}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,7 +181,7 @@ 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 \

View File

@ -3,9 +3,9 @@
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.3.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_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,17 @@ 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 LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.10.0}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,7 +181,7 @@ 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 \

View File

@ -465,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",
@ -773,7 +772,7 @@ Resources:
content: !Sub |
#!/bin/bash -x
set -e
OPENVIDU_VERSION=3.3.0
OPENVIDU_VERSION=main
DOMAIN=
YQ_VERSION=v4.44.5
@ -867,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
@ -939,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')
@ -973,10 +970,9 @@ 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"
)
@ -1113,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
@ -1166,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
@ -2046,7 +2040,7 @@ Resources:
ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMasterToMasterDefaultAppIngress:
OpenViduMasterToMasterMeetIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
@ -2055,7 +2049,7 @@ Resources:
ToPort: 6080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
OpenViduMediaNodeToMasterMeetWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -2087,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
@ -2103,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

View File

@ -495,7 +495,7 @@ var stringInterpolationParamsMaster4 = {
var installScriptTemplateMaster = '''
#!/bin/bash -x
set -e
OPENVIDU_VERSION=3.3.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,10 +658,9 @@ 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"
)
@ -804,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
@ -825,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
@ -876,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
@ -899,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
'''
@ -2728,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: [
@ -2751,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: [
@ -2847,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'
}
}
]
}
}
@ -2870,7 +2879,7 @@ resource loadBalancerToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups
]
destinationPortRange: '1945'
access: 'Allow'
priority: 150
priority: 160
direction: 'Inbound'
}
}
@ -2889,7 +2898,7 @@ resource loadBalancerToMediaHealthcheckIngress 'Microsoft.Network/networkSecurit
]
destinationPortRange: '9092'
access: 'Allow'
priority: 160
priority: 170
direction: 'Inbound'
}
}
@ -2908,7 +2917,7 @@ resource loadBalancerToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGro
]
destinationPortRange: '5349'
access: 'Allow'
priority: 170
priority: 180
direction: 'Inbound'
}
}
@ -2927,7 +2936,7 @@ resource loadBalancerToMediaTurnTlsHealthCheckIngress 'Microsoft.Network/network
]
destinationPortRange: '7880'
access: 'Allow'
priority: 180
priority: 190
direction: 'Inbound'
}
}
@ -2950,7 +2959,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '7880'
access: 'Allow'
priority: 190
priority: 200
direction: 'Inbound'
}
}
@ -2973,7 +2982,7 @@ resource masterToMediaClientIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '8080'
access: 'Allow'
priority: 200
priority: 210
direction: 'Inbound'
}
}

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